All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.unboundid.ldap.sdk.RoundRobinDNSServerSet Maven / Gradle / Ivy

Go to download

The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use Java API for communicating with LDAP directory servers and performing related tasks like reading and writing LDIF, encoding and decoding data using base64 and ASN.1 BER, and performing secure communication. This package contains the Standard Edition of the LDAP SDK, which is a complete, general-purpose library for communicating with LDAPv3 directory servers.

There is a newer version: 7.0.1
Show newest version
/*
 * Copyright 2014-2018 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright (C) 2014-2018 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.ldap.sdk;



import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.InitialDirContext;
import javax.net.SocketFactory;

import com.unboundid.util.Debug;
import com.unboundid.util.NotMutable;
import com.unboundid.util.ObjectPair;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadLocalRandom;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.Validator;

import static com.unboundid.ldap.sdk.LDAPMessages.*;



/**
 * This class provides a server set implementation that handles the case in
 * which a given host name may resolve to multiple IP addresses.  Note that
 * while a setup like this is typically referred to as "round-robin DNS", this
 * server set implementation does not strictly require DNS (as names may be
 * resolved through alternate mechanisms like a hosts file or an alternate name
 * service), and it does not strictly require round-robin use of those addresses
 * (as alternate ordering mechanisms, like randomized or failover, may be used).
 * 

*

Example

* The following example demonstrates the process for creating a round-robin DNS * server set for the case in which the hostname "directory.example.com" may be * associated with multiple IP addresses, and the LDAP SDK should attempt to use * them in a round robin manner. *
 *   // Define a number of variables that will be used by the server set.
 *   String                hostname           = "directory.example.com";
 *   int                   port               = 389;
 *   AddressSelectionMode  selectionMode      =
 *        AddressSelectionMode.ROUND_ROBIN;
 *   long                  cacheTimeoutMillis = 3600000L; // 1 hour
 *   String                providerURL        = "dns:"; // Default DNS config.
 *   SocketFactory         socketFactory      = null; // Default socket factory.
 *   LDAPConnectionOptions connectionOptions  = null; // Default options.
 *
 *   // Create the server set using the settings defined above.
 *   RoundRobinDNSServerSet serverSet = new RoundRobinDNSServerSet(hostname,
 *        port, selectionMode, cacheTimeoutMillis, providerURL, socketFactory,
 *        connectionOptions);
 *
 *   // Verify that we can establish a single connection using the server set.
 *   LDAPConnection connection = serverSet.getConnection();
 *   RootDSE rootDSEFromConnection = connection.getRootDSE();
 *   connection.close();
 *
 *   // Verify that we can establish a connection pool using the server set.
 *   SimpleBindRequest bindRequest =
 *        new SimpleBindRequest("uid=pool.user,dc=example,dc=com", "password");
 *   LDAPConnectionPool pool =
 *        new LDAPConnectionPool(serverSet, bindRequest, 10);
 *   RootDSE rootDSEFromPool = pool.getRootDSE();
 *   pool.close();
 * 
*/ @NotMutable() @ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) public final class RoundRobinDNSServerSet extends ServerSet { /** * The name of a system property that can be used to specify a comma-delimited * list of IP addresses to use if resolution fails. This is intended * primarily for testing purposes. */ static final String PROPERTY_DEFAULT_ADDRESSES = RoundRobinDNSServerSet.class.getName() + ".defaultAddresses"; /** * An enum that defines the modes that may be used to select the order in * which addresses should be used in attempts to establish connections. */ public enum AddressSelectionMode { /** * The address selection mode that will cause addresses to be consistently * attempted in the order they are retrieved from the name service. */ FAILOVER, /** * The address selection mode that will cause the order of addresses to be * randomized for each attempt. */ RANDOM, /** * The address selection mode that will cause connection attempts to be made * in a round-robin order. */ ROUND_ROBIN; /** * Retrieves the address selection mode with the specified name. * * @param name The name of the address selection mode to retrieve. It * must not be {@code null}. * * @return The requested address selection mode, or {@code null} if no such * change mode is defined. */ public static AddressSelectionMode forName(final String name) { switch (StaticUtils.toLowerCase(name)) { case "failover": return FAILOVER; case "random": return RANDOM; case "roundrobin": case "round-robin": case "round_robin": return ROUND_ROBIN; default: return null; } } } // The address selection mode that should be used if the provided hostname // resolves to multiple addresses. private final AddressSelectionMode selectionMode; // A counter that will be used to handle round-robin ordering. private final AtomicLong roundRobinCounter; // A reference to an object that combines the resolved addresses with a // timestamp indicating when the value should no longer be trusted. private final AtomicReference> resolvedAddressesWithTimeout; // The bind request to use to authenticate connections created by this // server set. private final BindRequest bindRequest; // The properties that will be used to initialize the JNDI context, if any. private final Hashtable jndiProperties; // The port number for the target server. private final int port; // The set of connection options to use for new connections. private final LDAPConnectionOptions connectionOptions; // The maximum length of time, in milliseconds, to cache resolved addresses. private final long cacheTimeoutMillis; // The post-connect processor to invoke against connections created by this // server set. private final PostConnectProcessor postConnectProcessor; // The socket factory to use to establish connections. private final SocketFactory socketFactory; // The hostname to be resolved. private final String hostname; // The provider URL to use to resolve names, if any. private final String providerURL; // The DNS record types that will be used to obtain the IP addresses for the // specified hostname. private final String[] dnsRecordTypes; /** * Creates a new round-robin DNS server set with the provided information. * * @param hostname The hostname to be resolved to one or more * addresses. It must not be {@code null}. * @param port The port to use to connect to the server. Note * that even if the provided hostname resolves to * multiple addresses, the same port must be used * for all addresses. * @param selectionMode The selection mode that should be used if the * hostname resolves to multiple addresses. It * must not be {@code null}. * @param cacheTimeoutMillis The maximum length of time in milliseconds to * cache addresses resolved from the provided * hostname. Caching resolved addresses can * result in better performance and can reduce the * number of requests to the name service. A * that is less than or equal to zero indicates * that no caching should be used. * @param providerURL The JNDI provider URL that should be used when * communicating with the DNS server. If this is * {@code null}, then the underlying system's * name service mechanism will be used (which may * make use of other services instead of or in * addition to DNS). If this is non-{@code null}, * then only DNS will be used to perform the name * resolution. A value of "dns:" indicates that * the underlying system's DNS configuration * should be used. * @param socketFactory The socket factory to use to establish the * connections. It may be {@code null} if the * JVM-default socket factory should be used. * @param connectionOptions The set of connection options that should be * used for the connections. It may be * {@code null} if a default set of connection * options should be used. */ public RoundRobinDNSServerSet(final String hostname, final int port, final AddressSelectionMode selectionMode, final long cacheTimeoutMillis, final String providerURL, final SocketFactory socketFactory, final LDAPConnectionOptions connectionOptions) { this(hostname, port, selectionMode, cacheTimeoutMillis, providerURL, null, null, socketFactory, connectionOptions); } /** * Creates a new round-robin DNS server set with the provided information. * * @param hostname The hostname to be resolved to one or more * addresses. It must not be {@code null}. * @param port The port to use to connect to the server. Note * that even if the provided hostname resolves to * multiple addresses, the same port must be used * for all addresses. * @param selectionMode The selection mode that should be used if the * hostname resolves to multiple addresses. It * must not be {@code null}. * @param cacheTimeoutMillis The maximum length of time in milliseconds to * cache addresses resolved from the provided * hostname. Caching resolved addresses can * result in better performance and can reduce the * number of requests to the name service. A * that is less than or equal to zero indicates * that no caching should be used. * @param providerURL The JNDI provider URL that should be used when * communicating with the DNS server.If both * {@code providerURL} and {@code jndiProperties} * are {@code null}, then then JNDI will not be * used to interact with DNS and the hostname * resolution will be performed via the underlying * system's name service mechanism (which may make * use of other services instead of or in addition * to DNS).. If this is non-{@code null}, then * only DNS will be used to perform the name * resolution. A value of "dns:" indicates that * the underlying system's DNS configuration * should be used. * @param jndiProperties A set of JNDI-related properties that should be * be used when initializing the context for * interacting with the DNS server via JNDI. If * both {@code providerURL} and * {@code jndiProperties} are {@code null}, then * then JNDI will not be used to interact with * DNS and the hostname resolution will be * performed via the underlying system's name * service mechanism (which may make use of other * services instead of or in addition to DNS). If * {@code providerURL} is {@code null} and * {@code jndiProperties} is non-{@code null}, * then the provided properties must specify the * URL. * @param dnsRecordTypes Specifies the types of DNS records that will be * used to obtain the addresses for the specified * hostname. This will only be used if at least * one of {@code providerURL} and * {@code jndiProperties} is non-{@code null}. If * this is {@code null} or empty, then a default * record type of "A" (indicating IPv4 addresses) * will be used. * @param socketFactory The socket factory to use to establish the * connections. It may be {@code null} if the * JVM-default socket factory should be used. * @param connectionOptions The set of connection options that should be * used for the connections. It may be * {@code null} if a default set of connection * options should be used. */ public RoundRobinDNSServerSet(final String hostname, final int port, final AddressSelectionMode selectionMode, final long cacheTimeoutMillis, final String providerURL, final Properties jndiProperties, final String[] dnsRecordTypes, final SocketFactory socketFactory, final LDAPConnectionOptions connectionOptions) { this(hostname, port, selectionMode, cacheTimeoutMillis, providerURL, jndiProperties, dnsRecordTypes, socketFactory, connectionOptions, null, null); } /** * Creates a new round-robin DNS server set with the provided information. * * @param hostname The hostname to be resolved to one or more * addresses. It must not be {@code null}. * @param port The port to use to connect to the server. * Note that even if the provided hostname * resolves to multiple addresses, the same * port must be used for all addresses. * @param selectionMode The selection mode that should be used if the * hostname resolves to multiple addresses. It * must not be {@code null}. * @param cacheTimeoutMillis The maximum length of time in milliseconds to * cache addresses resolved from the provided * hostname. Caching resolved addresses can * result in better performance and can reduce * the number of requests to the name service. * A that is less than or equal to zero * indicates that no caching should be used. * @param providerURL The JNDI provider URL that should be used * when communicating with the DNS server. If * both {@code providerURL} and * {@code jndiProperties} are {@code null}, * then then JNDI will not be used to interact * with DNS and the hostname resolution will be * performed via the underlying system's name * service mechanism (which may make use of * other services instead of or in addition to * DNS). If this is non-{@code null}, then only * DNS will be used to perform the name * resolution. A value of "dns:" indicates that * the underlying system's DNS configuration * should be used. * @param jndiProperties A set of JNDI-related properties that should * be used when initializing the context for * interacting with the DNS server via JNDI. If * both {@code providerURL} and * {@code jndiProperties} are {@code null}, then * JNDI will not be used to interact with DNS * and the hostname resolution will be * performed via the underlying system's name * service mechanism (which may make use of * other services instead of or in addition to * DNS). If {@code providerURL} is * {@code null} and {@code jndiProperties} is * non-{@code null}, then the provided * properties must specify the URL. * @param dnsRecordTypes Specifies the types of DNS records that will * be used to obtain the addresses for the * specified hostname. This will only be used * if at least one of {@code providerURL} and * {@code jndiProperties} is non-{@code null}. * If this is {@code null} or empty, then a * default record type of "A" (indicating IPv4 * addresses) will be used. * @param socketFactory The socket factory to use to establish the * connections. It may be {@code null} if the * JVM-default socket factory should be used. * @param connectionOptions The set of connection options that should be * used for the connections. It may be * {@code null} if a default set of connection * options should be used. * @param bindRequest The bind request that should be used to * authenticate newly-established connections. * It may be {@code null} if this server set * should not perform any authentication. * @param postConnectProcessor The post-connect processor that should be * invoked on newly-established connections. It * may be {@code null} if this server set should * not perform any post-connect processing. */ public RoundRobinDNSServerSet(final String hostname, final int port, final AddressSelectionMode selectionMode, final long cacheTimeoutMillis, final String providerURL, final Properties jndiProperties, final String[] dnsRecordTypes, final SocketFactory socketFactory, final LDAPConnectionOptions connectionOptions, final BindRequest bindRequest, final PostConnectProcessor postConnectProcessor) { Validator.ensureNotNull(hostname); Validator.ensureTrue((port >= 1) && (port <= 65_535)); Validator.ensureNotNull(selectionMode); this.hostname = hostname; this.port = port; this.selectionMode = selectionMode; this.providerURL = providerURL; this.bindRequest = bindRequest; this.postConnectProcessor = postConnectProcessor; if (jndiProperties == null) { if (providerURL == null) { this.jndiProperties = null; } else { this.jndiProperties = new Hashtable<>(2); this.jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); this.jndiProperties.put(Context.PROVIDER_URL, providerURL); } } else { this.jndiProperties = new Hashtable<>(jndiProperties.size()+2); for (final Map.Entry e : jndiProperties.entrySet()) { this.jndiProperties.put(String.valueOf(e.getKey()), String.valueOf(e.getValue())); } if (! this.jndiProperties.containsKey(Context.INITIAL_CONTEXT_FACTORY)) { this.jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); } if ((! this.jndiProperties.containsKey(Context.PROVIDER_URL)) && (providerURL != null)) { this.jndiProperties.put(Context.PROVIDER_URL, providerURL); } } if (dnsRecordTypes == null) { this.dnsRecordTypes = new String[] { "A" }; } else { this.dnsRecordTypes = dnsRecordTypes; } if (cacheTimeoutMillis > 0L) { this.cacheTimeoutMillis = cacheTimeoutMillis; } else { this.cacheTimeoutMillis = 0L; } if (socketFactory == null) { this.socketFactory = SocketFactory.getDefault(); } else { this.socketFactory = socketFactory; } if (connectionOptions == null) { this.connectionOptions = new LDAPConnectionOptions(); } else { this.connectionOptions = connectionOptions; } roundRobinCounter = new AtomicLong(0L); resolvedAddressesWithTimeout = new AtomicReference<>(); } /** * Retrieves the hostname to be resolved. * * @return The hostname to be resolved. */ public String getHostname() { return hostname; } /** * Retrieves the port to use to connect to the server. * * @return The port to use to connect to the server. */ public int getPort() { return port; } /** * Retrieves the address selection mode that should be used if the provided * hostname resolves to multiple addresses. * * @return The address selection */ public AddressSelectionMode getAddressSelectionMode() { return selectionMode; } /** * Retrieves the length of time in milliseconds that resolved addresses may be * cached. * * @return The length of time in milliseconds that resolved addresses may be * cached, or zero if no caching should be performed. */ public long getCacheTimeoutMillis() { return cacheTimeoutMillis; } /** * Retrieves the provider URL that should be used when interacting with DNS to * resolve the hostname to its corresponding addresses. * * @return The provider URL that should be used when interacting with DNS to * resolve the hostname to its corresponding addresses, or * {@code null} if the system's configured naming service should be * used. */ public String getProviderURL() { return providerURL; } /** * Retrieves an unmodifiable map of properties that will be used to initialize * the JNDI context used to interact with DNS. Note that the map returned * will reflect the actual properties that will be used, and may not exactly * match the properties provided when creating this server set. * * @return An unmodifiable map of properties that will be used to initialize * the JNDI context used to interact with DNS, or {@code null} if * JNDI will nto be used to interact with DNS. */ public Map getJNDIProperties() { if (jndiProperties == null) { return null; } else { return Collections.unmodifiableMap(jndiProperties); } } /** * Retrieves an array of record types that will be requested if JNDI will be * used to interact with DNS. * * @return An array of record types that will be requested if JNDI will be * used to interact with DNS. */ public String[] getDNSRecordTypes() { return dnsRecordTypes; } /** * Retrieves the socket factory that will be used to establish connections. * This will not be {@code null}, even if no socket factory was provided when * the server set was created. * * @return The socket factory that will be used to establish connections. */ public SocketFactory getSocketFactory() { return socketFactory; } /** * Retrieves the set of connection options that will be used for underlying * connections. This will not be {@code null}, even if no connection options * object was provided when the server set was created. * * @return The set of connection options that will be used for underlying * connections. */ public LDAPConnectionOptions getConnectionOptions() { return connectionOptions; } /** * {@inheritDoc} */ @Override() public boolean includesAuthentication() { return (bindRequest != null); } /** * {@inheritDoc} */ @Override() public boolean includesPostConnectProcessing() { return (postConnectProcessor != null); } /** * {@inheritDoc} */ @Override() public LDAPConnection getConnection() throws LDAPException { return getConnection(null); } /** * {@inheritDoc} */ @Override() public synchronized LDAPConnection getConnection( final LDAPConnectionPoolHealthCheck healthCheck) throws LDAPException { LDAPException firstException = null; final LDAPConnection conn = new LDAPConnection(socketFactory, connectionOptions); for (final InetAddress a : orderAddresses(resolveHostname())) { boolean close = true; try { conn.connect(hostname, a, port, connectionOptions.getConnectTimeoutMillis()); doBindPostConnectAndHealthCheckProcessing(conn, bindRequest, postConnectProcessor, healthCheck); close = false; return conn; } catch (final LDAPException le) { Debug.debugException(le); if (firstException == null) { firstException = le; } } finally { if (close) { conn.close(); } } } throw firstException; } /** * Resolve the hostname to its corresponding addresses. * * @return The addresses resolved from the hostname. * * @throws LDAPException If */ InetAddress[] resolveHostname() throws LDAPException { // First, see if we can use the cached addresses. final ObjectPair pair = resolvedAddressesWithTimeout.get(); if (pair != null) { if (pair.getSecond() >= System.currentTimeMillis()) { return pair.getFirst(); } } // Try to resolve the address. InetAddress[] addresses = null; try { if (jndiProperties == null) { addresses = InetAddress.getAllByName(hostname); } else { final Attributes attributes; final InitialDirContext context = new InitialDirContext(jndiProperties); try { attributes = context.getAttributes(hostname, dnsRecordTypes); } finally { context.close(); } if (attributes != null) { final ArrayList addressList = new ArrayList<>(10); for (final String recordType : dnsRecordTypes) { final Attribute a = attributes.get(recordType); if (a != null) { final NamingEnumeration values = a.getAll(); while (values.hasMore()) { final Object value = values.next(); addressList.add(getInetAddressForIP(String.valueOf(value))); } } } if (! addressList.isEmpty()) { addresses = new InetAddress[addressList.size()]; addressList.toArray(addresses); } } } } catch (final Exception e) { Debug.debugException(e); addresses = getDefaultAddresses(); } // If we were able to resolve the hostname, then cache and return the // resolved addresses. if ((addresses != null) && (addresses.length > 0)) { final long timeoutTime; if (cacheTimeoutMillis > 0L) { timeoutTime = System.currentTimeMillis() + cacheTimeoutMillis; } else { timeoutTime = System.currentTimeMillis() - 1L; } resolvedAddressesWithTimeout.set( new ObjectPair<>(addresses, timeoutTime)); return addresses; } // If we've gotten here, then we couldn't resolve the hostname. If we have // cached addresses, then use them even though the timeout has expired // because that's better than nothing. if (pair != null) { return pair.getFirst(); } throw new LDAPException(ResultCode.CONNECT_ERROR, ERR_ROUND_ROBIN_DNS_SERVER_SET_CANNOT_RESOLVE.get(hostname)); } /** * Orders the provided array of InetAddress objects to reflect the order in * which the addresses should be used to try to create a new connection. * * @param addresses The array of addresses to be ordered. * * @return A list containing the ordered addresses. */ List orderAddresses(final InetAddress[] addresses) { final ArrayList l = new ArrayList<>(addresses.length); switch (selectionMode) { case RANDOM: l.addAll(Arrays.asList(addresses)); Collections.shuffle(l, ThreadLocalRandom.get()); break; case ROUND_ROBIN: final int index = (int) (roundRobinCounter.getAndIncrement() % addresses.length); for (int i=index; i < addresses.length; i++) { l.add(addresses[i]); } for (int i=0; i < index; i++) { l.add(addresses[i]); } break; case FAILOVER: default: // We'll use the addresses in the same order we originally got them. l.addAll(Arrays.asList(addresses)); break; } return l; } /** * Retrieves a default set of addresses that may be used for testing. * * @return A default set of addresses that may be used for testing. */ InetAddress[] getDefaultAddresses() { final String defaultAddrsStr = System.getProperty(PROPERTY_DEFAULT_ADDRESSES); if (defaultAddrsStr == null) { return null; } final StringTokenizer tokenizer = new StringTokenizer(defaultAddrsStr, " ,"); final InetAddress[] addresses = new InetAddress[tokenizer.countTokens()]; for (int i=0; i < addresses.length; i++) { try { addresses[i] = getInetAddressForIP(tokenizer.nextToken()); } catch (final Exception e) { Debug.debugException(e); return null; } } return addresses; } /** * Retrieves an InetAddress object with the configured hostname and the * provided IP address. * * @param ipAddress The string representation of the IP address to use in * the returned InetAddress. * * @return The created InetAddress. * * @throws UnknownHostException If the provided string does not represent a * valid IPv4 or IPv6 address. */ private InetAddress getInetAddressForIP(final String ipAddress) throws UnknownHostException { // We want to create an InetAddress that has the provided hostname and the // specified IP address. To do that, we need to use // InetAddress.getByAddress. But that requires the IP address to be // specified as a byte array, and the easiest way to convert an IP address // string to a byte array is to use InetAddress.getByName. final InetAddress byName = InetAddress.getByName(String.valueOf(ipAddress)); return InetAddress.getByAddress(hostname, byName.getAddress()); } /** * {@inheritDoc} */ @Override() public void toString(final StringBuilder buffer) { buffer.append("RoundRobinDNSServerSet(hostname='"); buffer.append(hostname); buffer.append("', port="); buffer.append(port); buffer.append(", addressSelectionMode="); buffer.append(selectionMode.name()); buffer.append(", cacheTimeoutMillis="); buffer.append(cacheTimeoutMillis); if (providerURL != null) { buffer.append(", providerURL='"); buffer.append(providerURL); buffer.append('\''); } buffer.append(", includesAuthentication="); buffer.append(bindRequest != null); buffer.append(", includesPostConnectProcessing="); buffer.append(postConnectProcessor != null); buffer.append(')'); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy