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}