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}