/* * 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-2015 ForgeRock AS */ package org.opends.server.protocols.ldap; import static org.opends.messages.ProtocolMessages.*; 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.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.config.server.ConfigChangeResult; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.opendj.ldap.AddressMask; import org.forgerock.opendj.ldap.ResultCode; 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.core.DirectoryServer; import org.opends.server.core.PluginConfigManager; import org.opends.server.core.QueueingStrategy; import org.opends.server.core.ServerContext; 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.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) { final ConfigChangeResult ccr = new ConfigChangeResult(); // 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() && 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); ccr.setResultCode(e.getResultCode()); ccr.addMessage(e.getMessageObject()); return ccr; } if (config.isAllowLDAPV2()) { DirectoryServer.registerSupportedLDAPVersion(2, this); } else { DirectoryServer.deregisterSupportedLDAPVersion(2, this); } return ccr; } 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<>(); 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<>(); 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(ServerContext serverContext, 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( 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 { // This call may disable the connector if wrong SSL settings 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<>(); 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) { logger.error(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<>(); connectionFinalizerPendingJobQueue = new ArrayList<>(); 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. && (config.isUseSSL() || config.isAllowStartTLS())) { try { createSSLEngine(config, createSSLContext(config)); } 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", 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; boolean starting = true; 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; logger.info(NOTE_CONNHANDLER_STOPPED_LISTENING, handlerName); } if (starting) { // This may happen if there was an initialisation error // which led to disable the connector. // The main thread is waiting for the connector to listen // on its port, which will not occur yet, // so notify here to allow the server startup to complete. synchronized (waitListen) { starting = false; waitListen.notify(); } } 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) { logger.error(ERR_LDAP_CONNHANDLER_NO_ACCEPTORS, 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); logger.error(ERR_CONNHANDLER_CANNOT_ACCEPT_CONNECTION, friendlyName, 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, currentConfig.dn(), stackTraceToSingleLineString(e)); logger.error(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(currentConfig.dn(), stackTraceToSingleLineString(e)); logger.error(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++; logger.info(NOTE_CONNHANDLER_STARTED_LISTENING, handlerName); } catch (Exception e) { logger.traceException(e); logger.error(ERR_LDAP_CONNHANDLER_CREATE_CHANNEL_FAILED, 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)); logger.debug(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 void disableAndWarnIfUseSSL(LDAPConnectionHandlerCfg config) { if (config.isUseSSL()) { logger.warn(INFO_DISABLE_CONNECTION, friendlyName); enabled = false; } } private SSLContext createSSLContext(LDAPConnectionHandlerCfg config) throws DirectoryException { try { DN keyMgrDN = config.getKeyManagerProviderDN(); KeyManagerProvider<?> keyManagerProvider = DirectoryServer .getKeyManagerProvider(keyMgrDN); if (keyManagerProvider == null) { logger.error(ERR_NULL_KEY_PROVIDER_MANAGER, keyMgrDN, friendlyName); disableAndWarnIfUseSSL(config); keyManagerProvider = new NullKeyManagerProvider(); // The SSL connection is unusable without a key manager provider } else if (! keyManagerProvider.containsAtLeastOneKey()) { logger.error(ERR_INVALID_KEYSTORE, friendlyName); disableAndWarnIfUseSSL(config); } final SortedSet<String> aliases = new TreeSet<>(config.getSSLCertNickname()); final KeyManager[] keyManagers; if (aliases.isEmpty()) { keyManagers = keyManagerProvider.getKeyManagers(); } else { final Iterator<String> it = aliases.iterator(); while (it.hasNext()) { if (!keyManagerProvider.containsKeyWithAlias(it.next())) { logger.error(ERR_KEYSTORE_DOES_NOT_CONTAIN_ALIAS, aliases, friendlyName); it.remove(); } } if (aliases.isEmpty()) { disableAndWarnIfUseSSL(config); } keyManagers = SelectableCertificateKeyManager.wrap(keyManagerProvider.getKeyManagers(), aliases, friendlyName); } 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(); } } } }