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

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

/*
 * Copyright 2007-2024 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2007-2024 Ping Identity Corporation
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
/*
 * Copyright (C) 2007-2024 Ping Identity Corporation
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License (GPLv2 only)
 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
 * as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, see .
 */
package com.unboundid.ldap.sdk;



import java.io.BufferedInputStream;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.security.sasl.SaslClient;

import com.unboundid.asn1.ASN1Exception;
import com.unboundid.asn1.ASN1StreamReader;
import com.unboundid.asn1.InternalASN1Helper;
import com.unboundid.ldap.protocol.LDAPMessage;
import com.unboundid.ldap.protocol.LDAPResponse;
import com.unboundid.ldap.sdk.extensions.NoticeOfDisconnectionExtendedResult;
import com.unboundid.util.Debug;
import com.unboundid.util.DebugType;
import com.unboundid.util.InternalUseOnly;
import com.unboundid.util.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.WakeableSleeper;

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



/**
 * This class provides a thread that will read data from the socket associated
 * with an LDAP connection.  It will accept messages from the server, and
 * associate responses with their corresponding requests.
 */
@InternalUseOnly()
final class LDAPConnectionReader
      extends Thread
{
  /**
   * The default size that will be used for the input stream buffer.
   */
  private static final int DEFAULT_INPUT_BUFFER_SIZE = 4096;



  // The ASN.1 stream reader used to read LDAP messages from the server.
  @NotNull private volatile ASN1StreamReader asn1StreamReader;

  // Indicates whether a request has been made to close the associated socket.
  private volatile boolean closeRequested;

  // The map that will be used to associate message IDs with the corresponding
  // response acceptors.
  @NotNull private final ConcurrentHashMap
       acceptorMap;

  // The exception encountered during StartTLS processing.
  @Nullable private volatile Exception startTLSException;

  // The input stream used to read data from the socket.
  @Nullable private volatile InputStream inputStream;

  // The SSL-enabled output stream resulting from StartTLS negotiation.  It will
  // be non-null only immediately after StartTLS negotiation has completed and
  // this output stream is ready to be handed back to the connection.
  @Nullable private volatile OutputStream startTLSOutputStream;

  // The LDAP connection with which this reader is associated.
  @NotNull private final LDAPConnection connection;

  // The socket with which this reader is associated.
  @NotNull private volatile Socket socket;

  // The SSL socket factory to use to convert an insecure connection to a secure
  // one when performing StartTLS processing.  It will be null unless there is
  // an outstanding StartTLS request.
  @Nullable private volatile SSLSocketFactory sslSocketFactory;

  // The thread that is used to read data from the client.
  @Nullable private volatile Thread thread;

  // The wakeable sleeper that will be used during StartTLS processing.
  @NotNull private final WakeableSleeper startTLSSleeper;



  /**
   * Creates a new LDAP connection reader instance that will read data from the
   * provided socket.
   *
   * @param  connection           The LDAP connection with which this reader is
   *                              associated.
   * @param  connectionInternals  The elements of the LDAP connection actually
   *                              used to communicate with the directory server.
   *
   * @throws  IOException  If a problem occurs while preparing to read data from
   *                       the provided socket.
   */
  LDAPConnectionReader(@NotNull final LDAPConnection connection,
       @NotNull final LDAPConnectionInternals connectionInternals)
       throws IOException
  {
    this.connection = connection;

    setName(constructThreadName(connectionInternals));
    setDaemon(true);

    socket               = connectionInternals.getSocket();
    inputStream          = new BufferedInputStream(socket.getInputStream(),
                                                   DEFAULT_INPUT_BUFFER_SIZE);
    asn1StreamReader = new ASN1StreamReader(inputStream,
         connection.getConnectionOptions().getMaxMessageSize());

    acceptorMap = new ConcurrentHashMap<>(StaticUtils.computeMapCapacity(10));
    closeRequested = false;
    sslSocketFactory = null;
    startTLSException = null;
    startTLSOutputStream = null;
    startTLSSleeper = new WakeableSleeper();
  }



  /**
   * Registers the provided response acceptor to be notified of any responses
   * with the given message ID.
   *
   * @param  messageID  The message ID for which to register the acceptor.
   * @param  acceptor   The response acceptor that should be notified for any
   *                    responses with the provided message ID.
   *
   * @throws  LDAPException  If another acceptor is already registered for the
   *                         provided message ID.
   */
  void registerResponseAcceptor(final int messageID,
                                @NotNull final ResponseAcceptor acceptor)
       throws LDAPException
  {
    final ResponseAcceptor existingAcceptor =
         acceptorMap.putIfAbsent(messageID, acceptor);
    if (existingAcceptor != null)
    {
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_CONNREADER_MSGID_IN_USE.get(String.valueOf(acceptor), messageID,
                String.valueOf(connection), String.valueOf(existingAcceptor)));
    }
  }



  /**
   * Deregisters the response acceptor that has been registered with the
   * specified message ID.  This will have no effect if no response acceptor is
   * registered for the provided message ID.
   *
   * @param  messageID  The message ID for the response acceptor to deregister.
   */
  void deregisterResponseAcceptor(final int messageID)
  {
    acceptorMap.remove(messageID);
  }



  /**
   * Retrieves the number of outstanding operations on the LDAP connection,
   * which are operations for which the request has been sent but the final
   * result has not yet been received.  The value will only be valid for
   * connections not configured to use synchronous mode.
   *
   * @return  The number of outstanding operations on the associated LDAP
   *          connection.
   */
  int getActiveOperationCount()
  {
    return acceptorMap.size();
  }



  /**
   * Operates in a loop, reading data from the server and decoding the
   * responses, and associating them with their corresponding requests.
   */
  @Override()
  @SuppressWarnings("deprecation")
  public void run()
  {
    boolean reconnect  = false;

    thread = Thread.currentThread();

    while (! closeRequested)
    {
      try
      {
        final LDAPResponse response;
        try
        {
          response = LDAPMessage.readLDAPResponseFrom(asn1StreamReader, true,
               connection.getCachedSchema());
        }
        catch (final LDAPException le)
        {
          final Throwable t = le.getCause();
          if ((t != null) && (t instanceof SocketTimeoutException))
          {
            // This is rarely a problem, so we can make the debug message for
            // this exception only visible at a verbose log level.
            final SocketTimeoutException ste = (SocketTimeoutException) t;
            Debug.debugException(Level.FINEST, ste);
            if (sslSocketFactory != null)
            {
              final LDAPConnectionOptions connectionOptions =
                   connection.getConnectionOptions();
              try
              {
                final int responseTimeoutMillis =
                     (int) connectionOptions.getResponseTimeoutMillis();
                if (responseTimeoutMillis > 0)
                {
                  InternalSDKHelper.setSoTimeout(connection,
                       responseTimeoutMillis);
                }
                else
                {
                  InternalSDKHelper.setSoTimeout(connection, 0);
                }

                final SSLSocket sslSocket;
                synchronized (sslSocketFactory)
                {
                  sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket,
                       connection.getConnectedAddress(), socket.getPort(),
                       true);
                  sslSocket.startHandshake();
                }
                connectionOptions.getSSLSocketVerifier().verifySSLSocket(
                     connection.getConnectedAddress(), socket.getPort(),
                     sslSocket);
                inputStream =
                     new BufferedInputStream(sslSocket.getInputStream(),
                                             DEFAULT_INPUT_BUFFER_SIZE);
                asn1StreamReader = new ASN1StreamReader(inputStream,
                     connectionOptions.getMaxMessageSize());
                startTLSOutputStream = sslSocket.getOutputStream();
                socket = sslSocket;
                connection.getConnectionInternals(true).setSocket(sslSocket);
                startTLSSleeper.wakeup();
              }
              catch (final Exception e)
              {
                Debug.debugException(e);
                connection.setDisconnectInfo(DisconnectType.SECURITY_PROBLEM,
                     StaticUtils.getExceptionMessage(e), e);
                startTLSException = e;
                closeRequested = true;
                if (thread != null)
                {
                  thread.setName(thread.getName() + " (closed)");
                  thread = null;
                }
                closeInternal(true, StaticUtils.getExceptionMessage(e));
                startTLSSleeper.wakeup();
                return;
              }

              sslSocketFactory = null;
            }

            continue;
          }

          if (closeRequested || connection.closeRequested() ||
              (connection.getDisconnectType() != null))
          {
            // This exception resulted from the connection being closed in a way
            // that we already knew about.  We don't want to debug it at the
            // same level as a newly-detected invalidity.
            closeRequested = true;
            Debug.debugException(Level.FINEST, le);
          }
          else
          {
            Debug.debugException(le);
          }

          // We should terminate the connection regardless of the type of
          // exception, but might want to customize the debug message.
          final String message;
          Level debugLevel = Level.SEVERE;

          if (t == null)
          {
            connection.setDisconnectInfo(DisconnectType.DECODE_ERROR,
                 le.getMessage(), t);
            message = le.getMessage();
            debugLevel = Level.WARNING;
          }
          else if ((t instanceof InterruptedIOException) && socket.isClosed())
          {
            connection.setDisconnectInfo(
                 DisconnectType.SERVER_CLOSED_WITHOUT_NOTICE, le.getMessage(),
                 t);
            message = ERR_READER_CLOSING_DUE_TO_INTERRUPTED_IO.get(
                 connection.getHostPort());
            debugLevel = Level.WARNING;
          }
          else if (t instanceof IOException)
          {
            connection.setDisconnectInfo(DisconnectType.IO_ERROR,
                 le.getMessage(), t);
            message = ERR_READER_CLOSING_DUE_TO_IO_EXCEPTION.get(
                 connection.getHostPort(), StaticUtils.getExceptionMessage(t));
            debugLevel = Level.WARNING;
          }
          else if (t instanceof ASN1Exception)
          {
            connection.setDisconnectInfo(DisconnectType.DECODE_ERROR,
                 le.getMessage(), t);
            message = ERR_READER_CLOSING_DUE_TO_ASN1_EXCEPTION.get(
                 connection.getHostPort(), StaticUtils.getExceptionMessage(t));
          }
          else
          {
            connection.setDisconnectInfo(DisconnectType.LOCAL_ERROR,
                 le.getMessage(), t);
            message = ERR_READER_CLOSING_DUE_TO_EXCEPTION.get(
                 connection.getHostPort(), StaticUtils.getExceptionMessage(t));
          }

          Debug.debug(debugLevel, DebugType.LDAP, message, t);

          // If the connection is configured to try to auto-reconnect, then set
          // things up to do that.  Otherwise, terminate the connection.
          @SuppressWarnings("deprecation")
          final boolean autoReconnect =
               connection.getConnectionOptions().autoReconnect();
          if ((! closeRequested) && autoReconnect)
          {
            reconnect = true;
            break;
          }
          else
          {
            closeRequested = true;
            if (thread != null)
            {
              thread.setName(thread.getName() + " (closed)");
              thread = null;
            }
            closeInternal(true, message);
            return;
          }
        }

        if (response == null)
        {
          // This should only happen if the socket has been closed.
          connection.setDisconnectInfo(
               DisconnectType.SERVER_CLOSED_WITHOUT_NOTICE, null, null);
          @SuppressWarnings("deprecation")
          final boolean autoReconnect =
               connection.getConnectionOptions().autoReconnect();
          if ((! closeRequested) && (! connection.unbindRequestSent()) &&
              autoReconnect)
          {
            reconnect = true;
            break;
          }
          else
          {
            closeRequested = true;
            if (thread != null)
            {
              thread.setName(thread.getName() + " (closed)");
              thread = null;
            }
            closeInternal(true, null);
            return;
          }
        }

        connection.setLastCommunicationTime();
        Debug.debugLDAPResult(response, connection);
        logResponse(response);

        final ResponseAcceptor responseAcceptor;
        if ((response instanceof SearchResultEntry) ||
            (response instanceof SearchResultReference))
        {
          responseAcceptor = acceptorMap.get(response.getMessageID());
        }
        else if (response instanceof IntermediateResponse)
        {
          final IntermediateResponse ir = (IntermediateResponse) response;
          responseAcceptor = acceptorMap.get(response.getMessageID());
           IntermediateResponseListener l = null;
          if (responseAcceptor instanceof LDAPRequest)
          {
            final LDAPRequest r = (LDAPRequest) responseAcceptor;
            l = r.getIntermediateResponseListener();

          }
          else if (responseAcceptor instanceof IntermediateResponseListener)
          {
            l = (IntermediateResponseListener) responseAcceptor;
          }

          if (l == null)
          {
            Debug.debug(Level.WARNING, DebugType.LDAP,
                 WARN_INTERMEDIATE_RESPONSE_WITH_NO_LISTENER.get(
                      String.valueOf(ir)));
          }
          else
          {
            try
            {
              l.intermediateResponseReturned(ir);
            }
            catch (final Exception e)
            {
              Debug.debugException(e);
            }
          }
          continue;
        }
        else
        {
          responseAcceptor = acceptorMap.remove(response.getMessageID());
        }


        if (responseAcceptor == null)
        {
          if ((response instanceof ExtendedResult) &&
              (response.getMessageID() == 0))
          {
            // This is an intermediate response message, so handle it
            // appropriately.
            ExtendedResult extendedResult = (ExtendedResult) response;

            final String oid = extendedResult.getOID();
            if (NoticeOfDisconnectionExtendedResult.
                     NOTICE_OF_DISCONNECTION_RESULT_OID.equals(oid))
            {
              extendedResult = new NoticeOfDisconnectionExtendedResult(
                                        extendedResult);
              connection.setDisconnectInfo(
                   DisconnectType.SERVER_CLOSED_WITH_NOTICE,
                   extendedResult.getDiagnosticMessage(), null);
            }
            else if (com.unboundid.ldap.sdk.unboundidds.extensions.
                 InteractiveTransactionAbortedExtendedResult.
                      INTERACTIVE_TRANSACTION_ABORTED_RESULT_OID.equals(oid))
            {
              extendedResult = new com.unboundid.ldap.sdk.unboundidds.
                   extensions.InteractiveTransactionAbortedExtendedResult(
                        extendedResult);
            }

            final UnsolicitedNotificationHandler handler =
                 connection.getConnectionOptions().
                      getUnsolicitedNotificationHandler();
            if (handler == null)
            {
              if (Debug.debugEnabled(DebugType.LDAP))
              {
                Debug.debug(Level.WARNING, DebugType.LDAP,
                     WARN_READER_UNHANDLED_UNSOLICITED_NOTIFICATION.get(
                          response));
              }
            }
            else
            {
              handler.handleUnsolicitedNotification(connection,
                                                    extendedResult);
            }
            continue;
          }

          if (Debug.debugEnabled(DebugType.LDAP))
          {
            Debug.debug(Level.WARNING, DebugType.LDAP,
                  WARN_READER_NO_ACCEPTOR.get(response));
          }
          continue;
        }

        try
        {
          responseAcceptor.responseReceived(response);
        }
        catch (final LDAPException le)
        {
          Debug.debugException(le);
          Debug.debug(Level.WARNING, DebugType.LDAP,
                ERR_READER_ACCEPTOR_ERROR.get(String.valueOf(response),
                     connection.getHostPort(),
                     StaticUtils.getExceptionMessage(le)),
               le);
        }
      }
      catch (final Exception e)
      {
        Debug.debugException(e);

        // We should terminate the connection regardless of the type of
        // exception, but might want to customize the debug message.
        final String message;
        Level debugLevel = Level.SEVERE;
        if (e instanceof IOException)
        {
          connection.setDisconnectInfo(DisconnectType.IO_ERROR, null, e);
          message = ERR_READER_CLOSING_DUE_TO_IO_EXCEPTION.get(
               connection.getHostPort(), StaticUtils.getExceptionMessage(e));
          debugLevel = Level.WARNING;
        }
        else if (e instanceof ASN1Exception)
        {
          connection.setDisconnectInfo(DisconnectType.DECODE_ERROR, null, e);
          message = ERR_READER_CLOSING_DUE_TO_ASN1_EXCEPTION.get(
               connection.getHostPort(), StaticUtils.getExceptionMessage(e));
        }
        else
        {
          connection.setDisconnectInfo(DisconnectType.LOCAL_ERROR, null, e);
          message = ERR_READER_CLOSING_DUE_TO_EXCEPTION.get(
               connection.getHostPort(), StaticUtils.getExceptionMessage(e));
        }

        Debug.debug(debugLevel, DebugType.LDAP, message, e);

        // If the connection is configured to try to auto-reconnect, then set
        // things up to do that.  Otherwise, terminate the connection.
        @SuppressWarnings("deprecation")
        final boolean autoReconnect =
             connection.getConnectionOptions().autoReconnect();
        if (autoReconnect)
        {
          reconnect = true;
          break;
        }
        else
        {
          closeRequested = true;
          if (thread != null)
          {
            thread.setName(thread.getName() + " (closed)");
            thread = null;
          }
          closeInternal(true, message);
          return;
        }
      }
    }

    if (thread != null)
    {
      thread.setName(constructThreadName(null));
      thread = null;
    }

    if (reconnect && (! connection.closeRequested()))
    {
      try
      {
        connection.setNeedsReconnect();
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }
    }
    else
    {
      // Ensure that the connection has properly been closed.
      closeInternal(true, null);
    }
  }



  /**
   * Reads a response from the server, blocking if necessary until the response
   * has been received.  This should only be used for connections operating in
   * synchronous mode.
   *
   * @param  messageID  The message ID for the response to be read.  Any
   *                    response read with a different message ID will be
   *                    discarded, unless it is an unsolicited notification in
   *                    which case it will be provided to any registered
   *                    unsolicited notification handler.
   *
   * @return  The response read from the server.
   *
   * @throws  LDAPException  If a problem occurs while reading the response.
   */
  @NotNull()
  @SuppressWarnings("deprecation")
  LDAPResponse readResponse(final int messageID)
               throws LDAPException
  {
    while (true)
    {
      try
      {
        final LDAPResponse response = LDAPMessage.readLDAPResponseFrom(
             asn1StreamReader, false, connection.getCachedSchema());
        if (response == null)
        {
          return new ConnectionClosedResponse(ResultCode.SERVER_DOWN, null);
        }

        connection.setLastCommunicationTime();
        if (response.getMessageID() == messageID)
        {
          return response;
        }

        if ((response instanceof ExtendedResult) &&
            (response.getMessageID() == 0))
        {
          // This is an intermediate response message, so handle it
          // appropriately.
          ExtendedResult extendedResult = (ExtendedResult) response;

          final String oid = extendedResult.getOID();
          if (NoticeOfDisconnectionExtendedResult.
                   NOTICE_OF_DISCONNECTION_RESULT_OID.equals(oid))
          {
            extendedResult = new NoticeOfDisconnectionExtendedResult(
                                      extendedResult);
            connection.setDisconnectInfo(
                 DisconnectType.SERVER_CLOSED_WITH_NOTICE,
                 extendedResult.getDiagnosticMessage(), null);
          }
          else if (com.unboundid.ldap.sdk.unboundidds.extensions.
               InteractiveTransactionAbortedExtendedResult.
                    INTERACTIVE_TRANSACTION_ABORTED_RESULT_OID.equals(oid))
          {
            extendedResult = new com.unboundid.ldap.sdk.unboundidds.extensions.
                 InteractiveTransactionAbortedExtendedResult(extendedResult);
          }

          final UnsolicitedNotificationHandler handler =
               connection.getConnectionOptions().
                    getUnsolicitedNotificationHandler();
          if (handler == null)
          {
            if (Debug.debugEnabled(DebugType.LDAP))
            {
              Debug.debug(Level.WARNING, DebugType.LDAP,
                   WARN_READER_UNHANDLED_UNSOLICITED_NOTIFICATION.get(
                        response));
            }
          }
          else
          {
            handler.handleUnsolicitedNotification(connection,
                                                  extendedResult);
          }
          continue;
        }

        if (Debug.debugEnabled(DebugType.LDAP))
        {
          Debug.debug(Level.WARNING, DebugType.LDAP,
               WARN_READER_DISCARDING_UNEXPECTED_RESPONSE.get(response,
                    messageID));
        }
      }
      catch (final LDAPException le)
      {
        // If the cause was a SocketTimeoutException, then we shouldn't
        // terminate the connection, but we should propagate the failure to
        // the client with the appropriate result.
        final Throwable t = le.getCause();
        if ((t != null) && (t instanceof SocketTimeoutException))
        {
          Debug.debugException(Level.FINEST, le);
          throw new LDAPException(ResultCode.TIMEOUT, le.getMessage(), le);
        }
        else
        {
          Debug.debugException(le);
        }


        // We should terminate the connection regardless of the type of
        // exception, but might want to customize the debug message.
        final String message;
        Level debugLevel = Level.SEVERE;

        if (t == null)
        {
          connection.setDisconnectInfo(DisconnectType.DECODE_ERROR,
               le.getMessage(), t);
          message = le.getMessage();
          debugLevel = Level.WARNING;
        }
        else if (t instanceof IOException)
        {
          connection.setDisconnectInfo(DisconnectType.IO_ERROR,
               le.getMessage(), t);
          message = ERR_READER_CLOSING_DUE_TO_IO_EXCEPTION.get(
               connection.getHostPort(), StaticUtils.getExceptionMessage(t));
          debugLevel = Level.WARNING;
        }
        else if (t instanceof ASN1Exception)
        {
          connection.setDisconnectInfo(DisconnectType.DECODE_ERROR,
               le.getMessage(), t);
          message = ERR_READER_CLOSING_DUE_TO_ASN1_EXCEPTION.get(
               connection.getHostPort(), StaticUtils.getExceptionMessage(t));
        }
        else
        {
          connection.setDisconnectInfo(DisconnectType.LOCAL_ERROR,
               le.getMessage(), t);
          message = ERR_READER_CLOSING_DUE_TO_EXCEPTION.get(
               connection.getHostPort(), StaticUtils.getExceptionMessage(t));
        }

        Debug.debug(debugLevel, DebugType.LDAP, message, t);
        @SuppressWarnings("deprecation")
        final boolean autoReconnect =
             connection.getConnectionOptions().autoReconnect();
        if (! autoReconnect)
        {
          closeRequested = true;
        }
        closeInternal(true, message);
        throw le;
      }
      catch (final Exception e)
      {
        Debug.debugException(e);

        // We should terminate the connection regardless of the type of
        // exception, but might want to customize the debug message.
        final String message;
        Level debugLevel = Level.SEVERE;
        if (e instanceof IOException)
        {
          connection.setDisconnectInfo(DisconnectType.IO_ERROR, null, e);
          message = ERR_READER_CLOSING_DUE_TO_IO_EXCEPTION.get(
               connection.getHostPort(), StaticUtils.getExceptionMessage(e));
          debugLevel = Level.WARNING;
        }
        else if (e instanceof ASN1Exception)
        {
          connection.setDisconnectInfo(DisconnectType.DECODE_ERROR, null, e);
          message = ERR_READER_CLOSING_DUE_TO_ASN1_EXCEPTION.get(
               connection.getHostPort(), StaticUtils.getExceptionMessage(e));
        }
        else
        {
          connection.setDisconnectInfo(DisconnectType.LOCAL_ERROR, null, e);
          message = ERR_READER_CLOSING_DUE_TO_EXCEPTION.get(
               connection.getHostPort(), StaticUtils.getExceptionMessage(e));
        }

        Debug.debug(debugLevel, DebugType.LDAP, message, e);
        @SuppressWarnings("deprecation")
        final boolean autoReconnect =
             connection.getConnectionOptions().autoReconnect();
        if (! autoReconnect)
        {
          closeRequested = true;
        }
        closeInternal(true, message);
        throw new LDAPException(ResultCode.SERVER_DOWN,  message, e);
      }
    }
  }



  /**
   * Logs the provided response, if appropriate.
   *
   * @param  response  The response to be logged.  It must not be {@code null}.
   */
  void logResponse(@NotNull final LDAPResponse response)
  {
    final LDAPConnectionLogger logger =
         connection.getConnectionOptions().getConnectionLogger();
    if (logger == null)
    {
      return;
    }

    final int messageID = response.getMessageID();

    if (response instanceof BindResult)
    {
      logger.logBindResult(connection, messageID, (BindResult) response);
    }
    else if (response instanceof ExtendedResult)
    {
      logger.logExtendedResult(connection, messageID,
           (ExtendedResult) response);
    }
    else if (response instanceof SearchResult)
    {
      logger.logSearchResult(connection, messageID, (SearchResult) response);
    }
    else if (response instanceof LDAPResult)
    {
      final LDAPResult ldapResult = (LDAPResult) response;
      final OperationType operationType = ldapResult.getOperationType();
      if (operationType != null)
      {
        switch (operationType)
        {
          case ADD:
            logger.logAddResult(connection, messageID, ldapResult);
            break;
          case COMPARE:
            logger.logCompareResult(connection, messageID, ldapResult);
            break;
          case DELETE:
            logger.logDeleteResult(connection, messageID, ldapResult);
            break;
          case MODIFY:
            logger.logModifyResult(connection, messageID, ldapResult);
            break;
          case MODIFY_DN:
            logger.logModifyDNResult(connection, messageID, ldapResult);
            break;
        }
      }
    }
    else if (response instanceof SearchResultEntry)
    {
      logger.logSearchEntry(connection, messageID,
           (SearchResultEntry) response);
    }
    else if (response instanceof SearchResultReference)
    {
      logger.logSearchReference(connection, messageID,
           (SearchResultReference) response);
    }
    else if (response instanceof IntermediateResponse)
    {
      logger.logIntermediateResponse(connection, messageID,
           (IntermediateResponse) response);
    }
  }



  /**
   * Converts this clear-text connection to one that uses TLS.
   *
   * @param  sslSocketFactory  The SSL socket factory to use to convert an
   *                           insecure connection into a secure connection.  It
   *                           must not be {@code null}.
   *
   * @return  The TLS-enabled output stream that may be used to send encrypted
   *          requests to the server.
   *
   * @throws  LDAPException  If a problem occurs while attempting to convert the
   *                         connection to use TLS security.
   */
  @NotNull()
  OutputStream doStartTLS(@NotNull final SSLSocketFactory sslSocketFactory)
       throws LDAPException
  {
    final LDAPConnectionOptions connectionOptions =
         connection.getConnectionOptions();
    if (connection.synchronousMode())
    {
      try
      {
        final int connectTimeout = connectionOptions.getConnectTimeoutMillis();
        if (connectTimeout > 0)
        {
          InternalSDKHelper.setSoTimeout(connection, connectTimeout);
        }
        else
        {
          InternalSDKHelper.setSoTimeout(connection, 0);
        }

        final SSLSocket sslSocket;
        synchronized (sslSocketFactory)
        {
          sslSocket = (SSLSocket) sslSocketFactory.createSocket(socket,
               connection.getConnectedAddress(), socket.getPort(), true);
          sslSocket.startHandshake();
        }
        connectionOptions.getSSLSocketVerifier().verifySSLSocket(
             connection.getConnectedAddress(), socket.getPort(), sslSocket);
        inputStream =
             new BufferedInputStream(sslSocket.getInputStream(),
                                     DEFAULT_INPUT_BUFFER_SIZE);
        asn1StreamReader = new ASN1StreamReader(inputStream,
             connectionOptions.getMaxMessageSize());
        startTLSOutputStream = sslSocket.getOutputStream();
        socket = sslSocket;
        connection.getConnectionInternals(true).setSocket(sslSocket);
        final OutputStream outputStream = startTLSOutputStream;
        startTLSOutputStream = null;
        return outputStream;
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
        connection.setDisconnectInfo(DisconnectType.SECURITY_PROBLEM,
             StaticUtils.getExceptionMessage(e), e);
        startTLSException = e;
        closeRequested = true;
        closeInternal(true, StaticUtils.getExceptionMessage(e));
        throw new LDAPException(ResultCode.SERVER_DOWN,
             ERR_CONNREADER_STARTTLS_FAILED.get(
                  StaticUtils.getExceptionMessage(e)),
             e);
      }
    }
    else
    {
      this.sslSocketFactory = sslSocketFactory;

      // Since the connection isn't operating in synchronous mode, we'll want to
      // use a relatively small SO_TIMEOUT for the connection during this
      // process so that it'll be more responsive.  The original SO_TIMEOUT will
      // be restored after the TLS negotiation.
      final int originalSOTimeout = InternalSDKHelper.getSoTimeout(connection);
      boolean revertSOTimeout = false;
      try
      {
        InternalSDKHelper.setSoTimeout(connection, 50);
        revertSOTimeout = true;

        while (true)
        {
          if (startTLSOutputStream != null)
          {
            final OutputStream outputStream = startTLSOutputStream;
            startTLSOutputStream = null;
            return outputStream;
          }
          else if (thread == null)
          {
            if (startTLSException == null)
            {
              revertSOTimeout = false;
              throw new LDAPException(ResultCode.LOCAL_ERROR,
                   ERR_CONNREADER_STARTTLS_FAILED_NO_EXCEPTION.get());
            }
            else
            {
              final Exception e = startTLSException;
              startTLSException = null;
              revertSOTimeout = false;

              throw new LDAPException(ResultCode.LOCAL_ERROR,
                   ERR_CONNREADER_STARTTLS_FAILED.get(
                        StaticUtils.getExceptionMessage(e)),
                   e);
            }
          }

          startTLSSleeper.sleep(1);
        }
      }
      finally
      {
        if (revertSOTimeout)
        {
          InternalSDKHelper.setSoTimeout(connection, originalSOTimeout);
        }
      }
    }
  }



  /**
   * Updates this connection reader to ensure that any subsequent data read
   * over this connection will be decoded using the provided SASL client.
   *
   * @param  saslClient  The SASL client to use to decode data read over this
   *                     connection.
   */
  void applySASLQoP(@NotNull final SaslClient saslClient)
  {
    InternalASN1Helper.setSASLClient(asn1StreamReader, saslClient);
  }



  /**
   * Closes the connection and interrupts the reader thread.
   *
   * @param  notifyConnection  Indicates whether the associated connection
   *                           should be notified.
   */
   void close(final boolean notifyConnection)
   {
     closeRequested = true;

     final int initialJoinAttempts = 5;
     final long initialJoinTimeoutMillis = 100L;
     final boolean initialJoinSuccessful =
          interruptAndJoin(initialJoinAttempts, initialJoinTimeoutMillis);
     closeInternal(notifyConnection, null);

     if (! initialJoinSuccessful)
     {
       final int subsequentJoinAttempts = 60;
       final long subsequentJoinTimeoutMillis = 1_000L;
       if (! interruptAndJoin(subsequentJoinAttempts,
            subsequentJoinTimeoutMillis))
       {
         final long maxAttemptedTimeMillis =
              (initialJoinTimeoutMillis * initialJoinTimeoutMillis) +
              (subsequentJoinAttempts * subsequentJoinTimeoutMillis);
         Debug.debug(Level.WARNING, DebugType.CONNECT,
              "LDAPConnectionReader.close failed to join the reader thread " +
                   "after " + maxAttemptedTimeMillis + " millisecoonds.");
       }
     }
   }



   /**
    * Interrupts the reader thread and attempts to join it.
    *
    * @param  maxJoinAttempts    The maximum number of join attempts to make.
    * @param  joinTimeoutMillis  The maximum length of time, in milliseconds, to
    *                            wait for the join to complete.
    *
    * @return  {@code true} if the thread was successfully joined or the attempt
    *          was interrupted, or {@code false} if not and the thread is still
    *          running.
    */
   private boolean interruptAndJoin(final int maxJoinAttempts,
                                    final long joinTimeoutMillis)
   {
     for (int i=0; i < maxJoinAttempts; i++)
     {
       try
       {
         final Thread t = thread;
         if ((t == null) || (t == Thread.currentThread()) || (! t.isAlive()))
         {
           return true;
         }

         t.interrupt();
         t.join(joinTimeoutMillis);
       }
       catch (final Exception e)
       {
         Debug.debugException(e);

         if (e instanceof InterruptedException)
         {
           Thread.currentThread().interrupt();
           return true;
         }
       }
     }

     return false;
   }



   /**
    * Performs an internal close without interrupting the read thread.
    *
    * @param  notifyConnection  Indicates whether the associated connection
    *                           should be notified.
    * @param  message           A message with additional information about the
    *                           reason for the closure, if available.
    */
   private void closeInternal(final boolean notifyConnection,
                              @Nullable final String message)
   {
     final InputStream is = inputStream;
     inputStream = null;

     try
     {
       if (is != null)
       {
         is.close();
       }
     }
     catch (final Exception e)
     {
       Debug.debugException(e);
     }

     if (notifyConnection)
     {
       connection.setClosed();
     }

     final Iterator iterator = acceptorMap.keySet().iterator();
     while (iterator.hasNext())
     {
       final int messageID = iterator.next();
       final ResponseAcceptor acceptor = acceptorMap.get(messageID);

       try
       {
         if (message == null)
         {
           final DisconnectType disconnectType = connection.getDisconnectType();
           if (disconnectType == null)
           {
             acceptor.responseReceived(new ConnectionClosedResponse(
                  ResultCode.SERVER_DOWN, null));
           }
           else
           {
             acceptor.responseReceived(new ConnectionClosedResponse(
                  disconnectType.getResultCode(),
                  connection.getDisconnectMessage()));
           }
         }
         else
         {
           acceptor.responseReceived(new ConnectionClosedResponse(
                ResultCode.SERVER_DOWN, message));
         }
       }
       catch (final Exception e)
       {
         Debug.debugException(e);
       }

       iterator.remove();
     }
   }



  /**
   * Retrieves the handle to the thread used to read data from the server.  This
   * must not be used for any purpose other than test validation.
   *
   * @return  The handle to the thread used to read data from the server, or
   *          {@code null} if it is not available.
   */
  @Nullable()
  Thread getReaderThread()
  {
    return thread;
  }



  /**
   * Updates the name of the reader thread (if active) based on the information
   * known about the provided connection.
   */
  void updateThreadName()
  {
    final Thread t = thread;
    if (t != null)
    {
      try
      {
        t.setName(constructThreadName(connection.getConnectionInternals(true)));
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }
    }
  }



  /**
   * Determines the name that should be used for the reader thread based on
   * information about the associated client connection.
   *
   * @param  connectionInternals  The connection internals to use for
   *                              information about the address and port of the
   *                              directory server, or {@code null} if the
   *                              connection is not established.
   *
   * @return  The name that should be used for the reader thread based on
   *          information about the associated client connection.
   */
  @NotNull()
  private String constructThreadName(
       @Nullable final LDAPConnectionInternals connectionInternals)
  {
    final StringBuilder buffer = new StringBuilder();
    buffer.append("Connection reader for connection ");
    buffer.append(connection.getConnectionID());
    buffer.append(' ');

    String name = connection.getConnectionName();
    if (name != null)
    {
      buffer.append('\'');
      buffer.append(name);
      buffer.append("' ");
    }

    name = connection.getConnectionPoolName();
    if (name != null)
    {
      buffer.append("in pool '");
      buffer.append(name);
      buffer.append("' ");
    }

    if (connectionInternals == null)
    {
      buffer.append("(not connected)");
    }
    else
    {
      buffer.append("to ");
      buffer.append(connectionInternals.getHost());
      buffer.append(':');
      buffer.append(connectionInternals.getPort());
    }

    return buffer.toString();
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy