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