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}