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