001/*
002 * Copyright 2011-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2011-2014 UnboundID Corp.
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.util.Collections;
026import java.util.Hashtable;
027import java.util.Map;
028import java.util.Properties;
029import javax.naming.Context;
030import javax.net.SocketFactory;
031
032import com.unboundid.util.Debug;
033import com.unboundid.util.NotMutable;
034import com.unboundid.util.ThreadSafety;
035import com.unboundid.util.ThreadSafetyLevel;
036
037
038
039/**
040 * This class provides a server set implementation that can discover information
041 * about available directory servers through DNS SRV records as described in
042 * <A HREF="http://www.ietf.org/rfc/rfc2782.txt">RFC 2782</A>.  DNS SRV records
043 * make it possible for clients to use the domain name system to discover
044 * information about the systems that provide a given service, which can help
045 * avoid the need to explicitly configure clients with the addresses of the
046 * appropriate set of directory servers.
047 * <BR><BR>
048 * The standard service name used to reference LDAP directory servers is
049 * "_ldap._tcp".  If client systems have DNS configured properly with an
050 * appropriate search domain, then this may be all that is needed to discover
051 * any available directory servers.  Alternately, a record name of
052 * "_ldap._tcp.example.com" may be used to request DNS information about LDAP
053 * servers for the example.com domain.  However, there is no technical
054 * requirement that "_ldap._tcp" must be used for this purpose, and it may make
055 * sense to use a different name if there is something special about the way
056 * clients should interact with the servers (e.g., "_ldaps._tcp" would be more
057 * appropriate if LDAP clients need to use SSL when communicating with the
058 * server).
059 * <BR><BR>
060 * DNS SRV records contain a number of components, including:
061 * <UL>
062 *   <LI>The address of the system providing the service.</LI>
063 *   <LI>The port to which connections should be established to access the
064 *       service.</LI>
065 *   <LI>The priority assigned to the service record.  If there are multiple
066 *       servers that provide the associated service, then the priority can be
067 *       used to specify the order in which they should be contacted.  Records
068 *       with a lower priority value wil be used before those with a higher
069 *       priority value.</LI>
070 *   <LI>The weight assigned to the service record.  The weight will be used if
071 *       there are multiple service records with the same priority, and it
072 *       controls how likely each record is to be chosen.  A record with a
073 *       weight of 2 is twice as likely to be chosen as a record with the same
074 *       priority and a weight of 1.</LI>
075 * </UL>
076 * In the event that multiple SRV records exist for the target service, then the
077 * priorities and weights of those records will be used to determine the order
078 * in which the servers will be tried.  Records with a lower priority value will
079 * always be tried before those with a higher priority value.  For records with
080 * equal priority values and nonzero weights, then the ratio of those weight
081 * values will be used to control how likely one of those records is to be tried
082 * before another.  Records with a weight of zero will always be tried after
083 * records with the same priority and nonzero weights.
084 * <BR><BR>
085 * This server set implementation uses JNDI to communicate with DNS servers in
086 * order to obtain the requested SRV records (although it does not use JNDI for
087 * any LDAP communication).  In order to specify which DNS server(s) to query, a
088 * JNDI provider URL must be used.  In many cases, a URL of "dns:", which
089 * indicates that the client should use the DNS servers configured for use by
090 * the underlying system, should be sufficient.  However, if you wish to use a
091 * specific DNS server then you may explicitly specify it in the URL (e.g.,
092 * "dns://1.2.3.4:53" would attempt to communicate with the DNS server listening
093 * on IP address 1.2.3.4 and port 53).  If you wish to specify multiple DNS
094 * servers, you may provide multiple URLs separated with spaces and they will be
095 * tried in the order in which they were included in the list until a response
096 * can be retrieved (e.g., for a provider URL of "dns://1.2.3.4 dns://1.2.3.5",
097 * it will first try to use the DNS server running on system with IP address
098 * "1.2.3.4", but if that is not successful then it will try the DNS server
099 * running on the system with IP address "1.2.3.5").  See the <A HREF="
100 * http://docs.oracle.com/javase/6/docs/technotes/guides/jndi/jndi-dns.html
101 * "> JNDI DNS service provider documentation</A> for more details on acceptable
102 * formats for the provider URL.
103 */
104@NotMutable()
105@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
106public final class DNSSRVRecordServerSet
107       extends ServerSet
108{
109  /**
110   * The default SRV record name that will be retrieved if none is specified.
111   */
112  private static final String DEFAULT_RECORD_NAME = "_ldap._tcp";
113
114
115
116  /**
117   * The default time-to-live value (1 hour, represented in milliseconds) that
118   * will be used if no alternate value is specified.
119   */
120  private static final long DEFAULT_TTL_MILLIS = 60L * 60L * 1000L;
121
122
123
124  /**
125   * The default provider URL that will be used for specifying which DNS
126   * server(s) to query.  The default behavior will be to attempt to determine
127   * which DNS server(s) to use from the underlying system configuration.
128   */
129  private static final String DEFAULT_DNS_PROVIDER_URL = "dns:";
130
131
132
133  // The properties that will be used to initialize the JNDI context.
134  private final Hashtable<String,String> jndiProperties;
135
136  // The connection options to use for newly-created connections.
137  private final LDAPConnectionOptions connectionOptions;
138
139  // The maximum length of time in milliseconds that previously-retrieved
140  // information should be considered valid.
141  private final long ttlMillis;
142
143  // The socket factory that should be used to create connections.
144  private final SocketFactory socketFactory;
145
146  // The cached set of SRV records.
147  private volatile SRVRecordSet recordSet;
148
149  // The name of the DNS SRV record to retrieve.
150  private final String recordName;
151
152  // The DNS provider URL to use.
153  private final String providerURL;
154
155
156
157  /**
158   * Creates a new instance of this server set that will use the specified DNS
159   * record name, a default DNS provider URL that will attempt to determine DNS
160   * servers from the underlying system configuration, a default TTL of one
161   * hour, round-robin ordering for servers with the same priority, and default
162   * socket factory and connection options.
163   *
164   * @param  recordName  The name of the DNS SRV record to retrieve.  If this is
165   *                     {@code null}, then a default record name of
166   *                     "_ldap._tcp" will be used.
167   */
168  public DNSSRVRecordServerSet(final String recordName)
169  {
170    this(recordName, null, DEFAULT_TTL_MILLIS, null, null);
171  }
172
173
174
175  /**
176   * Creates a new instance of this server set that will use the provided
177   * settings.
178   *
179   * @param  recordName         The name of the DNS SRV record to retrieve.  If
180   *                            this is {@code null}, then a default record name
181   *                            of "_ldap._tcp" will be used.
182   * @param  providerURL        The JNDI provider URL that may be used to
183   *                            specify the DNS server(s) to use.  If this is
184   *                            not specified, then a default URL of "dns:" will
185   *                            be used, which will attempt to determine the
186   *                            appropriate servers from the underlying system
187   *                            configuration.
188   * @param  ttlMillis          Specifies the maximum length of time in
189   *                            milliseconds that DNS information should be
190   *                            cached before it needs to be retrieved again.  A
191   *                            value less than or equal to zero will use the
192   *                            default TTL of one hour.
193   * @param  socketFactory      The socket factory that will be used when
194   *                            creating connections.  It may be {@code null} if
195   *                            the JVM-default socket factory should be used.
196   * @param  connectionOptions  The set of connection options that should be
197   *                            used for the connections that are created.  It
198   *                            may be {@code null} if the default connection
199   *                            options should be used.
200   */
201  public DNSSRVRecordServerSet(final String recordName,
202                               final String providerURL, final long ttlMillis,
203                               final SocketFactory socketFactory,
204                               final LDAPConnectionOptions connectionOptions)
205  {
206    this(recordName, providerURL, null, ttlMillis, socketFactory,
207         connectionOptions);
208  }
209
210
211
212  /**
213   * Creates a new instance of this server set that will use the provided
214   * settings.
215   *
216   * @param  recordName         The name of the DNS SRV record to retrieve.  If
217   *                            this is {@code null}, then a default record name
218   *                            of "_ldap._tcp" will be used.
219   * @param  providerURL        The JNDI provider URL that may be used to
220   *                            specify the DNS server(s) to use.  If this is
221   *                            not specified, then a default URL of "dns:" will
222   *                            be used, which will attempt to determine the
223   *                            appropriate servers from the underlying system
224   *                            configuration.
225   * @param  jndiProperties     A set of JNDI-related properties that should be
226   *                            be used when initializing the context for
227   *                            interacting with the DNS server via JNDI.  If
228   *                            this is {@code null}, then a default set of
229   *                            properties will be used.
230   * @param  ttlMillis          Specifies the maximum length of time in
231   *                            milliseconds that DNS information should be
232   *                            cached before it needs to be retrieved again.  A
233   *                            value less than or equal to zero will use the
234   *                            default TTL of one hour.
235   * @param  socketFactory      The socket factory that will be used when
236   *                            creating connections.  It may be {@code null} if
237   *                            the JVM-default socket factory should be used.
238   * @param  connectionOptions  The set of connection options that should be
239   *                            used for the connections that are created.  It
240   *                            may be {@code null} if the default connection
241   *                            options should be used.
242   */
243  public DNSSRVRecordServerSet(final String recordName,
244                               final String providerURL,
245                               final Properties jndiProperties,
246                               final long ttlMillis,
247                               final SocketFactory socketFactory,
248                               final LDAPConnectionOptions connectionOptions)
249  {
250    this.socketFactory     = socketFactory;
251    this.connectionOptions = connectionOptions;
252
253    recordSet = null;
254
255    if (recordName == null)
256    {
257      this.recordName = DEFAULT_RECORD_NAME;
258    }
259    else
260    {
261      this.recordName = recordName;
262    }
263
264    if (providerURL == null)
265    {
266      this.providerURL = DEFAULT_DNS_PROVIDER_URL;
267    }
268    else
269    {
270      this.providerURL = providerURL;
271    }
272
273    this.jndiProperties = new Hashtable<String,String>(10);
274    if (jndiProperties != null)
275    {
276      for (final Map.Entry<Object,Object> e : jndiProperties.entrySet())
277      {
278        this.jndiProperties.put(String.valueOf(e.getKey()),
279             String.valueOf(e.getValue()));
280      }
281    }
282
283    if (! this.jndiProperties.containsKey(Context.INITIAL_CONTEXT_FACTORY))
284    {
285      this.jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY,
286           "com.sun.jndi.dns.DnsContextFactory");
287    }
288
289    if (! this.jndiProperties.containsKey(Context.PROVIDER_URL))
290    {
291      this.jndiProperties.put(Context.PROVIDER_URL, this.providerURL);
292    }
293
294    if (ttlMillis <= 0L)
295    {
296      this.ttlMillis = DEFAULT_TTL_MILLIS;
297    }
298    else
299    {
300      this.ttlMillis = ttlMillis;
301    }
302  }
303
304
305
306  /**
307   * Retrieves the name of the DNS SRV record to retrieve.
308   *
309   * @return  The name of the DNS SRV record to retrieve.
310   */
311  public String getRecordName()
312  {
313    return recordName;
314  }
315
316
317
318  /**
319   * Retrieves the JNDI provider URL that specifies the DNS server(s) to use.
320   *
321   * @return  The JNDI provider URL that specifies the DNS server(s) to use.
322   */
323  public String getProviderURL()
324  {
325    return providerURL;
326  }
327
328
329
330  /**
331   * Retrieves an unmodifiable map of properties that will be used to initialize
332   * the JNDI context used to interact with DNS.  Note that the map returned
333   * will reflect the actual properties that will be used, and may not exactly
334   * match the properties provided when creating this server set.
335   *
336   * @return  An unmodifiable map of properties that will be used to initialize
337   *          the JNDI context used to interact with DNS.
338   */
339  public Map<String,String> getJNDIProperties()
340  {
341    return Collections.unmodifiableMap(jndiProperties);
342  }
343
344
345
346  /**
347   * Retrieves the maximum length of time in milliseconds that
348   * previously-retrieved DNS information should be cached before it needs to be
349   * refreshed.
350   *
351   * @return  The maximum length of time in milliseconds that
352   *          previously-retrieved DNS information should be cached before it
353   *          needs to be refreshed.
354   */
355  public long getTTLMillis()
356  {
357    return ttlMillis;
358  }
359
360
361
362  /**
363   * Retrieves the socket factory that will be used when creating connections,
364   * if any.
365   *
366   * @return  The socket factory that will be used when creating connections, or
367   *          {@code null} if the JVM-default socket factory will be used.
368   */
369  public SocketFactory getSocketFactory()
370  {
371    return socketFactory;
372  }
373
374
375
376  /**
377   * Retrieves the set of connection options to use for connections that are
378   * created, if any.
379   *
380   * @return  The set of connection options to use for connections that are
381   *          created, or {@code null} if a default set of options should be
382   *          used.
383   */
384  public LDAPConnectionOptions getConnectionOptions()
385  {
386    return connectionOptions;
387  }
388
389
390
391  /**
392   * {@inheritDoc}
393   */
394  @Override()
395  public LDAPConnection getConnection()
396         throws LDAPException
397  {
398    return getConnection(null);
399  }
400
401
402
403  /**
404   * {@inheritDoc}
405   */
406  @Override()
407  public LDAPConnection getConnection(
408                             final LDAPConnectionPoolHealthCheck healthCheck)
409         throws LDAPException
410  {
411    // If there is no cached record set, or if the cached set is expired, then
412    // try to get a new one.
413    if ((recordSet == null) || recordSet.isExpired())
414    {
415      try
416      {
417        recordSet = SRVRecordSet.getRecordSet(recordName, jndiProperties,
418             ttlMillis);
419      }
420      catch (final LDAPException le)
421      {
422        Debug.debugException(le);
423
424        // We couldn't get a new record set.  If we have an existing one, then
425        // it's expired but we'll keep using it anyway because it's better than
426        // nothing.  But if we don't have an existing set, then we can't
427        // continue.
428        if (recordSet == null)
429        {
430          throw le;
431        }
432      }
433    }
434
435
436    // Iterate through the record set in an order based on priority and weight.
437    // Take the first one that we can connect to and that satisfies the health
438    // check (if any).
439    LDAPException firstException = null;
440    for (final SRVRecord r : recordSet.getOrderedRecords())
441    {
442      final LDAPConnection conn;
443      try
444      {
445        conn = new LDAPConnection(socketFactory, connectionOptions,
446             r.getAddress(), r.getPort());
447      }
448      catch (final LDAPException le)
449      {
450        Debug.debugException(le);
451        if (firstException == null)
452        {
453          firstException = le;
454        }
455
456        continue;
457      }
458
459      if (healthCheck != null)
460      {
461        try
462        {
463          healthCheck.ensureNewConnectionValid(conn);
464        }
465        catch (final LDAPException le)
466        {
467          Debug.debugException(le);
468          if (firstException == null)
469          {
470            firstException = le;
471          }
472
473          continue;
474        }
475      }
476
477      return conn;
478    }
479
480    // If we've gotten here, then we couldn't connect to any of the servers.
481    // Throw the first exception that we encountered.
482    throw firstException;
483  }
484
485
486
487  /**
488   * {@inheritDoc}
489   */
490  @Override()
491  public void toString(final StringBuilder buffer)
492  {
493    buffer.append("DNSSRVRecordServerSet(recordName='");
494    buffer.append(recordName);
495    buffer.append("', providerURL='");
496    buffer.append(providerURL);
497    buffer.append("', ttlMillis=");
498    buffer.append(ttlMillis);
499
500    if (socketFactory != null)
501    {
502      buffer.append(", socketFactoryClass='");
503      buffer.append(socketFactory.getClass().getName());
504      buffer.append('\'');
505    }
506
507    if (connectionOptions != null)
508    {
509      buffer.append(", connectionOptions");
510      connectionOptions.toString(buffer);
511    }
512
513    buffer.append(')');
514  }
515}