/* * 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 2014-2015 ForgeRock AS */ package org.opends.server.protocols.ldap; import static org.opends.messages.ProtocolMessages.*; import static org.opends.server.loggers.AccessLogger.logConnect; import static org.opends.server.util.StaticUtils.*; import java.io.IOException; import java.nio.channels.CancelledKeyException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import org.forgerock.i18n.LocalizableMessage; import org.opends.server.api.DirectoryThread; import org.opends.server.api.ServerShutdownListener; import org.opends.server.core.DirectoryServer; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.io.ASN1Reader; import org.forgerock.opendj.ldap.DecodeException; import org.opends.server.types.DisconnectReason; import org.opends.server.types.InitializationException; import org.opends.server.types.LDAPException; /** * This class defines an LDAP request handler, which is associated with an LDAP * connection handler and is responsible for reading and decoding any requests * that LDAP clients may send to the server. Multiple request handlers may be * used in conjunction with a single connection handler for better performance * and scalability. */ public class LDAPRequestHandler extends DirectoryThread implements ServerShutdownListener { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** Indicates whether the Directory Server is in the process of shutting down. */ private volatile boolean shutdownRequested; /** The current set of selection keys. */ private volatile SelectionKey[] keys = new SelectionKey[0]; /** * The queue that will be used to hold the set of pending connections that * need to be registered with the selector. * TODO: revisit, see Issue 4202. */ private List<LDAPClientConnection> pendingConnections = new LinkedList<>(); /** Lock object for synchronizing access to the pending connections queue. */ private final Object pendingConnectionsLock = new Object(); /** The list of connections ready for request processing. */ private LinkedList<LDAPClientConnection> readyConnections = new LinkedList<>(); /** The selector that will be used to monitor the client connections. */ private final Selector selector; /** The name to use for this request handler. */ private final String handlerName; /** * Creates a new LDAP request handler that will be associated with the * provided connection handler. * * @param connectionHandler The LDAP connection handler with which this * request handler is associated. * @param requestHandlerID The integer value that may be used to distinguish * this request handler from others associated with * the same connection handler. * @throws InitializationException If a problem occurs while initializing * this request handler. */ public LDAPRequestHandler(LDAPConnectionHandler connectionHandler, int requestHandlerID) throws InitializationException { super("LDAP Request Handler " + requestHandlerID + " for connection handler " + connectionHandler); handlerName = getName(); try { selector = Selector.open(); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_LDAP_REQHANDLER_OPEN_SELECTOR_FAILED.get(handlerName, e); throw new InitializationException(message, e); } try { // Check to see if we get an error while trying to perform a select. If // we do, then it's likely CR 6322825 and the server won't be able to // handle LDAP requests in its current state. selector.selectNow(); } catch (IOException ioe) { StackTraceElement[] stackElements = ioe.getStackTrace(); if (stackElements != null && stackElements.length > 0) { StackTraceElement ste = stackElements[0]; if (ste.getClassName().equals("sun.nio.ch.DevPollArrayWrapper") && ste.getMethodName().contains("poll") && ioe.getMessage().equalsIgnoreCase("Invalid argument")) { LocalizableMessage message = ERR_LDAP_REQHANDLER_DETECTED_JVM_ISSUE_CR6322825.get(ioe); throw new InitializationException(message, ioe); } } } } /** * Operates in a loop, waiting for client requests to arrive and ensuring that * they are processed properly. */ @Override public void run() { // Operate in a loop until the server shuts down. Each time through the // loop, check for new requests, then check for new connections. while (!shutdownRequested) { LDAPClientConnection readyConnection = null; while ((readyConnection = readyConnections.poll()) != null) { try { ASN1Reader asn1Reader = readyConnection.getASN1Reader(); boolean ldapMessageProcessed = false; while (true) { if (asn1Reader.elementAvailable()) { if (!ldapMessageProcessed) { if (readyConnection.processLDAPMessage( LDAPReader.readMessage(asn1Reader))) { ldapMessageProcessed = true; } else { break; } } else { readyConnections.add(readyConnection); break; } } else { if (readyConnection.processDataRead() <= 0) { break; } } } } catch (DecodeException | LDAPException e) { logger.traceException(e); readyConnection.disconnect(DisconnectReason.PROTOCOL_ERROR, true, e.getMessageObject()); } catch (Exception e) { logger.traceException(e); readyConnection.disconnect(DisconnectReason.PROTOCOL_ERROR, true, LocalizableMessage.raw(e.toString())); } } // Check to see if we have any pending connections that need to be // registered with the selector. List<LDAPClientConnection> tmp = null; synchronized (pendingConnectionsLock) { if (!pendingConnections.isEmpty()) { tmp = pendingConnections; pendingConnections = new LinkedList<>(); } } if (tmp != null) { for (LDAPClientConnection c : tmp) { try { SocketChannel socketChannel = c.getSocketChannel(); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ, c); logConnect(c); } catch (Exception e) { logger.traceException(e); c.disconnect(DisconnectReason.SERVER_ERROR, true, ERR_LDAP_REQHANDLER_CANNOT_REGISTER.get(handlerName, e)); } } } // Create a copy of the selection keys which can be used in a // thread-safe manner by getClientConnections. This copy is only // updated once per loop, so may not be accurate. keys = selector.keys().toArray(new SelectionKey[0]); int selectedKeys = 0; try { // We timeout every second so that we can refresh the key list. selectedKeys = selector.select(1000); } catch (Exception e) { logger.traceException(e); // FIXME -- Should we do something else with this? } if (shutdownRequested) { // Avoid further processing and disconnect all clients. break; } if (selectedKeys > 0) { Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); try { if (key.isReadable()) { LDAPClientConnection clientConnection = null; try { clientConnection = (LDAPClientConnection) key.attachment(); int readResult = clientConnection.processDataRead(); if (readResult < 0) { key.cancel(); } if (readResult > 0) { readyConnections.add(clientConnection); } } catch (Exception e) { logger.traceException(e); // We got some other kind of error. If nothing else, cancel the // key, but if the client connection is available then // disconnect it as well. key.cancel(); if (clientConnection != null) { clientConnection.disconnect(DisconnectReason.SERVER_ERROR, false, ERR_UNEXPECTED_EXCEPTION_ON_CLIENT_CONNECTION.get(getExceptionMessage(e))); } } } else if (! key.isValid()) { key.cancel(); } } catch (CancelledKeyException cke) { logger.traceException(cke); // This could happen if a connection was closed between the time // that select returned and the time that we try to access the // associated channel. If that was the case, we don't need to do // anything. } catch (Exception e) { logger.traceException(e); // This should not happen, and it would have caused our reader // thread to die. Log a severe error. logger.error(ERR_LDAP_REQHANDLER_UNEXPECTED_SELECT_EXCEPTION, getName(), getExceptionMessage(e)); } finally { if (!key.isValid()) { // Help GC - release the connection. key.attach(null); } iterator.remove(); } } } } // Disconnect all active connections. SelectionKey[] keyArray = selector.keys().toArray(new SelectionKey[0]); for (SelectionKey key : keyArray) { LDAPClientConnection c = (LDAPClientConnection) key.attachment(); try { key.channel().close(); } catch (Exception e) { logger.traceException(e); } try { key.cancel(); } catch (Exception e) { logger.traceException(e); } try { c.disconnect(DisconnectReason.SERVER_SHUTDOWN, true, ERR_LDAP_REQHANDLER_DEREGISTER_DUE_TO_SHUTDOWN.get()); } catch (Exception e) { logger.traceException(e); } } // Disconnect all pending connections. synchronized (pendingConnectionsLock) { for (LDAPClientConnection c : pendingConnections) { try { c.disconnect(DisconnectReason.SERVER_SHUTDOWN, true, ERR_LDAP_REQHANDLER_DEREGISTER_DUE_TO_SHUTDOWN.get()); } catch (Exception e) { logger.traceException(e); } } } } /** * Registers the provided client connection with this request * handler so that any requests received from that client will be * processed. * * @param clientConnection * The client connection to be registered with this request * handler. * @return <CODE>true</CODE> if the client connection was properly * registered with this request handler, or * <CODE>false</CODE> if not. */ public boolean registerClient(LDAPClientConnection clientConnection) { // FIXME -- Need to check if the maximum client limit has been reached. // If the server is in the process of shutting down, then we don't want to // accept it. if (shutdownRequested) { clientConnection.disconnect(DisconnectReason.SERVER_SHUTDOWN, true, ERR_LDAP_REQHANDLER_REJECT_DUE_TO_SHUTDOWN.get()); return false; } // Try to add the new connection to the queue. If it succeeds, then wake // up the selector so it will be picked up right away. Otherwise, // disconnect the client. synchronized (pendingConnectionsLock) { pendingConnections.add(clientConnection); } selector.wakeup(); return true; } /** * Retrieves the set of all client connections that are currently registered * with this request handler. * * @return The set of all client connections that are currently registered * with this request handler. */ public Collection<LDAPClientConnection> getClientConnections() { ArrayList<LDAPClientConnection> connList = new ArrayList<>(keys.length); for (SelectionKey key : keys) { LDAPClientConnection c = (LDAPClientConnection) key.attachment(); // If the client has disconnected the attachment may be null. if (c != null) { connList.add(c); } } return connList; } /** * Retrieves the human-readable name for this shutdown listener. * * @return The human-readable name for this shutdown listener. */ public String getShutdownListenerName() { return handlerName; } /** * Causes this request handler to register itself as a shutdown listener with * the Directory Server. This must be called if the connection handler is * shut down without closing all associated connections, otherwise the thread * would not be stopped by the server. */ public void registerShutdownListener() { DirectoryServer.registerShutdownListener(this); } /** * Indicates that the Directory Server has received a request to stop running * and that this shutdown listener should take any action necessary to prepare * for it. * * @param reason The human-readable reason for the shutdown. */ public void processServerShutdown(LocalizableMessage reason) { shutdownRequested = true; selector.wakeup(); } }