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