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

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

/*
 * Copyright 2009-2020 Ping Identity Corporation
 * All Rights Reserved.
 */
/*
 * Copyright 2009-2020 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) 2009-2020 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.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.logging.Level;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import javax.security.sasl.SaslClient;

import com.unboundid.asn1.ASN1Buffer;
import com.unboundid.ldap.protocol.LDAPMessage;
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 static com.unboundid.ldap.sdk.LDAPMessages.*;



/**
 * This class is used to hold references to the elements involved in network
 * communication for an LDAP connection.
 */
@InternalUseOnly()
final class LDAPConnectionInternals
{
  /**
   * A count of the number of active connections.
   */
  @NotNull private static final AtomicLong ACTIVE_CONNECTION_COUNT =
       new AtomicLong(0L);



  /**
   * A set of thread-local ASN.1 buffers used to prepare messages to be written.
   */
  @NotNull private static final AtomicReference>
       ASN1_BUFFERS = new AtomicReference<>(new ThreadLocal());



  // The counter that will be used to obtain the next message ID to use when
  // sending requests to the server.
  @NotNull private final AtomicInteger nextMessageID;

  // Indicates whether to operate in synchronous mode.
  private final boolean synchronousMode;

  // The inet address to which the connection is established.
  @NotNull private final InetAddress inetAddress;

  // The port of the server to which the connection is established.
  private final int port;

  // The time that this connection was established.
  private final long connectTime;

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

  // The LDAP connection reader with which this connection internals is
  // associated.
  @Nullable private final LDAPConnectionReader connectionReader;

  // The output stream used to send requests to the server.
  @Nullable private volatile OutputStream outputStream;

  // The SASL client used to provide communication security via QoP.
  @Nullable private volatile SaslClient saslClient;

  // The socket used to communicate with the directory server.
  @Nullable private volatile Socket socket;

  // The address of the server to which the connection is established.
  @NotNull private final String host;

  // The write timeout handler for this connection.
  @NotNull private final WriteTimeoutHandler writeTimeoutHandler;



  /**
   * Creates a new instance of this object.
   *
   * @param  connection     The LDAP connection created with this connection
   *                        internals object.
   * @param  options        The set of options for the connection.
   * @param  socketFactory  The socket factory to use to create the socket.
   * @param  host           The address of the server to which the connection
   *                        should be established.
   * @param  inetAddress    The inet address to which the connection is
   *                        established.
   * @param  port           The port of the server to which the connection
   *                        should be established.
   * @param  timeout        The maximum length of time in milliseconds to wait
   *                        for the connection to be established before failing,
   *                        or zero to indicate that no timeout should be
   *                        enforced (although if the attempt stalls long
   *                        enough, then the underlying operating system may
   *                        cause it to timeout).
   *
   * @throws  IOException  If a problem occurs while establishing the
   *                       connection.
   */
  LDAPConnectionInternals(@NotNull final LDAPConnection connection,
                          @NotNull final LDAPConnectionOptions options,
                          @NotNull final SocketFactory socketFactory,
                          @NotNull final String host,
                          @NotNull final InetAddress inetAddress,
                          final int port, final int timeout)
       throws IOException

  {
    this.connection  = connection;
    this.host        = host;
    this.inetAddress = inetAddress;
    this.port        = port;

    if (options.captureConnectStackTrace())
    {
      connection.setConnectStackTrace(Thread.currentThread().getStackTrace());
    }

    connectTime     = System.currentTimeMillis();
    nextMessageID   = new AtomicInteger(0);
    synchronousMode = options.useSynchronousMode();
    saslClient      = null;
    socket          = null;

    writeTimeoutHandler = new WriteTimeoutHandler(connection);

    try
    {
      final ConnectThread connectThread =
           new ConnectThread(socketFactory, inetAddress, port, timeout);
      connectThread.start();
      socket = connectThread.getConnectedSocket();

      if (socket instanceof SSLSocket)
      {
        final SSLSocket sslSocket = (SSLSocket) socket;
        options.getSSLSocketVerifier().verifySSLSocket(host, port, sslSocket);
      }
    }
    catch (final LDAPException le)
    {
      Debug.debugException(le);

      if (socket != null)
      {
        socket.close();
      }

      writeTimeoutHandler.destroy();

      throw new IOException(le);
    }

    try
    {
      Debug.debugConnect(host, port, connection);

      final LDAPConnectionLogger logger = options.getConnectionLogger();
      if (logger != null)
      {
        logger.logConnect(connection, host, inetAddress, port);
      }

      if (options.getReceiveBufferSize() > 0)
      {
        socket.setReceiveBufferSize(options.getReceiveBufferSize());
      }

      if (options.getSendBufferSize() > 0)
      {
        socket.setSendBufferSize(options.getSendBufferSize());
      }

      socket.setKeepAlive(options.useKeepAlive());
      socket.setReuseAddress(options.useReuseAddress());
      socket.setSoLinger(options.useLinger(),
                         options.getLingerTimeoutSeconds());
      socket.setTcpNoDelay(options.useTCPNoDelay());

      final int soTimeout =
           Math.max(0, (int) options.getResponseTimeoutMillis());
      Debug.debug(Level.INFO, DebugType.CONNECT,
           "Setting the SO_TIMEOUT value for connection " + connection +
                " to " + soTimeout + "ms.");
      socket.setSoTimeout(soTimeout);

      outputStream     = new BufferedOutputStream(socket.getOutputStream());
      connectionReader = new LDAPConnectionReader(connection, this);
    }
    catch (final IOException ioe)
    {
      Debug.debugException(ioe);
      try
      {
        socket.close();
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }

      writeTimeoutHandler.destroy();

      throw ioe;
    }

    ACTIVE_CONNECTION_COUNT.incrementAndGet();
  }



  /**
   * Starts the connection reader for this connection internals.  This will
   * have no effect if the connection is operating in synchronous mode.
   */
  void startConnectionReader()
  {
    if (! synchronousMode)
    {
      connectionReader.start();
    }
  }



  /**
   * Retrieves the LDAP connection with which this connection internals object
   * is associated.
   *
   * @return  The LDAP connection with which this connection internals object is
   *          associated.
   */
  @NotNull()
  LDAPConnection getConnection()
  {
    return connection;
  }



  /**
   * Retrieves the LDAP connection reader used to read responses from the
   * server.
   *
   * @return  The LDAP connection reader used to read responses from the server,
   *          or {@code null} if the connection is operating in synchronous mode
   *          and is not using a connection reader.
   */
  @Nullable()
  LDAPConnectionReader getConnectionReader()
  {
    return connectionReader;
  }



  /**
   * Retrieves the inet address to which this connection is established.
   *
   * @return  The inet address to which this connection is established.
   */
  @NotNull()
  InetAddress getInetAddress()
  {
    return inetAddress;
  }



  /**
   * Retrieves the address of the server to which this connection is
   * established.
   *
   * @return  The address of the server to which this connection is established.
   */
  @NotNull()
  String getHost()
  {
    return host;
  }



  /**
   * Retrieves the port of the server to which this connection is established.
   *
   * @return  The port of the server to which this connection is established.
   */
  int getPort()
  {
    return port;
  }



  /**
   * Retrieves the socket used to communicate with the directory server.
   *
   * @return  The socket used to communicate with the directory server.
   */
  @Nullable()
  Socket getSocket()
  {
    return socket;
  }



  /**
   * Replaces the socket used to communicate with the directory server.  This
   * should only be called by the {@code LDAPConnectionReader} class when
   * replacing the socket for StartTLS processing.
   *
   * @param  socket  The socket used to communicate with the directory server.
   */
  void setSocket(@NotNull final Socket socket)
  {
    this.socket = socket;
  }



  /**
   * Retrieves the output stream used to send requests to the server.
   *
   * @return  The output stream used to send requests to the server.
   */
  @Nullable()
  OutputStream getOutputStream()
  {
    return outputStream;
  }



  /**
   * Indicates whether the socket is currently connected.
   *
   * @return  {@code true} if the socket is currently connected, or
   *          {@code false} if not.
   */
  boolean isConnected()
  {
    return ((socket != null) && socket.isConnected());
  }



  /**
   * Indicates whether this connection is operating in synchronous mode.
   *
   * @return  {@code true} if this connection is operating in synchronous mode,
   *          or {@code false} if not.
   */
  boolean synchronousMode()
  {
    return synchronousMode;
  }



  /**
   * Converts this clear-text connection to one that encrypts all communication
   * using Transport Layer Security.  This method is intended for use as a
   * helper for processing in the course of the StartTLS extended operation and
   * should not be used for other purposes.
   *
   * @param  sslSocketFactory  The SSL socket factory to use to convert an
   *                           insecure connection into a secure connection.  It
   *                           must not be {@code null}.
   *
   * @throws  LDAPException  If a problem occurs while converting this
   *                         connection to use TLS.
   */
  void convertToTLS(@NotNull final SSLSocketFactory sslSocketFactory)
       throws LDAPException
  {
    outputStream = connectionReader.doStartTLS(sslSocketFactory);
  }



  /**
   * Converts this clear-text connection to one that uses SASL integrity and/or
   * confidentiality.
   *
   * @param  saslClient  The SASL client that will be used to secure the
   *                     communication.
   *
   * @throws  LDAPException  If a problem occurs while attempting to convert the
   *                         connection to use a SASL security layer.
   */
  void applySASLQoP(@NotNull final SaslClient saslClient)
       throws LDAPException
  {
    this.saslClient = saslClient;
    connectionReader.applySASLQoP(saslClient);
  }



  /**
   * Retrieves the message ID that should be used for the next message to send
   * to the directory server.
   *
   * @return  The message ID that should be used for the next message to send to
   *          the directory server.
   */
  int nextMessageID()
  {
    int msgID = nextMessageID.incrementAndGet();
    if (msgID > 0)
    {
      return msgID;
    }

    while (true)
    {
      if (nextMessageID.compareAndSet(msgID, 1))
      {
        return 1;
      }

      msgID = nextMessageID.incrementAndGet();
      if (msgID > 0)
      {
        return msgID;
      }
    }
  }



  /**
   * Registers the provided response acceptor with the connection reader.
   *
   * @param  messageID         The message ID for which the acceptor is to be
   *                           registered.
   * @param  responseAcceptor  The response acceptor to register.
   *
   * @throws  LDAPException  If another response acceptor is already registered
   *                         with the provided message ID.
   */
  void registerResponseAcceptor(final int messageID,
            @NotNull final ResponseAcceptor responseAcceptor)
       throws LDAPException
  {
    if (! isConnected())
    {
      final LDAPConnectionOptions connectionOptions =
           connection.getConnectionOptions();
      @SuppressWarnings("deprecation")
      final boolean autoReconnect = connectionOptions.autoReconnect();
      final boolean closeRequested = connection.closeRequested();
      if (autoReconnect && (! closeRequested))
      {
        connection.reconnect();
        connection.registerResponseAcceptor(messageID,  responseAcceptor);
      }
      else
      {
        throw new LDAPException(ResultCode.SERVER_DOWN,
                                ERR_CONN_NOT_ESTABLISHED.get());
      }
    }

    connectionReader.registerResponseAcceptor(messageID, responseAcceptor);
  }



  /**
   * Deregisters the response acceptor associated with the provided message ID.
   *
   * @param  messageID  The message ID for which to deregister the associated
   *                    response acceptor.
   */
  void deregisterResponseAcceptor(final int messageID)
  {
    connectionReader.deregisterResponseAcceptor(messageID);
  }



  /**
   * Sends the provided LDAP message to the directory server.
   *
   * @param  message            The LDAP message to be sent.
   * @param  sendTimeoutMillis  The maximum length of time, in milliseconds, to
   *                            block while trying to send the request.  If this
   *                            is less than or equal to zero, then no send
   *                            timeout will be enforced.
   * @param  allowRetry         Indicates whether to allow retrying the send
   *                            after a reconnect.
   *
   * @throws  LDAPException  If a problem occurs while sending the message.
   */
  void sendMessage(@NotNull final LDAPMessage message,
                   final long sendTimeoutMillis, final boolean allowRetry)
       throws LDAPException
  {
    if (! isConnected())
    {
      throw new LDAPException(ResultCode.SERVER_DOWN,
                              ERR_CONN_NOT_ESTABLISHED.get());
    }

    ASN1Buffer buffer = ASN1_BUFFERS.get().get();
    if (buffer == null)
    {
      buffer = new ASN1Buffer();
      ASN1_BUFFERS.get().set(buffer);
    }

    buffer.clear();
    try
    {
      message.writeTo(buffer);
    }
    catch (final LDAPRuntimeException lre)
    {
      Debug.debugException(lre);
      lre.throwLDAPException();
    }


    try
    {
      final int soTimeout = Math.max(0, (int) sendTimeoutMillis);
      if (Debug.debugEnabled())
      {
        Debug.debug(Level.INFO, DebugType.CONNECT,
             "Setting the SO_TIMEOUT value for connection " + connection +
                  " to " + soTimeout + "ms.");
      }
      socket.setSoTimeout(soTimeout);
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
    }


    final Long writeID;
    if (sendTimeoutMillis > 0)
    {
      writeID = writeTimeoutHandler.beginWrite(sendTimeoutMillis);
    }
    else
    {
      writeID = null;
    }

    try
    {
      final OutputStream os = outputStream;
      if (saslClient == null)
      {
        buffer.writeTo(os);
      }
      else
      {
        // We need to wrap the data that was read using the SASL client, but we
        // also need to precede that wrapped data with four bytes that specify
        // the number of bytes of wrapped data.
        final byte[] clearBytes = buffer.toByteArray();
        final byte[] saslBytes =
             saslClient.wrap(clearBytes, 0, clearBytes.length);
        final byte[] lengthBytes = new byte[4];
        lengthBytes[0] = (byte) ((saslBytes.length >> 24) & 0xFF);
        lengthBytes[1] = (byte) ((saslBytes.length >> 16) & 0xFF);
        lengthBytes[2] = (byte) ((saslBytes.length >> 8) & 0xFF);
        lengthBytes[3] = (byte) (saslBytes.length & 0xFF);
        os.write(lengthBytes);
        os.write(saslBytes);
      }
      os.flush();
    }
    catch (final IOException ioe)
    {
      Debug.debugException(ioe);

      // If the message was an unbind request, then we don't care that it
      // didn't get sent.  Otherwise, fail the send attempt but try to reconnect
      // first if appropriate.
      if (message.getProtocolOpType() ==
          LDAPMessage.PROTOCOL_OP_TYPE_UNBIND_REQUEST)
      {
        return;
      }

      final boolean closeRequested = connection.closeRequested();
      if (allowRetry && (! closeRequested) && (! connection.synchronousMode()))
      {
        connection.reconnect();

        try
        {
          sendMessage(message, sendTimeoutMillis, false);
          return;
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
        }
      }

      throw new LDAPException(ResultCode.SERVER_DOWN,
           ERR_CONN_SEND_ERROR.get(host + ':' + port,
                StaticUtils.getExceptionMessage(ioe)),
           ioe);
    }
    catch (final Exception e)
    {
      Debug.debugException(e);
      throw new LDAPException(ResultCode.LOCAL_ERROR,
           ERR_CONN_ENCODE_ERROR.get(host + ':' + port,
                StaticUtils.getExceptionMessage(e)),
           e);
    }
    finally
    {
      if (writeID != null)
      {
        writeTimeoutHandler.writeCompleted(writeID);
      }

      if (buffer.zeroBufferOnClear())
      {
        buffer.clear();
      }
    }
  }



  /**
   * Closes the connection associated with this connection internals.
   */
  void close()
  {
    DisconnectInfo disconnectInfo = connection.getDisconnectInfo();
    if (disconnectInfo == null)
    {
      disconnectInfo = connection.setDisconnectInfo(
           new DisconnectInfo(connection, DisconnectType.UNKNOWN, null, null));
    }

    // Determine if this connection was closed by a finalizer.
    final boolean closedByFinalizer =
         ((disconnectInfo.getType() == DisconnectType.CLOSED_BY_FINALIZER) &&
              (socket != null) && socket.isConnected());

    writeTimeoutHandler.destroy();

    final boolean alreadyClosed;
    if (socket == null)
    {
      alreadyClosed = true;
    }
    else
    {
      alreadyClosed = false;

      try
      {
        try
        {
          outputStream.close();
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
        }

        try
        {
          socket.close();
        }
        catch (final Exception e)
        {
          Debug.debugException(e);
        }
      }
      finally
      {
        outputStream = null;
        socket = null;
      }

      // Make sure that the connection reader is no longer running.
      try
      {
        connectionReader.close(false);
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }
    }

    if (saslClient != null)
    {
      try
      {
        saslClient.dispose();
      }
      catch (final Exception e)
      {
        Debug.debugException(e);
      }
      finally
      {
        saslClient = null;
      }
    }

    if (! alreadyClosed)
    {
      Debug.debugDisconnect(host, port, connection, disconnectInfo.getType(),
           disconnectInfo.getMessage(), disconnectInfo.getCause());

      final LDAPConnectionLogger logger =
           connection.getConnectionOptions().getConnectionLogger();
      if (logger != null)
      {
        logger.logDisconnect(connection, host, port, disconnectInfo.getType(),
             disconnectInfo.getMessage(), disconnectInfo.getCause());
      }
    }

    if (closedByFinalizer && Debug.debugEnabled(DebugType.LDAP))
    {
      Debug.debug(Level.WARNING, DebugType.LDAP,
           "Connection closed by LDAP SDK finalizer:  " + toString());
    }
    disconnectInfo.notifyDisconnectHandler();
    connection.setServerSet(null);

    final long remainingActiveConnections =
         ACTIVE_CONNECTION_COUNT.decrementAndGet();
    if (remainingActiveConnections <= 0L)
    {
      ASN1_BUFFERS.set(new ThreadLocal());

      if (remainingActiveConnections < 0L)
      {
        // This should never happen, but if it does then we'll reset the count
        // to zero so that we don't keep needlessly resetting the buffers.
        ACTIVE_CONNECTION_COUNT.compareAndSet(remainingActiveConnections, 0L);
      }
    }
  }



  /**
   * Retrieves the time that the connection was established.
   *
   * @return  The time that the connection was established, or -1 if the
   *          connection is not established.
   */
  public long getConnectTime()
  {
    if (isConnected())
    {
      return connectTime;
    }
    else
    {
      return -1L;
    }
  }



  /**
   * Retrieves the number of active connections.
   *
   * @return  The number of active connections.
   */
  static long getActiveConnectionCount()
  {
    return ACTIVE_CONNECTION_COUNT.get();
  }



  /**
   * Retrieves a string representation of this connection internals object.
   *
   * @return  A string representation of this connection internals object.
   */
  @Override()
  @NotNull()
  public String toString()
  {
    final StringBuilder buffer = new StringBuilder();
    toString(buffer);
    return buffer.toString();
  }



  /**
   * Appends a string representation of this connection internals object to the
   * provided buffer.
   *
   * @param  buffer  The buffer to which the information should be appended.
   */
  public void toString(@NotNull final StringBuilder buffer)
  {
    buffer.append("LDAPConnectionInternals(host='");
    buffer.append(host);
    buffer.append("', port=");
    buffer.append(port);
    buffer.append(", connected=");
    buffer.append((socket != null) && socket.isConnected());
    buffer.append(", nextMessageID=");
    buffer.append(nextMessageID.get());
    buffer.append(')');
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy