001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2017 ForgeRock AS.
016 */
017package org.opends.server.protocols.ldap;
018
019import static org.opends.messages.ProtocolMessages.*;
020import static org.opends.server.loggers.AccessLogger.logConnect;
021import static org.opends.server.util.StaticUtils.*;
022
023import java.io.IOException;
024import java.nio.channels.CancelledKeyException;
025import java.nio.channels.SelectionKey;
026import java.nio.channels.Selector;
027import java.nio.channels.SocketChannel;
028import java.util.ArrayList;
029import java.util.Collection;
030import java.util.Iterator;
031import java.util.LinkedList;
032import java.util.List;
033
034import org.forgerock.i18n.LocalizableMessage;
035import org.forgerock.i18n.slf4j.LocalizedLogger;
036import org.forgerock.opendj.io.ASN1Reader;
037import org.opends.server.api.DirectoryThread;
038import org.opends.server.api.ServerShutdownListener;
039import org.opends.server.types.DisconnectReason;
040import org.opends.server.types.InitializationException;
041import org.opends.server.types.LDAPException;
042
043/**
044 * This class defines an LDAP request handler, which is associated with an LDAP
045 * connection handler and is responsible for reading and decoding any requests
046 * that LDAP clients may send to the server.  Multiple request handlers may be
047 * used in conjunction with a single connection handler for better performance
048 * and scalability.
049 */
050public class LDAPRequestHandler
051       extends DirectoryThread
052       implements ServerShutdownListener
053{
054  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
055
056  /** Indicates whether the Directory Server is in the process of shutting down. */
057  private volatile boolean shutdownRequested;
058  /** The current set of selection keys. */
059  private volatile SelectionKey[] keys = new SelectionKey[0];
060
061  /**
062   * The queue that will be used to hold the set of pending connections that
063   * need to be registered with the selector.
064   * TODO: revisit, see Issue 4202.
065   */
066  private List<LDAPClientConnection> pendingConnections = new LinkedList<>();
067
068  /** Lock object for synchronizing access to the pending connections queue. */
069  private final Object pendingConnectionsLock = new Object();
070  /** The list of connections ready for request processing. */
071  private final LinkedList<LDAPClientConnection> readyConnections = new LinkedList<>();
072  /** The selector that will be used to monitor the client connections. */
073  private final Selector selector;
074  /** The name to use for this request handler. */
075  private final String handlerName;
076
077
078
079  /**
080   * Creates a new LDAP request handler that will be associated with the
081   * provided connection handler.
082   *
083   * @param  connectionHandler  The LDAP connection handler with which this
084   *                            request handler is associated.
085   * @param  requestHandlerID   The integer value that may be used to distinguish
086   *                            this request handler from others associated with
087   *                            the same connection handler.
088   * @throws  InitializationException  If a problem occurs while initializing
089   *                                   this request handler.
090   */
091  public LDAPRequestHandler(LDAPConnectionHandler connectionHandler,
092                            int requestHandlerID)
093         throws InitializationException
094  {
095    super("LDAP Request Handler " + requestHandlerID +
096          " for connection handler " + connectionHandler);
097
098
099    handlerName        = getName();
100
101    try
102    {
103      selector = Selector.open();
104    }
105    catch (Exception e)
106    {
107      logger.traceException(e);
108
109      LocalizableMessage message = ERR_LDAP_REQHANDLER_OPEN_SELECTOR_FAILED.get(handlerName, e);
110      throw new InitializationException(message, e);
111    }
112
113    try
114    {
115      // Check to see if we get an error while trying to perform a select.  If
116      // we do, then it's likely CR 6322825 and the server won't be able to
117      // handle LDAP requests in its current state.
118      selector.selectNow();
119    }
120    catch (IOException ioe)
121    {
122      StackTraceElement[] stackElements = ioe.getStackTrace();
123      if (stackElements != null && stackElements.length > 0)
124      {
125        StackTraceElement ste = stackElements[0];
126        if (ste.getClassName().equals("sun.nio.ch.DevPollArrayWrapper")
127            && ste.getMethodName().contains("poll")
128            && ioe.getMessage().equalsIgnoreCase("Invalid argument"))
129        {
130          LocalizableMessage message = ERR_LDAP_REQHANDLER_DETECTED_JVM_ISSUE_CR6322825.get(ioe);
131          throw new InitializationException(message, ioe);
132        }
133      }
134    }
135  }
136
137
138
139  /**
140   * Operates in a loop, waiting for client requests to arrive and ensuring that
141   * they are processed properly.
142   */
143  @Override
144  public void run()
145  {
146    // Operate in a loop until the server shuts down.  Each time through the
147    // loop, check for new requests, then check for new connections.
148    while (!shutdownRequested)
149    {
150      LDAPClientConnection readyConnection = null;
151      while ((readyConnection = readyConnections.poll()) != null)
152      {
153        try
154        {
155          ASN1Reader asn1Reader = readyConnection.getASN1Reader();
156          boolean ldapMessageProcessed = false;
157          while (true)
158          {
159            if (asn1Reader.elementAvailable())
160            {
161              if (!ldapMessageProcessed)
162              {
163                if (readyConnection.processLDAPMessage(
164                    LDAPReader.readMessage(asn1Reader)))
165                {
166                  ldapMessageProcessed = true;
167                }
168                else
169                {
170                  break;
171                }
172              }
173              else
174              {
175                readyConnections.add(readyConnection);
176                break;
177              }
178            }
179            else
180            {
181              if (readyConnection.processDataRead() <= 0)
182              {
183                break;
184              }
185            }
186          }
187        }
188        catch (LDAPException e)
189        {
190          logger.traceException(e);
191          readyConnection.disconnect(DisconnectReason.PROTOCOL_ERROR, false,
192            e.getMessageObject());
193        }
194        catch (IOException e)
195        {
196          logger.traceException(e);
197          readyConnection.disconnect(DisconnectReason.PROTOCOL_ERROR, false,
198            LocalizableMessage.raw(e.toString()));
199        }
200      }
201
202      // Check to see if we have any pending connections that need to be
203      // registered with the selector.
204      List<LDAPClientConnection> tmp = null;
205      synchronized (pendingConnectionsLock)
206      {
207        if (!pendingConnections.isEmpty())
208        {
209          tmp = pendingConnections;
210          pendingConnections = new LinkedList<>();
211        }
212      }
213
214      if (tmp != null)
215      {
216        for (LDAPClientConnection c : tmp)
217        {
218          try
219          {
220            SocketChannel socketChannel = c.getSocketChannel();
221            socketChannel.configureBlocking(false);
222            socketChannel.register(selector, SelectionKey.OP_READ, c);
223            logConnect(c);
224          }
225          catch (Exception e)
226          {
227            logger.traceException(e);
228
229            c.disconnect(DisconnectReason.SERVER_ERROR, true,
230                ERR_LDAP_REQHANDLER_CANNOT_REGISTER.get(handlerName, e));
231          }
232        }
233      }
234
235      // Create a copy of the selection keys which can be used in a
236      // thread-safe manner by getClientConnections. This copy is only
237      // updated once per loop, so may not be accurate.
238      keys = selector.keys().toArray(new SelectionKey[0]);
239
240      int selectedKeys = 0;
241      try
242      {
243        // We timeout every second so that we can refresh the key list.
244        selectedKeys = selector.select(1000);
245      }
246      catch (Exception e)
247      {
248        logger.traceException(e);
249
250        // FIXME -- Should we do something else with this?
251      }
252
253      if (shutdownRequested)
254      {
255        // Avoid further processing and disconnect all clients.
256        break;
257      }
258
259      if (selectedKeys > 0)
260      {
261        Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
262        while (iterator.hasNext())
263        {
264          SelectionKey key = iterator.next();
265
266          try
267          {
268            if (key.isReadable())
269            {
270              LDAPClientConnection clientConnection = null;
271
272              try
273              {
274                clientConnection = (LDAPClientConnection) key.attachment();
275
276                int readResult = clientConnection.processDataRead();
277                if (readResult < 0)
278                {
279                  key.cancel();
280                }
281                if (readResult > 0) {
282                  readyConnections.add(clientConnection);
283                }
284              }
285              catch (Exception e)
286              {
287                logger.traceException(e);
288
289                // We got some other kind of error.  If nothing else, cancel the
290                // key, but if the client connection is available then
291                // disconnect it as well.
292                key.cancel();
293
294                if (clientConnection != null)
295                {
296                  clientConnection.disconnect(DisconnectReason.SERVER_ERROR, false,
297                      ERR_UNEXPECTED_EXCEPTION_ON_CLIENT_CONNECTION.get(getExceptionMessage(e)));
298                }
299              }
300            }
301            else if (! key.isValid())
302            {
303              key.cancel();
304            }
305          }
306          catch (CancelledKeyException cke)
307          {
308            logger.traceException(cke);
309
310            // This could happen if a connection was closed between the time
311            // that select returned and the time that we try to access the
312            // associated channel.  If that was the case, we don't need to do
313            // anything.
314          }
315          catch (Exception e)
316          {
317            logger.traceException(e);
318
319            // This should not happen, and it would have caused our reader
320            // thread to die.  Log a severe error.
321            logger.error(ERR_LDAP_REQHANDLER_UNEXPECTED_SELECT_EXCEPTION, getName(), getExceptionMessage(e));
322          }
323          finally
324          {
325            if (!key.isValid())
326            {
327              // Help GC - release the connection.
328              key.attach(null);
329            }
330
331            iterator.remove();
332          }
333        }
334      }
335    }
336
337    // Disconnect all active connections.
338    SelectionKey[] keyArray = selector.keys().toArray(new SelectionKey[0]);
339    for (SelectionKey key : keyArray)
340    {
341      LDAPClientConnection c = (LDAPClientConnection) key.attachment();
342
343      try
344      {
345        key.channel().close();
346      }
347      catch (Exception e)
348      {
349        logger.traceException(e);
350      }
351
352      try
353      {
354        key.cancel();
355      }
356      catch (Exception e)
357      {
358        logger.traceException(e);
359      }
360
361      try
362      {
363        c.disconnect(DisconnectReason.SERVER_SHUTDOWN, true,
364            ERR_LDAP_REQHANDLER_DEREGISTER_DUE_TO_SHUTDOWN.get());
365      }
366      catch (Exception e)
367      {
368        logger.traceException(e);
369      }
370    }
371
372    // Disconnect all pending connections.
373    synchronized (pendingConnectionsLock)
374    {
375      for (LDAPClientConnection c : pendingConnections)
376      {
377        try
378        {
379          c.disconnect(DisconnectReason.SERVER_SHUTDOWN, true,
380              ERR_LDAP_REQHANDLER_DEREGISTER_DUE_TO_SHUTDOWN.get());
381        }
382        catch (Exception e)
383        {
384          logger.traceException(e);
385        }
386      }
387    }
388  }
389
390
391
392  /**
393   * Registers the provided client connection with this request
394   * handler so that any requests received from that client will be
395   * processed.
396   *
397   * @param clientConnection
398   *          The client connection to be registered with this request
399   *          handler.
400   * @return <CODE>true</CODE> if the client connection was properly
401   *         registered with this request handler, or
402   *         <CODE>false</CODE> if not.
403   */
404  public boolean registerClient(LDAPClientConnection clientConnection)
405  {
406    // FIXME -- Need to check if the maximum client limit has been reached.
407
408
409    // If the server is in the process of shutting down, then we don't want to
410    // accept it.
411    if (shutdownRequested)
412    {
413      clientConnection.disconnect(DisconnectReason.SERVER_SHUTDOWN, true,
414           ERR_LDAP_REQHANDLER_REJECT_DUE_TO_SHUTDOWN.get());
415      return false;
416    }
417
418    // Try to add the new connection to the queue.  If it succeeds, then wake
419    // up the selector so it will be picked up right away.  Otherwise,
420    // disconnect the client.
421    synchronized (pendingConnectionsLock)
422    {
423      pendingConnections.add(clientConnection);
424    }
425
426    selector.wakeup();
427    return true;
428  }
429
430
431
432  /**
433   * Retrieves the set of all client connections that are currently registered
434   * with this request handler.
435   *
436   * @return  The set of all client connections that are currently registered
437   *          with this request handler.
438   */
439  public Collection<LDAPClientConnection> getClientConnections()
440  {
441    ArrayList<LDAPClientConnection> connList = new ArrayList<>(keys.length);
442    for (SelectionKey key : keys)
443    {
444      LDAPClientConnection c = (LDAPClientConnection) key.attachment();
445
446      // If the client has disconnected the attachment may be null.
447      if (c != null)
448      {
449        connList.add(c);
450      }
451    }
452
453    return connList;
454  }
455
456  @Override
457  public String getShutdownListenerName()
458  {
459    return handlerName;
460  }
461
462  @Override
463  public void processServerShutdown(LocalizableMessage reason)
464  {
465    shutdownRequested = true;
466    selector.wakeup();
467  }
468}
469