001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2009 Sun Microsystems, Inc.
015 * Portions copyright 2011-2014 ForgeRock AS.
016 */
017package org.forgerock.opendj.ldap;
018
019import static com.forgerock.opendj.ldap.CoreMessages.*;
020
021import java.lang.reflect.Method;
022import java.net.Inet6Address;
023import java.net.InetAddress;
024import java.net.UnknownHostException;
025import java.util.BitSet;
026import java.util.Collection;
027
028import org.forgerock.i18n.LocalizedIllegalArgumentException;
029
030/**
031 * An address mask can be used to perform efficient comparisons against IP
032 * addresses to determine whether a particular IP address is in a given range.
033 */
034public final class AddressMask {
035    /**
036     * Types of rules we have. IPv4 - ipv4 rule IPv6 - ipv6 rule (begin with '['
037     * or contains an ':'). HOST - hostname match (foo.sun.com) HOSTPATTERN -
038     * host pattern match (begin with '.') ALLWILDCARD - *.*.*.* (first HOST is
039     * applied then ipv4)
040     */
041    enum RuleType {
042        ALLWILDCARD, HOST, HOSTPATTERN, IPv4, IPv6
043    }
044
045    /** IPv4 values for number of bytes and max CIDR prefix. */
046    private static final int IN4ADDRSZ = 4;
047    private static final int IPV4MAXPREFIX = 32;
048
049    /** IPv6 values for number of bytes and max CIDR prefix. */
050    private static final int IN6ADDRSZ = 16;
051    private static final int IPV6MAXPREFIX = 128;
052
053    /**
054     * Returns {@code true} if an address matches any of the provided address
055     * masks.
056     *
057     * @param address
058     *            The address.
059     * @param masks
060     *            A collection of address masks to check.
061     * @return {@code true} if an address matches any of the provided address
062     *         masks.
063     */
064    public static boolean matchesAny(final Collection<AddressMask> masks, final InetAddress address) {
065        if (address != null) {
066            for (final AddressMask mask : masks) {
067                if (mask.matches(address)) {
068                    return true;
069                }
070            }
071        }
072        return false;
073    }
074
075    /**
076     * Parses the provided string as an address mask.
077     *
078     * @param mask
079     *            The address mask string to be parsed.
080     * @return The parsed address mask.
081     * @throws LocalizedIllegalArgumentException
082     *             If the provided string cannot be decoded as an address mask.
083     */
084    public static AddressMask valueOf(final String mask) {
085        return new AddressMask(mask);
086    }
087
088    /** Array that holds each component of a hostname. */
089    private String[] hostName;
090
091    /** Holds a hostname pattern (ie, rule that begins with '.');'. */
092    private String hostPattern;
093
094    /** Holds binary representations of rule and mask respectively. */
095    private byte[] ruleMask, prefixMask;
096
097    /** Holds string passed into the constructor. */
098    private final String ruleString;
099
100    /** Type of rule determined. */
101    private RuleType ruleType;
102
103    /** Bit array that holds wildcard info for above binary arrays. */
104    private final BitSet wildCard = new BitSet();
105
106    private AddressMask(final String rule) {
107        determineRuleType(rule);
108        switch (ruleType) {
109        case IPv6:
110            processIPv6(rule);
111            break;
112
113        case IPv4:
114            processIpv4(rule);
115            break;
116
117        case HOST:
118            processHost(rule);
119            break;
120
121        case HOSTPATTERN:
122            processHostPattern(rule);
123            break;
124
125        case ALLWILDCARD:
126            processAllWilds(rule);
127        }
128        ruleString = rule;
129    }
130
131    /**
132     * Returns {@code true} if this address mask matches the provided address.
133     *
134     * @param address
135     *            The address.
136     * @return {@code true} if this address mask matches the provided address.
137     */
138    public boolean matches(final InetAddress address) {
139        boolean ret = false;
140
141        switch (ruleType) {
142        case IPv6:
143        case IPv4:
144            // this Address mask is an IPv4 rule
145            ret = matchAddress(address.getAddress());
146            break;
147
148        case HOST:
149            // HOST rule use hostname
150            ret = matchHostName(address.getHostName());
151            break;
152
153        case HOSTPATTERN:
154            // HOSTPATTERN rule
155            ret = matchPattern(address.getHostName());
156            break;
157
158        case ALLWILDCARD:
159            // first try ipv4 addr match, then hostname
160            ret = matchAddress(address.getAddress());
161            if (!ret) {
162                ret = matchHostName(address.getHostName());
163            }
164            break;
165        }
166        return ret;
167    }
168
169    /**
170     * Returns the string representation of this address mask.
171     *
172     * @return The string representation of this address mask.
173     */
174    @Override
175    public String toString() {
176        return ruleString;
177    }
178
179    /**
180     * Try to determine what type of rule string this is. See RuleType above for
181     * valid types.
182     *
183     * @param ruleString
184     *            The rule string to be examined.
185     * @throws LocalizedIllegalArgumentException
186     *             If the rule type cannot be determined from the rule string.
187     */
188    private void determineRuleType(final String ruleString) {
189        // Rule ending with '.' is invalid'
190        if (ruleString.endsWith(".")) {
191            throw genericDecodeError();
192        } else if (ruleString.startsWith(".")) {
193            ruleType = RuleType.HOSTPATTERN;
194        } else if (ruleString.startsWith("[") || ruleString.indexOf(':') != -1) {
195            ruleType = RuleType.IPv6;
196        } else {
197            int wildcardsCount = 0;
198            final String[] s = ruleString.split("\\.", -1);
199            /*
200             * Try to figure out how many wildcards and if the rule is hostname
201             * (can't begin with digit) or ipv4 address. Default to IPv4 ruletype.
202             */
203            ruleType = RuleType.HOST;
204            for (final String value : s) {
205                if ("*".equals(value)) {
206                    wildcardsCount++;
207                    continue;
208                }
209                // Looks like an ipv4 address
210                if (Character.isDigit(value.charAt(0))) {
211                    ruleType = RuleType.IPv4;
212                    break;
213                }
214            }
215            // All wildcards (*.*.*.*)
216            if (wildcardsCount == s.length) {
217                ruleType = RuleType.ALLWILDCARD;
218            }
219        }
220    }
221
222    /**
223     * Try to match remote client address using prefix mask and rule mask.
224     *
225     * @param remoteMask
226     *            The byte array with remote client address.
227     * @return <CODE>true</CODE> if remote client address matches or
228     *         <CODE>false</CODE>if not.
229     */
230    private boolean matchAddress(final byte[] remoteMask) {
231        if (ruleType == RuleType.ALLWILDCARD) {
232            return true;
233        }
234        if (prefixMask == null) {
235            return false;
236        }
237        if (remoteMask.length != prefixMask.length) {
238            return false;
239        }
240        for (int i = 0; i < prefixMask.length; i++) {
241            if (!wildCard.get(i)
242                    && (ruleMask[i] & prefixMask[i]) != (remoteMask[i] & prefixMask[i])) {
243                return false;
244            }
245        }
246        return true;
247    }
248
249    /**
250     * Try to match remote client host name against rule host name.
251     *
252     * @param remoteHostName
253     *            The remote host name string.
254     * @return <CODE>true</CODE>if the remote client host name matches
255     *         <CODE>false</CODE> if it does not.
256     */
257    private boolean matchHostName(final String remoteHostName) {
258        final String[] s = remoteHostName.split("\\.", -1);
259        if (s.length != hostName.length) {
260            return false;
261        }
262        if (ruleType == RuleType.ALLWILDCARD) {
263            return true;
264        }
265        for (int i = 0; i < s.length; i++) {
266            // skip if wildcard
267            if (!"*".equals(hostName[i])
268                    && !s[i].equalsIgnoreCase(hostName[i])) {
269                return false;
270            }
271        }
272        return true;
273    }
274
275    /**
276     * Try to match remote host name string against the pattern rule.
277     *
278     * @param remoteHostName
279     *            The remote client host name.
280     * @return <CODE>true</CODE>if the remote host name matches or
281     *         <CODE>false</CODE>if not.
282     */
283    private boolean matchPattern(final String remoteHostName) {
284        final int len = remoteHostName.length() - hostPattern.length();
285        return len > 0
286                && remoteHostName.regionMatches(true, len, hostPattern, 0, hostPattern.length());
287    }
288
289    /**
290     * Build the prefix mask of prefix len bits set in the array.
291     *
292     * @param prefix
293     *            The len of the prefix to use.
294     */
295    private void prefixMask(int prefix) {
296        int i;
297        for (i = 0; prefix > 8; i++) {
298            this.prefixMask[i] = (byte) 0xff;
299            prefix -= 8;
300        }
301        this.prefixMask[i] = (byte) (0xff << 8 - prefix);
302    }
303
304    /**
305     * The rule string is all wildcards. Set both address wildcard bitmask and
306     * hostname wildcard array.
307     *
308     * @param rule
309     *            The rule string containing all wildcards.
310     */
311    private void processAllWilds(final String rule) {
312        final String[] s = rule.split("\\.", -1);
313        if (s.length == IN4ADDRSZ) {
314            for (int i = 0; i < IN4ADDRSZ; i++) {
315                wildCard.set(i);
316            }
317        }
318        hostName = rule.split("\\.", -1);
319    }
320
321    /**
322     * Examine rule string and build a hostname string array of its parts.
323     *
324     * @param rule
325     *            The rule string.
326     * @throws LocalizedIllegalArgumentException
327     *             If the rule string is not a valid host name.
328     */
329    private void processHost(final String rule) {
330        // Note that '*' is valid in host rule
331        final String[] s = rule.split("^[0-9a-zA-z-.*]+");
332        if (s.length > 0) {
333            throw genericDecodeError();
334        }
335        hostName = rule.split("\\.", -1);
336    }
337
338    /**
339     * Examine the rule string of a host pattern and set the host pattern from
340     * the rule.
341     *
342     * @param rule
343     *            The rule string to examine.
344     * @throws LocalizedIllegalArgumentException
345     *             If the rule string is not a valid host pattern rule.
346     */
347    private void processHostPattern(final String rule) {
348        // quick check for invalid chars like " "
349        final String[] s = rule.split("^[0-9a-zA-z-.]+");
350        if (s.length > 0) {
351            throw genericDecodeError();
352        }
353        hostPattern = rule;
354    }
355
356    /**
357     * The rule string is an IPv4 rule. Build both the prefix mask array and
358     * rule mask from the string.
359     *
360     * @param rule
361     *            The rule string containing the IPv4 rule.
362     * @throws LocalizedIllegalArgumentException
363     *             If the rule string is not a valid IPv4 rule.
364     */
365    private void processIpv4(final String rule) {
366        final String[] s = rule.split("/", -1);
367        this.ruleMask = new byte[IN4ADDRSZ];
368        this.prefixMask = new byte[IN4ADDRSZ];
369        prefixMask(processPrefix(s, IPV4MAXPREFIX));
370        processIPv4Subnet(s.length == 0 ? rule : s[0]);
371    }
372
373    /**
374     * Examine the subnet part of a rule string and build a byte array
375     * representation of it.
376     *
377     * @param subnet
378     *            The subnet string part of the rule.
379     * @throws LocalizedIllegalArgumentException
380     *             If the subnet string is not a valid IPv4 subnet string.
381     */
382    private void processIPv4Subnet(final String subnet) {
383        final String[] s = subnet.split("\\.", -1);
384        try {
385            // Make sure we have four parts
386            if (s.length != IN4ADDRSZ) {
387                throw genericDecodeError();
388            }
389            for (int i = 0; i < IN4ADDRSZ; i++) {
390                final String quad = s[i].trim();
391                if ("*".equals(quad)) {
392                    wildCard.set(i); // see wildcard mark bitset
393                } else {
394                    final long val = Integer.parseInt(quad);
395                    // must be between 0-255
396                    if (val < 0 || val > 0xff) {
397                        throw genericDecodeError();
398                    }
399                    ruleMask[i] = (byte) (val & 0xff);
400                }
401            }
402        } catch (final NumberFormatException nfex) {
403            throw genericDecodeError();
404        }
405    }
406
407    /**
408     * The rule string is an IPv6 rule. Build both the prefix mask array and
409     * rule mask from the string.
410     *
411     * @param rule
412     *            The rule string containing the IPv6 rule.
413     * @throws LocalizedIllegalArgumentException
414     *             If the rule string is not a valid IPv6 rule.
415     */
416    private void processIPv6(final String rule) {
417        final String[] s = rule.split("/", -1);
418        final String address = s[0];
419
420        // Try to avoid calling InetAddress.getByName() because it may do a reverse lookup.
421        final String ipv6Literal;
422        if (address.charAt(0) == '[' && address.charAt(address.length() - 1) == ']') {
423            // isIPv6LiteralAddress must be invoked without surrounding brackets.
424            ipv6Literal = address.substring(1, address.length() - 1);
425        } else {
426            ipv6Literal = address;
427        }
428
429        boolean isValid;
430        try {
431            // Use reflection to avoid dependency on Sun JRE.
432            final Class<?> ipUtils = Class.forName("sun.net.util.IPAddressUtil");
433            final Method method = ipUtils.getMethod("isIPv6LiteralAddress", String.class);
434            isValid = (Boolean) method.invoke(null, ipv6Literal);
435        } catch (Exception e) {
436            /*
437             * Unable to invoke Sun private API. Assume it's ok, but accept that
438             * a DNS query may be performed if it is not valid.
439             */
440            isValid = true;
441        }
442        if (!isValid) {
443            throw genericDecodeError();
444        }
445
446        final InetAddress addr;
447        try {
448            addr = InetAddress.getByName(address);
449        } catch (final UnknownHostException ex) {
450            throw genericDecodeError();
451        }
452        if (addr instanceof Inet6Address) {
453            this.ruleType = RuleType.IPv6;
454            final Inet6Address addr6 = (Inet6Address) addr;
455            this.ruleMask = addr6.getAddress();
456            this.prefixMask = new byte[IN6ADDRSZ];
457            prefixMask(processPrefix(s, IPV6MAXPREFIX));
458        } else {
459            /*
460             * The address might be an IPv4-compat address. Throw an error if
461             * the rule has a prefix.
462             */
463            if (s.length == 2) {
464                throw genericDecodeError();
465            }
466            this.ruleMask = addr.getAddress();
467            this.ruleType = RuleType.IPv4;
468            this.prefixMask = new byte[IN4ADDRSZ];
469            prefixMask(processPrefix(s, IPV4MAXPREFIX));
470        }
471    }
472
473    /**
474     * Examine rule string for correct prefix usage.
475     *
476     * @param s
477     *            The string array with rule string add and prefix strings.
478     * @param maxPrefix
479     *            The max value the prefix can be.
480     * @return The prefix integer value.
481     * @throws LocalizedIllegalArgumentException
482     *             If the string array and prefix are not valid.
483     */
484    private int processPrefix(final String[] s, final int maxPrefix) {
485        int prefix = maxPrefix;
486        try {
487            // can only have one prefix value and a subnet string
488            if (s.length < 1 || s.length > 2) {
489                throw genericDecodeError();
490            } else if (s.length == 2) {
491                // can't have wildcard with a prefix
492                if (s[0].indexOf('*') > -1) {
493                    throw new LocalizedIllegalArgumentException(
494                            ERR_ADDRESSMASK_WILDCARD_DECODE_ERROR.get());
495                }
496                prefix = Integer.parseInt(s[1]);
497            }
498            // must be between 0-maxprefix
499            if (prefix < 0 || prefix > maxPrefix) {
500                throw new LocalizedIllegalArgumentException(ERR_ADDRESSMASK_PREFIX_DECODE_ERROR
501                        .get());
502            }
503        } catch (final NumberFormatException nfex) {
504            throw genericDecodeError();
505        }
506        return prefix;
507    }
508
509    private LocalizedIllegalArgumentException genericDecodeError() {
510        return new LocalizedIllegalArgumentException(ERR_ADDRESSMASK_FORMAT_DECODE_ERROR.get());
511    }
512}