001/* 002 * Copyright 2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2019 Ping Identity Corporation 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.ldap.sdk; 022 023 024 025import java.net.InetAddress; 026import java.net.UnknownHostException; 027import java.util.Arrays; 028import java.util.Map; 029import java.util.concurrent.ConcurrentHashMap; 030import java.util.concurrent.atomic.AtomicReference; 031 032import com.unboundid.util.Debug; 033import com.unboundid.util.ObjectPair; 034import com.unboundid.util.StaticUtils; 035import com.unboundid.util.ThreadLocalRandom; 036import com.unboundid.util.ThreadSafety; 037import com.unboundid.util.ThreadSafetyLevel; 038 039 040 041/** 042 * This class provides an implementation of a {@code NameResolver} that will 043 * cache lookups to potentially improve performance and provide a degree of 044 * resiliency against name service outages. 045 */ 046@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 047public final class CachingNameResolver 048 extends NameResolver 049{ 050 /** 051 * The default timeout that will be used if none is specified. 052 */ 053 private static final int DEFAULT_TIMEOUT_MILLIS = 3_600_000; // 1 hour 054 055 056 057 // A cached version of the address of the local host system. 058 private final AtomicReference<ObjectPair<Long,InetAddress>> localHostAddress; 059 060 // A cached version of the loopback address. 061 private final AtomicReference<ObjectPair<Long,InetAddress>> loopbackAddress; 062 063 // A map that associates IP addresses with their canonical host names. The 064 // key will be the IP address, and the value will be an object pair that 065 // associates the time that the cache record expires with the cached canonical 066 // host name for the IP address. 067 private final Map<InetAddress,ObjectPair<Long,String>> addressToNameMap; 068 069 // A map that associates host names with the set of all associated IP 070 // addresses. The key will be an all-lowercase representation of the host 071 // name, and the value will be an object pair that associates the time that 072 // the cache record expires with the cached set of IP addresses for the host 073 // name. 074 private final Map<String,ObjectPair<Long,InetAddress[]>> nameToAddressMap; 075 076 // The length of time, in milliseconds, that a cached record should be 077 // considered valid. 078 private final long timeoutMillis; 079 080 081 082 /** 083 * Creates a new instance of this caching name resolver that will use a 084 * default timeout. 085 */ 086 public CachingNameResolver() 087 { 088 this(DEFAULT_TIMEOUT_MILLIS); 089 } 090 091 092 093 /** 094 * Creates a new instance of this caching name resolver that will use the 095 * specified timeout. 096 * 097 * @param timeoutMillis The length of time, in milliseconds, that cache 098 * records should be considered valid. It must be 099 * greater than zero. If a record has been in the 100 * cache for less than this period of time, then the 101 * cached record will be used instead of making a name 102 * service call. If a record has been in the cache 103 * for longer than this period of time, then the 104 * cached record will only be used if it is not 105 * possible to get an updated version of the record 106 * from the name service. 107 */ 108 public CachingNameResolver(final int timeoutMillis) 109 { 110 this.timeoutMillis = timeoutMillis; 111 localHostAddress = new AtomicReference<>(); 112 loopbackAddress = new AtomicReference<>(); 113 addressToNameMap = new ConcurrentHashMap<>(20); 114 nameToAddressMap = new ConcurrentHashMap<>(20); 115 } 116 117 118 119 /** 120 * Retrieves the length of time, in milliseconds, that cache records should 121 * be considered valid. If a record has been in the cache for less than this 122 * period fo time, then the cached record will be used instead of making a 123 * name service call. If a record has been in the cache for longer than this 124 * period of time, then the cached record will only be used if it is not 125 * possible to get an updated version of the record from the name service. 126 * 127 * @return The length of time, in milliseconds, that cache records should be 128 * considered valid. 129 */ 130 public int getTimeoutMillis() 131 { 132 return (int) timeoutMillis; 133 } 134 135 136 137 /** 138 * {@inheritDoc} 139 */ 140 @Override() 141 public InetAddress getByName(final String host) 142 throws UnknownHostException, SecurityException 143 { 144 // Use the getAllByNameInternal method to get all addresses associated with 145 // the provided name. If there's only one name associated with the address, 146 // then return that name. If there are multiple names, then return one at 147 // random. 148 final InetAddress[] addresses = getAllByNameInternal(host); 149 if (addresses.length == 1) 150 { 151 return addresses[0]; 152 } 153 154 return addresses[ThreadLocalRandom.get().nextInt(addresses.length)]; 155 } 156 157 158 159 /** 160 * {@inheritDoc} 161 */ 162 @Override() 163 public InetAddress[] getAllByName(final String host) 164 throws UnknownHostException, SecurityException 165 { 166 // Create a defensive copy of the address array so that the caller cannot 167 // alter the original. 168 final InetAddress[] addresses = getAllByNameInternal(host); 169 return Arrays.copyOf(addresses, addresses.length); 170 } 171 172 173 174 /** 175 * Retrieves an array of {@code InetAddress} objects that encapsulate all 176 * known IP addresses associated with the provided host name. 177 * 178 * @param host The host name for which to retrieve the corresponding 179 * {@code InetAddress} objects. It can be a resolvable name or 180 * a textual representation of an IP address. If the provided 181 * name is the textual representation of an IPv6 address, then 182 * it can use either the form described in RFC 2373 or RFC 2732, 183 * or it can be an IPv6 scoped address. If it is {@code null}, 184 * then the returned address should represent an address of the 185 * loopback interface. 186 * 187 * @return An array of {@code InetAddress} objects that encapsulate all known 188 * IP addresses associated with the provided host name. 189 * 190 * @throws UnknownHostException If the provided name cannot be resolved to 191 * its corresponding IP addresses. 192 * 193 * @throws SecurityException If a security manager prevents the name 194 * resolution attempt. 195 */ 196 public InetAddress[] getAllByNameInternal(final String host) 197 throws UnknownHostException, SecurityException 198 { 199 // Get an all-lowercase representation of the provided host name. Note that 200 // the provided host name can be null, so we need to handle that possibility 201 // as well. 202 final String lowerHost; 203 if (host == null) 204 { 205 lowerHost = ""; 206 } 207 else 208 { 209 lowerHost = StaticUtils.toLowerCase(host); 210 } 211 212 213 // Get the appropriate record from the cache. If there isn't a cached 214 // then do perform a name service lookup and cache the result before 215 // returning it. 216 final ObjectPair<Long,InetAddress[]> cachedRecord = 217 nameToAddressMap.get(lowerHost); 218 if (cachedRecord == null) 219 { 220 return lookUpAndCache(host, lowerHost); 221 } 222 223 224 // If the cached record is not expired, then return its set of addresses. 225 if (System.currentTimeMillis() <= cachedRecord.getFirst()) 226 { 227 return cachedRecord.getSecond(); 228 } 229 230 231 // The cached record is expired. Try to get a new record from the name 232 // service, and if that attempt succeeds, then cache the result before 233 // returning it. If the name service lookup fails, then fall back to using 234 // the cached addresses even though they're expired. 235 try 236 { 237 return lookUpAndCache(host, lowerHost); 238 } 239 catch (final Exception e) 240 { 241 Debug.debugException(e); 242 return cachedRecord.getSecond(); 243 } 244 } 245 246 247 248 /** 249 * Performs a name service lookup to retrieve all addresses for the provided 250 * name. If the lookup succeeds, then cache the result before returning it. 251 * 252 * @param host The host name for which to retrieve the corresponding 253 * {@code InetAddress} objects. It can be a resolvable 254 * name or a textual representation of an IP address. If 255 * the provided name is the textual representation of an 256 * IPv6 address, then it can use either the form described 257 * in RFC 2373 or RFC 2732, or it can be an IPv6 scoped 258 * address. If it is {@code null}, then the returned 259 * address should represent an address of the loopback 260 * interface. 261 * @param lowerHost An all-lowercase representation of the provided host 262 * name, or an empty string if the provided host name is 263 * {@code null}. This will be the key under which the 264 * record will be stored in the cache. 265 * 266 * @return An array of {@code InetAddress} objects that represent all 267 * addresses for the provided name. 268 * 269 * @throws UnknownHostException If the provided name cannot be resolved to 270 * its corresponding IP addresses. 271 * 272 * @throws SecurityException If a security manager prevents the name 273 * resolution attempt. 274 */ 275 private InetAddress[] lookUpAndCache(final String host, 276 final String lowerHost) 277 throws UnknownHostException, SecurityException 278 { 279 final InetAddress[] addresses = InetAddress.getAllByName(host); 280 final long cacheRecordExpirationTime = 281 System.currentTimeMillis() + timeoutMillis; 282 final ObjectPair<Long,InetAddress[]> cacheRecord = 283 new ObjectPair<>(cacheRecordExpirationTime, addresses); 284 nameToAddressMap.put(lowerHost, cacheRecord); 285 return addresses; 286 } 287 288 289 290 /** 291 * {@inheritDoc} 292 */ 293 @Override() 294 public String getHostName(final InetAddress inetAddress) 295 { 296 // The default InetAddress.getHostName() method has the potential to perform 297 // a name service lookup, which we want to avoid if at all possible. 298 // However, if the provided inet address has a name associated with it, then 299 // we'll want to use it. Fortunately, we can tell if the provided address 300 // has a name associated with it by looking at the toString method, which is 301 // defined in the specification to be "hostName/ipAddress" if there is a 302 // host name, or just "/ipAddress" if there is no associated host name and a 303 // name service lookup would be required. So look at the string 304 // representation to extract the host name if it's available, but then fall 305 // back to using the canonical name otherwise. 306 final String stringRepresentation = String.valueOf(inetAddress); 307 final int lastSlashPos = stringRepresentation.lastIndexOf('/'); 308 if (lastSlashPos > 0) 309 { 310 return stringRepresentation.substring(0, lastSlashPos); 311 } 312 313 return getCanonicalHostName(inetAddress); 314 } 315 316 317 318 /** 319 * {@inheritDoc} 320 */ 321 @Override() 322 public String getCanonicalHostName(final InetAddress inetAddress) 323 { 324 // Get the appropriate record from the cache. If there isn't a cached 325 // then do perform a name service lookup and cache the result before 326 // returning it. 327 final ObjectPair<Long,String> cachedRecord = 328 addressToNameMap.get(inetAddress); 329 if (cachedRecord == null) 330 { 331 return lookUpAndCache(inetAddress, null); 332 } 333 334 335 // If the cached record is not expired, then return its canonical host name. 336 if (System.currentTimeMillis() <= cachedRecord.getFirst()) 337 { 338 return cachedRecord.getSecond(); 339 } 340 341 342 // The cached record is expired. Try to get a new record from the name 343 // service, and if that attempt succeeds, then cache the result before 344 // returning it. If the name service lookup fails, then fall back to using 345 // the cached canonical host name even though it's expired. 346 return lookUpAndCache(inetAddress, cachedRecord.getSecond()); 347 } 348 349 350 351 /** 352 * Performs a name service lookup to retrieve the canonical host name for the 353 * provided {@code InetAddress} object. If the lookup succeeds, then cache 354 * the result before returning it. If the lookup fails (which will be 355 * indicated by the returned name matching the textual representation of the 356 * IP address for the provided {@code InetAddress} object) and the provided 357 * cached result is not {@code null}, then the cached name will be returned, 358 * but the cache will not be updated. 359 * 360 * @param inetAddress The address to use when performing the name service 361 * lookup to retrieve the canonical name. It must not be 362 * {@code null}. 363 * @param cachedName The cached name to be returned if the name service 364 * lookup fails. It may be {@code null} if there is no 365 * cached name for the provided address. 366 * 367 * @return The canonical host name resulting from the name service lookup, 368 * the cached name if the lookup failed and the cached name was 369 * non-{@code null}, or a textual representation of the IP address as 370 * a last resort. 371 */ 372 private String lookUpAndCache(final InetAddress inetAddress, 373 final String cachedName) 374 { 375 final String canonicalHostName = inetAddress.getCanonicalHostName(); 376 if (canonicalHostName.equals(inetAddress.getHostAddress())) 377 { 378 // The name that we got back is a textual representation of the IP 379 // address. This suggests that either the canonical lookup failed because 380 // of a problem while communicating with the name service, or that the 381 // IP address is not mapped to a name. If a cached name was provided, 382 // then we'll return that. Otherwise, we'll fall back to returning the 383 // textual address. In either case, we won't alter the cache. 384 if (cachedName == null) 385 { 386 return canonicalHostName; 387 } 388 else 389 { 390 return cachedName; 391 } 392 } 393 else 394 { 395 // The name service lookup succeeded, so cache the result before returning 396 // it. 397 final long cacheRecordExpirationTime = 398 System.currentTimeMillis() + timeoutMillis; 399 final ObjectPair<Long,String> cacheRecord = 400 new ObjectPair<>(cacheRecordExpirationTime, canonicalHostName); 401 addressToNameMap.put(inetAddress, cacheRecord); 402 return canonicalHostName; 403 } 404 } 405 406 407 408 /** 409 * {@inheritDoc} 410 */ 411 @Override() 412 public InetAddress getLocalHost() 413 throws UnknownHostException, SecurityException 414 { 415 // If we don't have a cached version of the local host address, then 416 // make a name service call to resolve it and store it in the cache before 417 // returning it. 418 final ObjectPair<Long,InetAddress> cachedAddress = localHostAddress.get(); 419 if (cachedAddress == null) 420 { 421 final InetAddress localHost = InetAddress.getLocalHost(); 422 final long expirationTime = 423 System.currentTimeMillis() + timeoutMillis; 424 localHostAddress.set(new ObjectPair<Long,InetAddress>(expirationTime, 425 localHost)); 426 return localHost; 427 } 428 429 430 // If the cached address has not yet expired, then use the cached address. 431 final long cachedRecordExpirationTime = cachedAddress.getFirst(); 432 if (System.currentTimeMillis() <= cachedRecordExpirationTime) 433 { 434 return cachedAddress.getSecond(); 435 } 436 437 438 // The cached address is expired. Make a name service call to get it again 439 // and cache that result if we can. If the name service lookup fails, then 440 // return the cached version even though it's expired. 441 try 442 { 443 final InetAddress localHost = InetAddress.getLocalHost(); 444 final long expirationTime = 445 System.currentTimeMillis() + timeoutMillis; 446 localHostAddress.set(new ObjectPair<Long,InetAddress>(expirationTime, 447 localHost)); 448 return localHost; 449 } 450 catch (final Exception e) 451 { 452 Debug.debugException(e); 453 return cachedAddress.getSecond(); 454 } 455 } 456 457 458 459 /** 460 * {@inheritDoc} 461 */ 462 @Override() 463 public InetAddress getLoopbackAddress() 464 { 465 // If we don't have a cached version of the loopback address, then make a 466 // name service call to resolve it and store it in the cache before 467 // returning it. 468 final ObjectPair<Long,InetAddress> cachedAddress = loopbackAddress.get(); 469 if (cachedAddress == null) 470 { 471 final InetAddress address = InetAddress.getLoopbackAddress(); 472 final long expirationTime = 473 System.currentTimeMillis() + timeoutMillis; 474 loopbackAddress.set(new ObjectPair<Long,InetAddress>(expirationTime, 475 address)); 476 return address; 477 } 478 479 480 // If the cached address has not yet expired, then use the cached address. 481 final long cachedRecordExpirationTime = cachedAddress.getFirst(); 482 if (System.currentTimeMillis() <= cachedRecordExpirationTime) 483 { 484 return cachedAddress.getSecond(); 485 } 486 487 488 // The cached address is expired. Make a name service call to get it again 489 // and cache that result if we can. If the name service lookup fails, then 490 // return the cached version even though it's expired. 491 try 492 { 493 final InetAddress address = InetAddress.getLoopbackAddress(); 494 final long expirationTime = 495 System.currentTimeMillis() + timeoutMillis; 496 loopbackAddress.set(new ObjectPair<Long,InetAddress>(expirationTime, 497 address)); 498 return address; 499 } 500 catch (final Exception e) 501 { 502 Debug.debugException(e); 503 return cachedAddress.getSecond(); 504 } 505 } 506 507 508 509 /** 510 * Clears all information from the name resolver cache. 511 */ 512 public void clearCache() 513 { 514 localHostAddress.set(null); 515 loopbackAddress.set(null); 516 addressToNameMap.clear(); 517 nameToAddressMap.clear(); 518 } 519 520 521 522 /** 523 * Retrieves a handle to the map used to cache address-to-name lookups. This 524 * method should only be used for unit testing. 525 * 526 * @return A handle to the address-to-name map. 527 */ 528 Map<InetAddress,ObjectPair<Long,String>> getAddressToNameMap() 529 { 530 return addressToNameMap; 531 } 532 533 534 535 /** 536 * Retrieves a handle to the map used to cache name-to-address lookups. This 537 * method should only be used for unit testing. 538 * 539 * @return A handle to the name-to-address map. 540 */ 541 Map<String,ObjectPair<Long,InetAddress[]>> getNameToAddressMap() 542 { 543 return nameToAddressMap; 544 } 545 546 547 548 /** 549 * Retrieves a handle to the {@code AtomicReference} used to cache the local 550 * host address. This should only be used for testing. 551 * 552 * @return A handle to the {@code AtomicReference} used to cache the local 553 * host address. 554 */ 555 AtomicReference<ObjectPair<Long,InetAddress>> getLocalHostAddressReference() 556 { 557 return localHostAddress; 558 } 559 560 561 562 /** 563 * Retrieves a handle to the {@code AtomicReference} used to cache the 564 * loopback address. This should only be used for testing. 565 * 566 * @return A handle to the {@code AtomicReference} used to cache the 567 * loopback address. 568 */ 569 AtomicReference<ObjectPair<Long,InetAddress>> getLoopbackAddressReference() 570 { 571 return loopbackAddress; 572 } 573 574 575 576 /** 577 * {@inheritDoc} 578 */ 579 @Override() 580 public void toString(final StringBuilder buffer) 581 { 582 buffer.append("CachingNameResolver(timeoutMillis="); 583 buffer.append(timeoutMillis); 584 buffer.append(')'); 585 } 586}