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}