com.unboundid.ldap.sdk.LDAPConnectionReader Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of unboundid-ldapsdk Show documentation
Show all versions of unboundid-ldapsdk Show documentation
The UnboundID LDAP SDK for Java is a fast, comprehensive, and easy-to-use
Java API for communicating with LDAP directory servers and performing
related tasks like reading and writing LDIF, encoding and decoding data
using base64 and ASN.1 BER, and performing secure communication. This
package contains the Standard Edition of the LDAP SDK, which is a
complete, general-purpose library for communicating with LDAPv3 directory
servers.
/*
* Copyright 2007-2023 Ping Identity Corporation
* All Rights Reserved.
*/
/*
* Copyright 2007-2023 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-2023 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();
}
}