/*
|
* CDDL HEADER START
|
*
|
* The contents of this file are subject to the terms of the
|
* Common Development and Distribution License, Version 1.0 only
|
* (the "License"). You may not use this file except in compliance
|
* with the License.
|
*
|
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
|
* or http://forgerock.org/license/CDDLv1.0.html.
|
* See the License for the specific language governing permissions
|
* and limitations under the License.
|
*
|
* When distributing Covered Code, include this CDDL HEADER in each
|
* file and include the License file at legal-notices/CDDLv1_0.txt.
|
* If applicable, add the following below this CDDL HEADER, with the
|
* fields enclosed by brackets "[]" replaced with your own identifying
|
* information:
|
* Portions Copyright [yyyy] [name of copyright owner]
|
*
|
* CDDL HEADER END
|
*
|
*
|
* Copyright 2006-2010 Sun Microsystems, Inc.
|
* Portions Copyright 2011-2014 ForgeRock AS
|
*/
|
package org.opends.server.protocols.ldap;
|
|
import static org.opends.messages.ProtocolMessages.*;
|
import static org.opends.server.loggers.ErrorLogger.*;
|
import static org.opends.server.util.ServerConstants.*;
|
import static org.opends.server.util.StaticUtils.*;
|
|
import java.io.IOException;
|
import java.net.InetAddress;
|
import java.net.InetSocketAddress;
|
import java.net.SocketException;
|
import java.nio.channels.*;
|
import java.util.*;
|
import java.util.concurrent.Executors;
|
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.TimeUnit;
|
|
import javax.net.ssl.KeyManager;
|
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLEngine;
|
|
import org.forgerock.i18n.LocalizableMessage;
|
import org.forgerock.opendj.ldap.AddressMask;
|
import org.opends.server.admin.server.ConfigurationChangeListener;
|
import org.opends.server.admin.std.server.ConnectionHandlerCfg;
|
import org.opends.server.admin.std.server.LDAPConnectionHandlerCfg;
|
import org.opends.server.api.*;
|
import org.opends.server.api.plugin.PluginResult;
|
import org.opends.server.config.ConfigException;
|
import org.opends.server.core.DirectoryServer;
|
import org.opends.server.core.PluginConfigManager;
|
import org.opends.server.core.QueueingStrategy;
|
import org.opends.server.core.WorkQueueStrategy;
|
import org.opends.server.extensions.NullKeyManagerProvider;
|
import org.opends.server.extensions.NullTrustManagerProvider;
|
import org.opends.server.extensions.TLSByteChannel;
|
import org.forgerock.i18n.slf4j.LocalizedLogger;
|
import org.opends.server.monitors.ClientConnectionMonitorProvider;
|
import org.opends.server.types.*;
|
import org.opends.server.util.SelectableCertificateKeyManager;
|
import org.opends.server.util.StaticUtils;
|
|
/**
|
* This class defines a connection handler that will be used for communicating
|
* with clients over LDAP. It is actually implemented in two parts: as a
|
* connection handler and one or more request handlers. The connection handler
|
* is responsible for accepting new connections and registering each of them
|
* with a request handler. The request handlers then are responsible for reading
|
* requests from the clients and parsing them as operations. A single request
|
* handler may be used, but having multiple handlers might provide better
|
* performance in a multi-CPU system.
|
*/
|
public final class LDAPConnectionHandler extends
|
ConnectionHandler<LDAPConnectionHandlerCfg> implements
|
ConfigurationChangeListener<LDAPConnectionHandlerCfg>,
|
ServerShutdownListener, AlertGenerator
|
{
|
|
/**
|
* Task run periodically by the connection finalizer.
|
*/
|
private final class ConnectionFinalizerRunnable implements Runnable
|
{
|
@Override
|
public void run()
|
{
|
if (!connectionFinalizerActiveJobQueue.isEmpty())
|
{
|
for (Runnable r : connectionFinalizerActiveJobQueue)
|
{
|
r.run();
|
}
|
connectionFinalizerActiveJobQueue.clear();
|
}
|
|
// Switch the lists.
|
synchronized (connectionFinalizerLock)
|
{
|
List<Runnable> tmp = connectionFinalizerActiveJobQueue;
|
connectionFinalizerActiveJobQueue = connectionFinalizerPendingJobQueue;
|
connectionFinalizerPendingJobQueue = tmp;
|
}
|
|
}
|
}
|
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
|
|
/**
|
* Default friendly name for the LDAP connection handler.
|
*/
|
private static final String DEFAULT_FRIENDLY_NAME = "LDAP Connection Handler";
|
|
/** SSL instance name used in context creation. */
|
private static final String SSL_CONTEXT_INSTANCE_NAME = "TLS";
|
|
/** The current configuration state. */
|
private LDAPConnectionHandlerCfg currentConfig;
|
|
/* Properties that cannot be modified dynamically */
|
|
/** The set of addresses on which to listen for new connections. */
|
private Set<InetAddress> listenAddresses;
|
|
/** The port on which this connection handler should listen for requests. */
|
private int listenPort;
|
|
/** The SSL client auth policy used by this connection handler. */
|
private SSLClientAuthPolicy sslClientAuthPolicy;
|
|
/** The backlog that will be used for the accept queue. */
|
private int backlog;
|
|
/** Indicates whether to allow the reuse address socket option. */
|
private boolean allowReuseAddress;
|
|
/**
|
* The number of request handlers that should be used for this connection
|
* handler.
|
*/
|
private int numRequestHandlers;
|
|
/**
|
* Indicates whether the Directory Server is in the process of shutting down.
|
*/
|
private volatile boolean shutdownRequested;
|
|
/* Internal LDAP connection handler state */
|
|
/** Indicates whether this connection handler is enabled. */
|
private boolean enabled;
|
|
/** The set of clients that are explicitly allowed access to the server. */
|
private Collection<AddressMask> allowedClients;
|
|
/**
|
* The set of clients that have been explicitly denied access to the server.
|
*/
|
private Collection<AddressMask> deniedClients;
|
|
/**
|
* The index to the request handler that will be used for the next connection
|
* accepted by the server.
|
*/
|
private int requestHandlerIndex;
|
|
/** The set of listeners for this connection handler. */
|
private List<HostPort> listeners;
|
|
/**
|
* The set of request handlers that are associated with this connection
|
* handler.
|
*/
|
private LDAPRequestHandler[] requestHandlers;
|
|
/** The set of statistics collected for this connection handler. */
|
private LDAPStatistics statTracker;
|
|
/**
|
* The client connection monitor provider associated with this connection
|
* handler.
|
*/
|
private ClientConnectionMonitorProvider connMonitor;
|
|
/**
|
* The selector that will be used to multiplex connection acceptance across
|
* multiple sockets by a single thread.
|
*/
|
private Selector selector;
|
|
/** The unique name assigned to this connection handler. */
|
private String handlerName;
|
|
/** The protocol used by this connection handler. */
|
private String protocol;
|
|
/** Queueing strategy. */
|
private final QueueingStrategy queueingStrategy;
|
|
/**
|
* The condition variable that will be used by the start method to wait for
|
* the socket port to be opened and ready to process requests before
|
* returning.
|
*/
|
private final Object waitListen = new Object();
|
|
/** The friendly name of this connection handler. */
|
private String friendlyName;
|
|
/**
|
* SSL context.
|
*
|
* @see LDAPConnectionHandler#sslEngine
|
*/
|
private SSLContext sslContext;
|
|
/** The SSL engine is used for obtaining default SSL parameters. */
|
private SSLEngine sslEngine;
|
|
/**
|
* Connection finalizer thread.
|
* <p>
|
* This thread is defers closing clients for approximately 100ms. This gives
|
* the client a chance to close the connection themselves before the server
|
* thus avoiding leaving the server side in the TIME WAIT state.
|
*/
|
private final Object connectionFinalizerLock = new Object();
|
private ScheduledExecutorService connectionFinalizer;
|
private List<Runnable> connectionFinalizerActiveJobQueue;
|
private List<Runnable> connectionFinalizerPendingJobQueue;
|
|
|
|
/**
|
* Creates a new instance of this LDAP connection handler. It must be
|
* initialized before it may be used.
|
*/
|
public LDAPConnectionHandler()
|
{
|
this(new WorkQueueStrategy(), null); // Use name from configuration.
|
}
|
|
|
|
/**
|
* Creates a new instance of this LDAP connection handler, using a queueing
|
* strategy. It must be initialized before it may be used.
|
*
|
* @param strategy
|
* Request handling strategy.
|
* @param friendlyName
|
* The name of of this connection handler, or {@code null} if the
|
* name should be taken from the configuration.
|
*/
|
public LDAPConnectionHandler(QueueingStrategy strategy, String friendlyName)
|
{
|
super(friendlyName != null ? friendlyName : DEFAULT_FRIENDLY_NAME
|
+ " Thread");
|
|
this.friendlyName = friendlyName;
|
this.queueingStrategy = strategy;
|
|
// No real implementation is required. Do all the work in the
|
// initializeConnectionHandler method.
|
}
|
|
|
|
/**
|
* Indicates whether this connection handler should allow interaction with
|
* LDAPv2 clients.
|
*
|
* @return <CODE>true</CODE> if LDAPv2 is allowed, or <CODE>false</CODE> if
|
* not.
|
*/
|
public boolean allowLDAPv2()
|
{
|
return currentConfig.isAllowLDAPV2();
|
}
|
|
|
|
/**
|
* Indicates whether this connection handler should allow the use of the
|
* StartTLS extended operation.
|
*
|
* @return <CODE>true</CODE> if StartTLS is allowed, or <CODE>false</CODE> if
|
* not.
|
*/
|
public boolean allowStartTLS()
|
{
|
return currentConfig.isAllowStartTLS() && !currentConfig.isUseSSL();
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public ConfigChangeResult applyConfigurationChange(
|
LDAPConnectionHandlerCfg config)
|
{
|
// Create variables to include in the response.
|
ResultCode resultCode = ResultCode.SUCCESS;
|
boolean adminActionRequired = false;
|
ArrayList<LocalizableMessage> messages = new ArrayList<LocalizableMessage>();
|
|
// Note that the following properties cannot be modified:
|
//
|
// * listen port and addresses
|
// * use ssl
|
// * ssl policy
|
// * ssl cert nickname
|
// * accept backlog
|
// * tcp reuse address
|
// * num request handler
|
|
// Clear the stat tracker if LDAPv2 is being enabled.
|
if (currentConfig.isAllowLDAPV2() != config.isAllowLDAPV2())
|
{
|
if (config.isAllowLDAPV2())
|
{
|
statTracker.clearStatistics();
|
}
|
}
|
|
// Apply the changes.
|
currentConfig = config;
|
enabled = config.isEnabled();
|
allowedClients = config.getAllowedClient();
|
deniedClients = config.getDeniedClient();
|
|
// Reconfigure SSL if needed.
|
try
|
{
|
configureSSL(config);
|
}
|
catch (DirectoryException e)
|
{
|
logger.traceException(e);
|
messages.add(e.getMessageObject());
|
return new ConfigChangeResult(e.getResultCode(), adminActionRequired,
|
messages);
|
}
|
|
if (config.isAllowLDAPV2())
|
{
|
DirectoryServer.registerSupportedLDAPVersion(2, this);
|
}
|
else
|
{
|
DirectoryServer.deregisterSupportedLDAPVersion(2, this);
|
}
|
|
return new ConfigChangeResult(resultCode, adminActionRequired, messages);
|
}
|
|
private void configureSSL(LDAPConnectionHandlerCfg config)
|
throws DirectoryException
|
{
|
protocol = config.isUseSSL() ? "LDAPS" : "LDAP";
|
if (config.isUseSSL() || config.isAllowStartTLS())
|
{
|
sslContext = createSSLContext(config);
|
sslEngine = createSSLEngine(config, sslContext);
|
}
|
else
|
{
|
sslContext = null;
|
sslEngine = null;
|
}
|
}
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void finalizeConnectionHandler(LocalizableMessage finalizeReason)
|
{
|
shutdownRequested = true;
|
currentConfig.removeLDAPChangeListener(this);
|
|
if (connMonitor != null)
|
{
|
DirectoryServer.deregisterMonitorProvider(connMonitor);
|
}
|
|
if (statTracker != null)
|
{
|
DirectoryServer.deregisterMonitorProvider(statTracker);
|
}
|
|
DirectoryServer.deregisterSupportedLDAPVersion(2, this);
|
DirectoryServer.deregisterSupportedLDAPVersion(3, this);
|
|
try
|
{
|
selector.wakeup();
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
}
|
|
for (LDAPRequestHandler requestHandler : requestHandlers)
|
{
|
requestHandler.processServerShutdown(finalizeReason);
|
}
|
|
// Shutdown the connection finalizer and ensure that any pending
|
// unclosed connections are closed.
|
synchronized (connectionFinalizerLock)
|
{
|
connectionFinalizer.shutdown();
|
connectionFinalizer = null;
|
|
Runnable r = new ConnectionFinalizerRunnable();
|
r.run(); // Flush active queue.
|
r.run(); // Flush pending queue.
|
}
|
}
|
|
|
|
/**
|
* Retrieves information about the set of alerts that this generator may
|
* produce. The map returned should be between the notification type for a
|
* particular notification and the human-readable description for that
|
* notification. This alert generator must not generate any alerts with types
|
* that are not contained in this list.
|
*
|
* @return Information about the set of alerts that this generator may
|
* produce.
|
*/
|
@Override
|
public Map<String, String> getAlerts()
|
{
|
Map<String, String> alerts = new LinkedHashMap<String, String>();
|
|
alerts.put(ALERT_TYPE_LDAP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES,
|
ALERT_DESCRIPTION_LDAP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES);
|
alerts.put(ALERT_TYPE_LDAP_CONNECTION_HANDLER_UNCAUGHT_ERROR,
|
ALERT_DESCRIPTION_LDAP_CONNECTION_HANDLER_UNCAUGHT_ERROR);
|
|
return alerts;
|
}
|
|
|
|
/**
|
* Retrieves the fully-qualified name of the Java class for this alert
|
* generator implementation.
|
*
|
* @return The fully-qualified name of the Java class for this alert generator
|
* implementation.
|
*/
|
@Override
|
public String getClassName()
|
{
|
return LDAPConnectionHandler.class.getName();
|
}
|
|
|
|
/**
|
* Retrieves the set of active client connections that have been established
|
* through this connection handler.
|
*
|
* @return The set of active client connections that have been established
|
* through this connection handler.
|
*/
|
@Override
|
public Collection<ClientConnection> getClientConnections()
|
{
|
List<ClientConnection> connectionList = new LinkedList<ClientConnection>();
|
for (LDAPRequestHandler requestHandler : requestHandlers)
|
{
|
connectionList.addAll(requestHandler.getClientConnections());
|
}
|
return connectionList;
|
}
|
|
|
|
/**
|
* Retrieves the DN of the configuration entry with which this alert generator
|
* is associated.
|
*
|
* @return The DN of the configuration entry with which this alert generator
|
* is associated.
|
*/
|
@Override
|
public DN getComponentEntryDN()
|
{
|
return currentConfig.dn();
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public String getConnectionHandlerName()
|
{
|
return handlerName;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public Collection<String> getEnabledSSLCipherSuites()
|
{
|
final SSLEngine engine = sslEngine;
|
if (engine != null)
|
{
|
return Arrays.asList(engine.getEnabledCipherSuites());
|
}
|
return super.getEnabledSSLCipherSuites();
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public Collection<String> getEnabledSSLProtocols()
|
{
|
final SSLEngine engine = sslEngine;
|
if (engine != null)
|
{
|
return Arrays.asList(engine.getEnabledProtocols());
|
}
|
return super.getEnabledSSLProtocols();
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public Collection<HostPort> getListeners()
|
{
|
return listeners;
|
}
|
|
|
|
/**
|
* Retrieves the port on which this connection handler is listening for client
|
* connections.
|
*
|
* @return The port on which this connection handler is listening for client
|
* connections.
|
*/
|
public int getListenPort()
|
{
|
return listenPort;
|
}
|
|
|
|
/**
|
* Retrieves the maximum length of time in milliseconds that attempts to write
|
* to LDAP client connections should be allowed to block.
|
*
|
* @return The maximum length of time in milliseconds that attempts to write
|
* to LDAP client connections should be allowed to block, or zero if
|
* there should not be any limit imposed.
|
*/
|
public long getMaxBlockedWriteTimeLimit()
|
{
|
return currentConfig.getMaxBlockedWriteTimeLimit();
|
}
|
|
|
|
/**
|
* Retrieves the maximum ASN.1 element value length that will be allowed by
|
* this connection handler.
|
*
|
* @return The maximum ASN.1 element value length that will be allowed by this
|
* connection handler.
|
*/
|
public int getMaxRequestSize()
|
{
|
return (int) currentConfig.getMaxRequestSize();
|
}
|
|
|
|
/**
|
* Retrieves the size in bytes of the LDAP response message write buffer
|
* defined for this connection handler.
|
*
|
* @return The size in bytes of the LDAP response message write buffer.
|
*/
|
public int getBufferSize()
|
{
|
return (int) currentConfig.getBufferSize();
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public String getProtocol()
|
{
|
return protocol;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public String getShutdownListenerName()
|
{
|
return handlerName;
|
}
|
|
|
|
/**
|
* Retrieves the SSL client authentication policy for this connection handler.
|
*
|
* @return The SSL client authentication policy for this connection handler.
|
*/
|
public SSLClientAuthPolicy getSSLClientAuthPolicy()
|
{
|
return sslClientAuthPolicy;
|
}
|
|
|
|
/**
|
* Retrieves the set of statistics maintained by this connection handler.
|
*
|
* @return The set of statistics maintained by this connection handler.
|
*/
|
public LDAPStatistics getStatTracker()
|
{
|
return statTracker;
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void initializeConnectionHandler(LDAPConnectionHandlerCfg config)
|
throws ConfigException, InitializationException
|
{
|
if (friendlyName == null)
|
{
|
friendlyName = config.dn().rdn().getAttributeValue(0).toString();
|
}
|
|
// Open the selector.
|
try
|
{
|
selector = Selector.open();
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
LocalizableMessage message = ERR_LDAP_CONNHANDLER_OPEN_SELECTOR_FAILED.get(
|
String.valueOf(config.dn()), stackTraceToSingleLineString(e));
|
throw new InitializationException(message, e);
|
}
|
|
// Save this configuration for future reference.
|
currentConfig = config;
|
enabled = config.isEnabled();
|
requestHandlerIndex = 0;
|
allowedClients = config.getAllowedClient();
|
deniedClients = config.getDeniedClient();
|
|
// Configure SSL if needed.
|
try
|
{
|
configureSSL(config);
|
}
|
catch (DirectoryException e)
|
{
|
logger.traceException(e);
|
throw new InitializationException(e.getMessageObject());
|
}
|
|
// Save properties that cannot be dynamically modified.
|
allowReuseAddress = config.isAllowTCPReuseAddress();
|
backlog = config.getAcceptBacklog();
|
listenAddresses = config.getListenAddress();
|
listenPort = config.getListenPort();
|
numRequestHandlers =
|
getNumRequestHandlers(config.getNumRequestHandlers(), friendlyName);
|
|
// Construct a unique name for this connection handler, and put
|
// together the set of listeners.
|
listeners = new LinkedList<HostPort>();
|
StringBuilder nameBuffer = new StringBuilder();
|
nameBuffer.append(friendlyName);
|
for (InetAddress a : listenAddresses)
|
{
|
listeners.add(new HostPort(a.getHostAddress(), listenPort));
|
nameBuffer.append(" ");
|
nameBuffer.append(a.getHostAddress());
|
}
|
nameBuffer.append(" port ");
|
nameBuffer.append(listenPort);
|
handlerName = nameBuffer.toString();
|
|
// Attempt to bind to the listen port on all configured addresses to
|
// verify whether the connection handler will be able to start.
|
LocalizableMessage errorMessage =
|
checkAnyListenAddressInUse(listenAddresses, listenPort,
|
allowReuseAddress, config.dn());
|
if (errorMessage != null)
|
{
|
logError(errorMessage);
|
throw new InitializationException(errorMessage);
|
}
|
|
// Create a system property to store the LDAP(S) port the server is
|
// listening to. This information can be displayed with jinfo.
|
System.setProperty(protocol + "_port", String.valueOf(listenPort));
|
|
// Create and start a connection finalizer thread for this
|
// connection handler.
|
connectionFinalizer = Executors
|
.newSingleThreadScheduledExecutor(new DirectoryThread.Factory(
|
"LDAP Connection Finalizer for connection handler " + toString()));
|
|
connectionFinalizerActiveJobQueue = new ArrayList<Runnable>();
|
connectionFinalizerPendingJobQueue = new ArrayList<Runnable>();
|
|
connectionFinalizer.scheduleWithFixedDelay(
|
new ConnectionFinalizerRunnable(), 100, 100, TimeUnit.MILLISECONDS);
|
|
// Create and start the request handlers.
|
requestHandlers = new LDAPRequestHandler[numRequestHandlers];
|
for (int i = 0; i < numRequestHandlers; i++)
|
{
|
requestHandlers[i] = new LDAPRequestHandler(this, i);
|
}
|
|
for (int i = 0; i < numRequestHandlers; i++)
|
{
|
requestHandlers[i].start();
|
}
|
|
// Register the set of supported LDAP versions.
|
DirectoryServer.registerSupportedLDAPVersion(3, this);
|
if (config.isAllowLDAPV2())
|
{
|
DirectoryServer.registerSupportedLDAPVersion(2, this);
|
}
|
|
// Create and register monitors.
|
statTracker = new LDAPStatistics(handlerName + " Statistics");
|
DirectoryServer.registerMonitorProvider(statTracker);
|
|
connMonitor = new ClientConnectionMonitorProvider(this);
|
DirectoryServer.registerMonitorProvider(connMonitor);
|
|
// Register this as a change listener.
|
config.addLDAPChangeListener(this);
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override()
|
public boolean isConfigurationAcceptable(ConnectionHandlerCfg configuration,
|
List<LocalizableMessage> unacceptableReasons)
|
{
|
LDAPConnectionHandlerCfg config = (LDAPConnectionHandlerCfg) configuration;
|
|
if ((currentConfig == null)
|
|| (!currentConfig.isEnabled() && config.isEnabled()))
|
{
|
// Attempt to bind to the listen port on all configured addresses to
|
// verify whether the connection handler will be able to start.
|
LocalizableMessage errorMessage =
|
checkAnyListenAddressInUse(config.getListenAddress(), config
|
.getListenPort(), config.isAllowTCPReuseAddress(), config.dn());
|
if (errorMessage != null)
|
{
|
unacceptableReasons.add(errorMessage);
|
return false;
|
}
|
}
|
|
if (config.isEnabled())
|
{
|
// Check that the SSL configuration is valid.
|
if (config.isUseSSL() || config.isAllowStartTLS())
|
{
|
try
|
{
|
SSLContext sslContext = createSSLContext(config);
|
createSSLEngine(config, sslContext);
|
}
|
catch (DirectoryException e)
|
{
|
logger.traceException(e);
|
|
unacceptableReasons.add(e.getMessageObject());
|
return false;
|
}
|
}
|
}
|
|
return true;
|
}
|
|
/**
|
* Checks whether any listen address is in use for the given port. The check
|
* is performed by binding to each address and port.
|
*
|
* @param listenAddresses
|
* the listen {@link InetAddress} to test
|
* @param listenPort
|
* the listen port to test
|
* @param allowReuseAddress
|
* whether addresses can be reused
|
* @param configEntryDN
|
* the configuration entry DN
|
* @return an error message if at least one of the address is already in use,
|
* null otherwise.
|
*/
|
private LocalizableMessage checkAnyListenAddressInUse(
|
Collection<InetAddress> listenAddresses, int listenPort,
|
boolean allowReuseAddress, DN configEntryDN)
|
{
|
for (InetAddress a : listenAddresses)
|
{
|
try
|
{
|
if (StaticUtils.isAddressInUse(a, listenPort, allowReuseAddress))
|
{
|
throw new IOException(ERR_CONNHANDLER_ADDRESS_INUSE.get().toString());
|
}
|
}
|
catch (IOException e)
|
{
|
logger.traceException(e);
|
return ERR_CONNHANDLER_CANNOT_BIND.get("LDAP", String
|
.valueOf(configEntryDN), a.getHostAddress(), listenPort,
|
getExceptionMessage(e));
|
}
|
}
|
return null;
|
}
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public boolean isConfigurationChangeAcceptable(
|
LDAPConnectionHandlerCfg config, List<LocalizableMessage> unacceptableReasons)
|
{
|
return isConfigurationAcceptable(config, unacceptableReasons);
|
}
|
|
|
|
/**
|
* Indicates whether this connection handler should maintain usage statistics.
|
*
|
* @return <CODE>true</CODE> if this connection handler should maintain usage
|
* statistics, or <CODE>false</CODE> if not.
|
*/
|
public boolean keepStats()
|
{
|
return currentConfig.isKeepStats();
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void processServerShutdown(LocalizableMessage reason)
|
{
|
shutdownRequested = true;
|
|
try
|
{
|
for (LDAPRequestHandler requestHandler : requestHandlers)
|
{
|
try
|
{
|
requestHandler.processServerShutdown(reason);
|
}
|
catch (Exception ignored)
|
{
|
}
|
}
|
}
|
catch (Exception ignored)
|
{
|
}
|
}
|
|
|
|
/**
|
* {@inheritDoc}
|
*/
|
@Override
|
public void start()
|
{
|
// The Directory Server start process should only return
|
// when the connection handlers port are fully opened
|
// and working. The start method therefore needs to wait for
|
// the created thread to
|
synchronized (waitListen)
|
{
|
super.start();
|
|
try
|
{
|
waitListen.wait();
|
}
|
catch (InterruptedException e)
|
{
|
// If something interrupted the start its probably better
|
// to return ASAP.
|
}
|
}
|
}
|
|
|
|
/**
|
* Operates in a loop, accepting new connections and ensuring that requests on
|
* those connections are handled properly.
|
*/
|
@Override
|
public void run()
|
{
|
setName(handlerName);
|
boolean listening = false;
|
|
while (!shutdownRequested)
|
{
|
// If this connection handler is not enabled, then just sleep
|
// for a bit and check again.
|
if (!enabled)
|
{
|
if (listening)
|
{
|
cleanUpSelector();
|
listening = false;
|
|
logError(NOTE_CONNHANDLER_STOPPED_LISTENING.get(handlerName));
|
}
|
|
StaticUtils.sleep(1000);
|
continue;
|
}
|
|
// If we have gotten here, then we are about to start listening
|
// for the first time since startup or since we were previously
|
// disabled. Make sure to start with a clean selector and then
|
// create all the listeners.
|
try
|
{
|
cleanUpSelector();
|
|
int numRegistered = registerChannels();
|
|
// At this point, the connection Handler either started
|
// correctly or failed to start but the start process
|
// should be notified and resume its work in any cases.
|
synchronized (waitListen)
|
{
|
waitListen.notify();
|
}
|
|
// If none of the listeners were created successfully, then
|
// consider the connection handler disabled and require
|
// administrative action before trying again.
|
if (numRegistered == 0)
|
{
|
logError(ERR_LDAP_CONNHANDLER_NO_ACCEPTORS.get(String
|
.valueOf(currentConfig.dn())));
|
|
enabled = false;
|
continue;
|
}
|
|
listening = true;
|
|
// Enter a loop, waiting for new connections to arrive and
|
// then accepting them as they come in.
|
boolean lastIterationFailed = false;
|
while (enabled && (!shutdownRequested))
|
{
|
try
|
{
|
serveIncomingConnections();
|
|
lastIterationFailed = false;
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
logError(ERR_CONNHANDLER_CANNOT_ACCEPT_CONNECTION.get(friendlyName,
|
String.valueOf(currentConfig.dn()), getExceptionMessage(e)));
|
|
if (lastIterationFailed)
|
{
|
// The last time through the accept loop we also
|
// encountered a failure. Rather than enter a potential
|
// infinite loop of failures, disable this acceptor and
|
// log an error.
|
LocalizableMessage message = ERR_CONNHANDLER_CONSECUTIVE_ACCEPT_FAILURES
|
.get(friendlyName, String.valueOf(currentConfig.dn()),
|
stackTraceToSingleLineString(e));
|
logError(message);
|
|
DirectoryServer.sendAlertNotification(this,
|
ALERT_TYPE_LDAP_CONNECTION_HANDLER_CONSECUTIVE_FAILURES,
|
message);
|
|
cleanUpSelector();
|
enabled = false;
|
}
|
else
|
{
|
lastIterationFailed = true;
|
}
|
}
|
}
|
|
if (shutdownRequested)
|
{
|
cleanUpSelector();
|
selector.close();
|
listening = false;
|
enabled = false;
|
}
|
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
// This is very bad because we failed outside the loop. The
|
// only thing we can do here is log a message, send an alert,
|
// and disable the selector until an administrator can figure
|
// out what's going on.
|
LocalizableMessage message = ERR_LDAP_CONNHANDLER_UNCAUGHT_ERROR
|
.get(String.valueOf(currentConfig.dn()),
|
stackTraceToSingleLineString(e));
|
logError(message);
|
|
DirectoryServer.sendAlertNotification(this,
|
ALERT_TYPE_LDAP_CONNECTION_HANDLER_UNCAUGHT_ERROR, message);
|
|
cleanUpSelector();
|
enabled = false;
|
}
|
}
|
}
|
|
|
/**
|
* Serves the incoming connections.
|
*
|
* @throws IOException
|
* @throws DirectoryException
|
*/
|
private void serveIncomingConnections() throws IOException, DirectoryException
|
{
|
int selectorState = selector.select();
|
|
// We can't rely on return value of select to determine if any keys
|
// are ready.
|
// see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4850373
|
for (Iterator<SelectionKey> iterator =
|
selector.selectedKeys().iterator(); iterator.hasNext();)
|
{
|
SelectionKey key = iterator.next();
|
iterator.remove();
|
if (key.isAcceptable())
|
{
|
// Accept the new client connection.
|
ServerSocketChannel serverChannel = (ServerSocketChannel) key
|
.channel();
|
SocketChannel clientChannel = serverChannel.accept();
|
if (clientChannel != null)
|
{
|
acceptConnection(clientChannel);
|
}
|
}
|
|
if (selectorState == 0 && enabled && (!shutdownRequested)
|
&& logger.isTraceEnabled())
|
{
|
// Selected keys was non empty but select() returned 0.
|
// Log warning and hope it blocks on the next select() call.
|
logger.trace("Selector.select() returned 0. "
|
+ "Selected Keys: %d, Interest Ops: %d, Ready Ops: %d ",
|
selector.selectedKeys().size(), key.interestOps(),
|
key.readyOps());
|
}
|
}
|
}
|
|
|
/**
|
* Open channels for each listen address and register them against this
|
* ConnectionHandler's {@link Selector}.
|
*
|
* @return the number of successfully registered channel
|
*/
|
private int registerChannels()
|
{
|
int numRegistered = 0;
|
for (InetAddress a : listenAddresses)
|
{
|
try
|
{
|
ServerSocketChannel channel = ServerSocketChannel.open();
|
channel.socket().setReuseAddress(allowReuseAddress);
|
channel.socket()
|
.bind(new InetSocketAddress(a, listenPort), backlog);
|
channel.configureBlocking(false);
|
channel.register(selector, SelectionKey.OP_ACCEPT);
|
numRegistered++;
|
|
logError(NOTE_CONNHANDLER_STARTED_LISTENING.get(handlerName));
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
logError(ERR_LDAP_CONNHANDLER_CREATE_CHANNEL_FAILED.get(
|
String.valueOf(currentConfig.dn()), a.getHostAddress(),
|
listenPort, stackTraceToSingleLineString(e)));
|
}
|
}
|
return numRegistered;
|
}
|
|
|
|
private void acceptConnection(SocketChannel clientChannel)
|
throws DirectoryException
|
{
|
try
|
{
|
clientChannel.socket().setKeepAlive(currentConfig.isUseTCPKeepAlive());
|
clientChannel.socket().setTcpNoDelay(currentConfig.isUseTCPNoDelay());
|
}
|
catch (SocketException se)
|
{
|
// TCP error occurred because connection reset/closed? In any case,
|
// just close it and ignore.
|
// See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6378870
|
close(clientChannel);
|
}
|
|
// Check to see if the core server rejected the
|
// connection (e.g., already too many connections
|
// established).
|
LDAPClientConnection clientConnection = new LDAPClientConnection(this,
|
clientChannel, getProtocol());
|
if (clientConnection.getConnectionID() < 0)
|
{
|
clientConnection.disconnect(DisconnectReason.ADMIN_LIMIT_EXCEEDED, true,
|
ERR_CONNHANDLER_REJECTED_BY_SERVER.get());
|
return;
|
}
|
|
InetAddress clientAddr = clientConnection.getRemoteAddress();
|
// Check to see if the client is on the denied list.
|
// If so, then reject it immediately.
|
if ((!deniedClients.isEmpty())
|
&& AddressMask.matchesAny(deniedClients, clientAddr))
|
{
|
clientConnection.disconnect(DisconnectReason.CONNECTION_REJECTED,
|
currentConfig.isSendRejectionNotice(), ERR_CONNHANDLER_DENIED_CLIENT
|
.get(clientConnection.getClientHostPort(), clientConnection
|
.getServerHostPort()));
|
return;
|
}
|
// Check to see if there is an allowed list and if
|
// there is whether the client is on that list. If
|
// not, then reject the connection.
|
if ((!allowedClients.isEmpty())
|
&& (!AddressMask.matchesAny(allowedClients, clientAddr)))
|
{
|
clientConnection.disconnect(DisconnectReason.CONNECTION_REJECTED,
|
currentConfig.isSendRejectionNotice(),
|
ERR_CONNHANDLER_DISALLOWED_CLIENT.get(clientConnection
|
.getClientHostPort(), clientConnection.getServerHostPort()));
|
return;
|
}
|
|
// If we've gotten here, then we'll take the
|
// connection so invoke the post-connect plugins and
|
// register the client connection with a request
|
// handler.
|
try
|
{
|
PluginConfigManager pluginManager = DirectoryServer
|
.getPluginConfigManager();
|
PluginResult.PostConnect pluginResult = pluginManager
|
.invokePostConnectPlugins(clientConnection);
|
if (!pluginResult.continueProcessing())
|
{
|
clientConnection.disconnect(pluginResult.getDisconnectReason(),
|
pluginResult.sendDisconnectNotification(),
|
pluginResult.getErrorMessage());
|
return;
|
}
|
|
LDAPRequestHandler requestHandler =
|
requestHandlers[requestHandlerIndex++];
|
if (requestHandlerIndex >= numRequestHandlers)
|
{
|
requestHandlerIndex = 0;
|
}
|
requestHandler.registerClient(clientConnection);
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
|
LocalizableMessage message =
|
INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(clientConnection
|
.getClientHostPort(), clientConnection.getServerHostPort(),
|
getExceptionMessage(e));
|
logError(message);
|
|
clientConnection.disconnect(DisconnectReason.SERVER_ERROR,
|
currentConfig.isSendRejectionNotice(), message);
|
}
|
}
|
|
|
|
/**
|
* Appends a string representation of this connection handler to the provided
|
* buffer.
|
*
|
* @param buffer
|
* The buffer to which the information should be appended.
|
*/
|
@Override
|
public void toString(StringBuilder buffer)
|
{
|
buffer.append(handlerName);
|
}
|
|
|
|
/**
|
* Indicates whether this connection handler should use SSL to communicate
|
* with clients.
|
*
|
* @return {@code true} if this connection handler should use SSL to
|
* communicate with clients, or {@code false} if not.
|
*/
|
public boolean useSSL()
|
{
|
return currentConfig.isUseSSL();
|
}
|
|
|
|
/**
|
* Cleans up the contents of the selector, closing any server socket channels
|
* that might be associated with it. Any connections that might have been
|
* established through those channels should not be impacted.
|
*/
|
private void cleanUpSelector()
|
{
|
try
|
{
|
for (SelectionKey key : selector.keys())
|
{
|
try
|
{
|
key.cancel();
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
}
|
|
try
|
{
|
key.channel().close();
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
}
|
}
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
}
|
}
|
|
|
|
/**
|
* Get the queueing strategy.
|
*
|
* @return The queueing strategy.
|
*/
|
public QueueingStrategy getQueueingStrategy()
|
{
|
return queueingStrategy;
|
}
|
|
|
|
/**
|
* Creates a TLS Byte Channel instance using the specified socket channel.
|
*
|
* @param channel
|
* The socket channel to use in the creation.
|
* @return A TLS Byte Channel instance.
|
* @throws DirectoryException
|
* If the channel cannot be created.
|
*/
|
public TLSByteChannel getTLSByteChannel(ByteChannel channel)
|
throws DirectoryException
|
{
|
SSLEngine sslEngine = createSSLEngine(currentConfig, sslContext);
|
return new TLSByteChannel(channel, sslEngine);
|
}
|
|
|
|
private SSLEngine createSSLEngine(LDAPConnectionHandlerCfg config,
|
SSLContext sslContext) throws DirectoryException
|
{
|
try
|
{
|
SSLEngine sslEngine = sslContext.createSSLEngine();
|
sslEngine.setUseClientMode(false);
|
|
final Set<String> protocols = config.getSSLProtocol();
|
if (!protocols.isEmpty())
|
{
|
sslEngine.setEnabledProtocols(protocols.toArray(new String[0]));
|
}
|
|
final Set<String> ciphers = config.getSSLCipherSuite();
|
if (!ciphers.isEmpty())
|
{
|
sslEngine.setEnabledCipherSuites(ciphers.toArray(new String[0]));
|
}
|
|
switch (config.getSSLClientAuthPolicy())
|
{
|
case DISABLED:
|
sslEngine.setNeedClientAuth(false);
|
sslEngine.setWantClientAuth(false);
|
break;
|
case REQUIRED:
|
sslEngine.setWantClientAuth(true);
|
sslEngine.setNeedClientAuth(true);
|
break;
|
case OPTIONAL:
|
default:
|
sslEngine.setNeedClientAuth(false);
|
sslEngine.setWantClientAuth(true);
|
break;
|
}
|
|
return sslEngine;
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
ResultCode resCode = DirectoryServer.getServerErrorResultCode();
|
LocalizableMessage message = ERR_CONNHANDLER_SSL_CANNOT_INITIALIZE
|
.get(getExceptionMessage(e));
|
throw new DirectoryException(resCode, message, e);
|
}
|
}
|
|
|
|
private SSLContext createSSLContext(LDAPConnectionHandlerCfg config)
|
throws DirectoryException
|
{
|
try
|
{
|
DN keyMgrDN = config.getKeyManagerProviderDN();
|
KeyManagerProvider<?> keyManagerProvider = DirectoryServer
|
.getKeyManagerProvider(keyMgrDN);
|
if (keyManagerProvider == null)
|
{
|
keyManagerProvider = new NullKeyManagerProvider();
|
}
|
|
String alias = config.getSSLCertNickname();
|
KeyManager[] keyManagers;
|
if (alias == null)
|
{
|
keyManagers = keyManagerProvider.getKeyManagers();
|
}
|
else
|
{
|
keyManagers = SelectableCertificateKeyManager.wrap(
|
keyManagerProvider.getKeyManagers(), alias);
|
}
|
|
DN trustMgrDN = config.getTrustManagerProviderDN();
|
TrustManagerProvider<?> trustManagerProvider = DirectoryServer
|
.getTrustManagerProvider(trustMgrDN);
|
if (trustManagerProvider == null)
|
{
|
trustManagerProvider = new NullTrustManagerProvider();
|
}
|
|
SSLContext sslContext = SSLContext.getInstance(SSL_CONTEXT_INSTANCE_NAME);
|
sslContext.init(keyManagers, trustManagerProvider.getTrustManagers(),
|
null);
|
return sslContext;
|
}
|
catch (Exception e)
|
{
|
logger.traceException(e);
|
ResultCode resCode = DirectoryServer.getServerErrorResultCode();
|
LocalizableMessage message = ERR_CONNHANDLER_SSL_CANNOT_INITIALIZE
|
.get(getExceptionMessage(e));
|
throw new DirectoryException(resCode, message, e);
|
}
|
}
|
|
|
|
/**
|
* Enqueue a connection finalizer which will be invoked after a short delay.
|
*
|
* @param r
|
* The connection finalizer runnable.
|
*/
|
void registerConnectionFinalizer(Runnable r)
|
{
|
synchronized (connectionFinalizerLock)
|
{
|
if (connectionFinalizer != null)
|
{
|
connectionFinalizerPendingJobQueue.add(r);
|
}
|
else
|
{
|
// Already finalized - invoked immediately.
|
r.run();
|
}
|
}
|
}
|
|
}
|