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