/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2010 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. */ package org.forgerock.opendj.grizzly; import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_BIND_OR_START_TLS_CONNECTION_TIMEOUT; import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_BIND_OR_START_TLS_REQUEST_TIMEOUT; import static com.forgerock.opendj.grizzly.GrizzlyMessages.LDAP_CONNECTION_REQUEST_TIMEOUT; import static org.forgerock.opendj.ldap.LDAPConnectionFactory.REQUEST_TIMEOUT; import static org.forgerock.opendj.ldap.LdapException.newLdapException; import static org.forgerock.opendj.ldap.ResultCode.CLIENT_SIDE_LOCAL_ERROR; import static org.forgerock.opendj.ldap.responses.Responses.newResult; import static org.forgerock.opendj.ldap.spi.LdapPromises.*; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.io.LDAPWriter; import org.forgerock.opendj.ldap.ConnectionEventListener; import org.forgerock.opendj.ldap.Connections; import org.forgerock.opendj.ldap.IntermediateResponseHandler; import org.forgerock.opendj.ldap.LdapException; import org.forgerock.opendj.ldap.LdapPromise; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.SSLContextBuilder; import org.forgerock.opendj.ldap.SearchResultHandler; import org.forgerock.opendj.ldap.TimeoutEventListener; import org.forgerock.opendj.ldap.TrustManagers; import org.forgerock.opendj.ldap.requests.AbandonRequest; import org.forgerock.opendj.ldap.requests.AddRequest; import org.forgerock.opendj.ldap.requests.BindClient; import org.forgerock.opendj.ldap.requests.BindRequest; import org.forgerock.opendj.ldap.requests.CompareRequest; import org.forgerock.opendj.ldap.requests.DeleteRequest; import org.forgerock.opendj.ldap.requests.ExtendedRequest; import org.forgerock.opendj.ldap.requests.GenericBindRequest; import org.forgerock.opendj.ldap.requests.ModifyDNRequest; import org.forgerock.opendj.ldap.requests.ModifyRequest; import org.forgerock.opendj.ldap.requests.Requests; import org.forgerock.opendj.ldap.requests.SearchRequest; import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest; import org.forgerock.opendj.ldap.requests.UnbindRequest; import org.forgerock.opendj.ldap.responses.BindResult; import org.forgerock.opendj.ldap.responses.CompareResult; import org.forgerock.opendj.ldap.responses.ExtendedResult; import org.forgerock.opendj.ldap.responses.Responses; import org.forgerock.opendj.ldap.responses.Result; import org.forgerock.opendj.ldap.spi.BindResultLdapPromiseImpl; import org.forgerock.opendj.ldap.spi.ExtendedResultLdapPromiseImpl; import org.forgerock.opendj.ldap.spi.LDAPConnectionImpl; import org.forgerock.opendj.ldap.spi.ResultLdapPromiseImpl; import org.forgerock.opendj.ldap.spi.SearchResultLdapPromiseImpl; import org.forgerock.util.Options; import org.forgerock.util.Reject; import org.forgerock.util.promise.Promise; import org.forgerock.util.promise.PromiseImpl; import org.forgerock.util.time.Duration; import org.glassfish.grizzly.CompletionHandler; import org.glassfish.grizzly.EmptyCompletionHandler; import org.glassfish.grizzly.filterchain.Filter; import org.glassfish.grizzly.filterchain.FilterChain; import org.glassfish.grizzly.ssl.SSLEngineConfigurator; import org.glassfish.grizzly.ssl.SSLFilter; /** LDAP connection implementation. */ final class GrizzlyLDAPConnection implements LDAPConnectionImpl, TimeoutEventListener { static final int LDAP_V3 = 3; /** * A dummy SSL client engine configurator as SSLFilter only needs client * config. This prevents Grizzly from needlessly using JVM defaults which * may be incorrectly configured. */ private static final SSLEngineConfigurator DUMMY_SSL_ENGINE_CONFIGURATOR; static { try { DUMMY_SSL_ENGINE_CONFIGURATOR = new SSLEngineConfigurator(new SSLContextBuilder().setTrustManager( TrustManagers.distrustAll()).getSSLContext()); } catch (GeneralSecurityException e) { // This should never happen. throw new IllegalStateException("Unable to create Dummy SSL Engine Configurator", e); } } private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); private final AtomicBoolean bindOrStartTLSInProgress = new AtomicBoolean(false); private final org.glassfish.grizzly.Connection connection; private final AtomicInteger nextMsgID = new AtomicInteger(1); private final GrizzlyLDAPConnectionFactory factory; private final ConcurrentHashMap> pendingRequests = new ConcurrentHashMap<>(); private final long requestTimeoutMS; private final Object stateLock = new Object(); /** Guarded by stateLock. */ private Result connectionInvalidReason; private boolean failedDueToDisconnect; private boolean isClosed; private boolean isFailed; private List listeners; /** * Create a LDAP Connection with provided Grizzly connection and LDAP * connection factory. * * @param connection * actual connection * @param factory * factory that provides LDAP connections */ GrizzlyLDAPConnection(final org.glassfish.grizzly.Connection connection, final GrizzlyLDAPConnectionFactory factory) { this.connection = connection; this.factory = factory; final Duration requestTimeout = factory.getLDAPOptions().get(REQUEST_TIMEOUT); this.requestTimeoutMS = requestTimeout.isUnlimited() ? 0 : requestTimeout.to(TimeUnit.MILLISECONDS); } @Override public LdapPromise abandonAsync(final AbandonRequest request) { /* * Need to be careful here since both abandonAsync and Promise.cancel can * be called separately by the client application. Therefore * promise.cancel() should abandon the request, and abandonAsync should * cancel the promise. In addition, bind or StartTLS requests cannot be * abandoned. */ try { synchronized (stateLock) { checkConnectionIsValid(); /* * If there is a bind or startTLS in progress then it must be * this request which is being abandoned. The following check * will prevent it from happening. */ checkBindOrStartTLSInProgress(); } } catch (final LdapException e) { return newFailedLdapPromise(e); } // Remove the promise associated with the request to be abandoned. final ResultLdapPromiseImpl pendingRequest = pendingRequests.remove(request.getRequestID()); if (pendingRequest == null) { /* * There has never been a request with the specified message ID or * the response has already been received and handled. We can ignore * this abandon request. */ return newSuccessfulLdapPromise((Void) null); } /* * This will cancel the promise, but will also recursively invoke this * method. Since the pending request has been removed, there is no risk * of an infinite loop. */ pendingRequest.cancel(false); /* * FIXME: there's a potential race condition here if a bind or startTLS * is initiated just after we removed the pending request. */ return sendAbandonRequest(request); } private LdapPromise sendAbandonRequest(final AbandonRequest request) { final LDAPWriter writer = GrizzlyUtils.getWriter(connection.getMemoryManager(), LDAP_V3); try { final int messageID = nextMsgID.getAndIncrement(); writer.writeAbandonRequest(messageID, request); connection.write(writer.getASN1Writer().getBuffer(), null); return newSuccessfulLdapPromise((Void) null, messageID); } catch (final IOException e) { return newFailedLdapPromise(adaptRequestIOException(e)); } finally { GrizzlyUtils.recycleWriter(writer); } } @Override public LdapPromise addAsync(final AddRequest request, final IntermediateResponseHandler intermediateResponseHandler) { final int messageID = nextMsgID.getAndIncrement(); final ResultLdapPromiseImpl promise = newResultLdapPromise(messageID, request, intermediateResponseHandler, this); try { synchronized (stateLock) { checkConnectionIsValid(); checkBindOrStartTLSInProgress(); pendingRequests.put(messageID, promise); } try { final LDAPWriter writer = GrizzlyUtils.getWriter(connection.getMemoryManager(), LDAP_V3); try { writer.writeAddRequest(messageID, request); connection.write(writer.getASN1Writer().getBuffer(), null); } finally { GrizzlyUtils.recycleWriter(writer); } } catch (final IOException e) { pendingRequests.remove(messageID); throw adaptRequestIOException(e); } } catch (final LdapException e) { promise.adaptErrorResult(e.getResult()); } return promise; } @Override public void addConnectionEventListener(final ConnectionEventListener listener) { Reject.ifNull(listener); final boolean notifyClose; final boolean notifyErrorOccurred; synchronized (stateLock) { notifyClose = isClosed; notifyErrorOccurred = isFailed; if (!isClosed) { if (listeners == null) { listeners = new CopyOnWriteArrayList<>(); } listeners.add(listener); } } if (notifyErrorOccurred) { // Use the reason provided in the disconnect notification. listener.handleConnectionError(failedDueToDisconnect, newLdapException(connectionInvalidReason)); } if (notifyClose) { listener.handleConnectionClosed(); } } @Override public LdapPromise bindAsync(final BindRequest request, final IntermediateResponseHandler intermediateResponseHandler) { final int messageID = nextMsgID.getAndIncrement(); final BindClient context; try { context = request.createBindClient(Connections.getHostString(factory.getSocketAddress())); } catch (final LdapException e) { return newFailedLdapPromise(e, messageID); } final BindResultLdapPromiseImpl promise = newBindLdapPromise(messageID, request, context, intermediateResponseHandler); try { synchronized (stateLock) { checkConnectionIsValid(); if (!pendingRequests.isEmpty()) { promise.setResultOrError(Responses.newBindResult(ResultCode.OPERATIONS_ERROR).setDiagnosticMessage( "There are other operations pending on this connection")); return promise; } if (!bindOrStartTLSInProgress.compareAndSet(false, true)) { promise.setResultOrError(Responses.newBindResult(ResultCode.OPERATIONS_ERROR).setDiagnosticMessage( "Bind or Start TLS operation in progress")); return promise; } pendingRequests.put(messageID, promise); } try { final LDAPWriter writer = GrizzlyUtils.getWriter(connection.getMemoryManager(), LDAP_V3); try { // Use the bind client to get the initial request instead of // using the bind request passed to this method. final GenericBindRequest initialRequest = context.nextBindRequest(); writer.writeBindRequest(messageID, 3, initialRequest); connection.write(writer.getASN1Writer().getBuffer(), null); } finally { GrizzlyUtils.recycleWriter(writer); } } catch (final IOException e) { pendingRequests.remove(messageID); bindOrStartTLSInProgress.set(false); throw adaptRequestIOException(e); } } catch (final LdapException e) { promise.adaptErrorResult(e.getResult()); } return promise; } @Override public void close() { close(Requests.newUnbindRequest(), null); } @Override public void close(final UnbindRequest request, final String reason) { // FIXME: I18N need to internationalize this message. Reject.ifNull(request); close(request, false, Responses.newResult(ResultCode.CLIENT_SIDE_USER_CANCELLED) .setDiagnosticMessage(reason != null ? reason : "Connection closed by client")); } @Override public LdapPromise compareAsync(final CompareRequest request, final IntermediateResponseHandler intermediateResponseHandler) { final int messageID = nextMsgID.getAndIncrement(); final ResultLdapPromiseImpl promise = newCompareLdapPromise(messageID, request, intermediateResponseHandler, this); try { synchronized (stateLock) { checkConnectionIsValid(); checkBindOrStartTLSInProgress(); pendingRequests.put(messageID, promise); } try { final LDAPWriter writer = GrizzlyUtils.getWriter(connection.getMemoryManager(), LDAP_V3); try { writer.writeCompareRequest(messageID, request); connection.write(writer.getASN1Writer().getBuffer(), null); } finally { GrizzlyUtils.recycleWriter(writer); } } catch (final IOException e) { pendingRequests.remove(messageID); throw adaptRequestIOException(e); } } catch (final LdapException e) { promise.adaptErrorResult(e.getResult()); } return promise; } @Override public LdapPromise deleteAsync(final DeleteRequest request, final IntermediateResponseHandler intermediateResponseHandler) { final int messageID = nextMsgID.getAndIncrement(); final ResultLdapPromiseImpl promise = newResultLdapPromise(messageID, request, intermediateResponseHandler, this); try { synchronized (stateLock) { checkConnectionIsValid(); checkBindOrStartTLSInProgress(); pendingRequests.put(messageID, promise); } try { final LDAPWriter writer = GrizzlyUtils.getWriter(connection.getMemoryManager(), LDAP_V3); try { writer.writeDeleteRequest(messageID, request); connection.write(writer.getASN1Writer().getBuffer(), null); } finally { GrizzlyUtils.recycleWriter(writer); } } catch (final IOException e) { pendingRequests.remove(messageID); throw adaptRequestIOException(e); } } catch (final LdapException e) { promise.adaptErrorResult(e.getResult()); } return promise; } @Override public LdapPromise extendedRequestAsync(final ExtendedRequest request, final IntermediateResponseHandler intermediateResponseHandler) { final int messageID = nextMsgID.getAndIncrement(); final ExtendedResultLdapPromiseImpl promise = newExtendedLdapPromise(messageID, request, intermediateResponseHandler, this); try { synchronized (stateLock) { checkConnectionIsValid(); if (StartTLSExtendedRequest.OID.equals(request.getOID())) { if (!pendingRequests.isEmpty()) { promise.setResultOrError(request.getResultDecoder().newExtendedErrorResult( ResultCode.OPERATIONS_ERROR, "", "There are pending operations on this connection")); return promise; } else if (isTLSEnabled()) { promise.setResultOrError(request.getResultDecoder().newExtendedErrorResult( ResultCode.OPERATIONS_ERROR, "", "This connection is already TLS enabled")); return promise; } else if (!bindOrStartTLSInProgress.compareAndSet(false, true)) { promise.setResultOrError(request.getResultDecoder().newExtendedErrorResult( ResultCode.OPERATIONS_ERROR, "", "Bind or Start TLS operation in progress")); return promise; } } else { checkBindOrStartTLSInProgress(); } pendingRequests.put(messageID, promise); } try { final LDAPWriter writer = GrizzlyUtils.getWriter(connection.getMemoryManager(), LDAP_V3); try { writer.writeExtendedRequest(messageID, request); connection.write(writer.getASN1Writer().getBuffer(), null); } finally { GrizzlyUtils.recycleWriter(writer); } } catch (final IOException e) { pendingRequests.remove(messageID); bindOrStartTLSInProgress.set(false); throw adaptRequestIOException(e); } } catch (final LdapException e) { promise.adaptErrorResult(e.getResult()); } return promise; } @Override public boolean isClosed() { synchronized (stateLock) { return isClosed; } } @Override public boolean isValid() { synchronized (stateLock) { return isValid0(); } } @Override public LdapPromise modifyAsync(final ModifyRequest request, final IntermediateResponseHandler intermediateResponseHandler) { final int messageID = nextMsgID.getAndIncrement(); final ResultLdapPromiseImpl promise = newResultLdapPromise(messageID, request, intermediateResponseHandler, this); try { synchronized (stateLock) { checkConnectionIsValid(); checkBindOrStartTLSInProgress(); pendingRequests.put(messageID, promise); } try { final LDAPWriter writer = GrizzlyUtils.getWriter(connection.getMemoryManager(), LDAP_V3); try { writer.writeModifyRequest(messageID, request); connection.write(writer.getASN1Writer().getBuffer(), null); } finally { GrizzlyUtils.recycleWriter(writer); } } catch (final IOException e) { pendingRequests.remove(messageID); throw adaptRequestIOException(e); } } catch (final LdapException e) { promise.adaptErrorResult(e.getResult()); } return promise; } @Override public LdapPromise modifyDNAsync(final ModifyDNRequest request, final IntermediateResponseHandler intermediateResponseHandler) { final int messageID = nextMsgID.getAndIncrement(); final ResultLdapPromiseImpl promise = newResultLdapPromise(messageID, request, intermediateResponseHandler, this); try { synchronized (stateLock) { checkConnectionIsValid(); checkBindOrStartTLSInProgress(); pendingRequests.put(messageID, promise); } try { final LDAPWriter writer = GrizzlyUtils.getWriter(connection.getMemoryManager(), LDAP_V3); try { writer.writeModifyDNRequest(messageID, request); connection.write(writer.getASN1Writer().getBuffer(), null); } finally { GrizzlyUtils.recycleWriter(writer); } } catch (final IOException e) { pendingRequests.remove(messageID); throw adaptRequestIOException(e); } } catch (final LdapException e) { promise.adaptErrorResult(e.getResult()); } return promise; } @Override public void removeConnectionEventListener(final ConnectionEventListener listener) { Reject.ifNull(listener); synchronized (stateLock) { if (listeners != null) { listeners.remove(listener); } } } @Override public LdapPromise searchAsync(final SearchRequest request, final IntermediateResponseHandler intermediateResponseHandler, final SearchResultHandler entryHandler) { final int messageID = nextMsgID.getAndIncrement(); final SearchResultLdapPromiseImpl promise = newSearchLdapPromise(messageID, request, entryHandler, intermediateResponseHandler, this); try { synchronized (stateLock) { checkConnectionIsValid(); checkBindOrStartTLSInProgress(); pendingRequests.put(messageID, promise); } try { final LDAPWriter writer = GrizzlyUtils.getWriter(connection.getMemoryManager(), LDAP_V3); try { writer.writeSearchRequest(messageID, request); connection.write(writer.getASN1Writer().getBuffer(), null); } finally { GrizzlyUtils.recycleWriter(writer); } } catch (final IOException e) { pendingRequests.remove(messageID); throw adaptRequestIOException(e); } } catch (final LdapException e) { promise.adaptErrorResult(e.getResult()); } return promise; } @Override public String toString() { return getClass().getSimpleName() + "(" + connection.getLocalAddress() + ',' + connection.getPeerAddress() + ')'; } @Override public long handleTimeout(final long currentTime) { if (requestTimeoutMS <= 0) { return 0; } long delay = requestTimeoutMS; for (final ResultLdapPromiseImpl promise : pendingRequests.values()) { if (promise == null || !promise.checkForTimeout()) { continue; } final long diff = (promise.getTimestamp() + requestTimeoutMS) - currentTime; if (diff > 0) { // Will expire in diff milliseconds. delay = Math.min(delay, diff); } else if (pendingRequests.remove(promise.getRequestID()) == null) { // Result arrived at the same time. continue; } else if (promise.isBindOrStartTLS()) { /* * No other operations can be performed while a bind or StartTLS * request is active, so we cannot time out the request. We * therefore have a choice: either ignore timeouts for these * operations, or enforce them but doing so requires * invalidating the connection. We'll do the latter, since * ignoring timeouts could cause the application to hang. */ logger.debug(LocalizableMessage.raw("Failing bind or StartTLS request due to timeout %s" + "(connection will be invalidated): ", promise)); final Result result = Responses.newResult(ResultCode.CLIENT_SIDE_TIMEOUT).setDiagnosticMessage( LDAP_CONNECTION_BIND_OR_START_TLS_REQUEST_TIMEOUT.get(requestTimeoutMS).toString()); promise.adaptErrorResult(result); // Fail the connection. final Result errorResult = Responses.newResult(ResultCode.CLIENT_SIDE_TIMEOUT).setDiagnosticMessage( LDAP_CONNECTION_BIND_OR_START_TLS_CONNECTION_TIMEOUT.get(requestTimeoutMS).toString()); connectionErrorOccurred(errorResult); } else { logger.debug(LocalizableMessage.raw("Failing request due to timeout: %s", promise)); final Result result = Responses.newResult(ResultCode.CLIENT_SIDE_TIMEOUT).setDiagnosticMessage( LDAP_CONNECTION_REQUEST_TIMEOUT.get(requestTimeoutMS).toString()); promise.adaptErrorResult(result); /* * FIXME: there's a potential race condition here if a bind or * startTLS is initiated just after we check the boolean. It * seems potentially even more dangerous to send the abandon * request while holding the state lock, since a blocking write * could hang the application. */ // if (!bindOrStartTLSInProgress.get()) { // sendAbandonRequest(newAbandonRequest(promise.getRequestID())); // } } } return delay; } @Override public long getTimeout() { return requestTimeoutMS; } /** * Closes this connection, invoking event listeners as needed. * * @param unbindRequest * The client provided unbind request if this is a client * initiated close, or {@code null} if the connection has failed. * @param isDisconnectNotification * {@code true} if this is a connection failure signalled by a * server disconnect notification. * @param reason * The result indicating why the connection was closed. */ void close(final UnbindRequest unbindRequest, final boolean isDisconnectNotification, final Result reason) { final boolean notifyClose; final boolean notifyErrorOccurred; final List tmpListeners; synchronized (stateLock) { if (isClosed) { // Already closed locally. return; } else if (unbindRequest != null) { // Local close. notifyClose = true; notifyErrorOccurred = false; isClosed = true; tmpListeners = listeners; listeners = null; // Prevent future invocations. if (connectionInvalidReason == null) { connectionInvalidReason = reason; } } else if (isFailed) { // Already failed. return; } else { // Connection has failed and this is the first indication. notifyClose = false; notifyErrorOccurred = true; isFailed = true; failedDueToDisconnect = isDisconnectNotification; connectionInvalidReason = reason; tmpListeners = listeners; // Keep list for client close. } } // First abort all outstanding requests. for (final int requestID : pendingRequests.keySet()) { final ResultLdapPromiseImpl promise = pendingRequests.remove(requestID); if (promise != null) { promise.adaptErrorResult(connectionInvalidReason); } } /* * If this is the final client initiated close then release close the * connection and release resources. */ if (notifyClose) { final LDAPWriter writer = GrizzlyUtils.getWriter(connection.getMemoryManager(), LDAP_V3); try { writer.writeUnbindRequest(nextMsgID.getAndIncrement(), unbindRequest); connection.write(writer.getASN1Writer().getBuffer(), null); } catch (final Exception ignore) { /* * Underlying channel probably blown up. Ignore all errors, * including possibly runtime exceptions (see OPENDJ-672). */ } finally { GrizzlyUtils.recycleWriter(writer); } factory.getTimeoutChecker().removeListener(this); connection.closeSilently(); factory.releaseTransportAndTimeoutChecker(); } // Notify listeners. if (tmpListeners != null) { if (notifyErrorOccurred) { for (final ConnectionEventListener listener : tmpListeners) { // Use the reason provided in the disconnect notification. listener.handleConnectionError(isDisconnectNotification, newLdapException(reason)); } } if (notifyClose) { for (final ConnectionEventListener listener : tmpListeners) { listener.handleConnectionClosed(); } } } } int continuePendingBindRequest(final BindResultLdapPromiseImpl promise) throws LdapException { final int newMsgID = nextMsgID.getAndIncrement(); synchronized (stateLock) { checkConnectionIsValid(); pendingRequests.put(newMsgID, promise); } return newMsgID; } Options getLDAPOptions() { return factory.getLDAPOptions(); } ResultLdapPromiseImpl getPendingRequest(final Integer messageID) { return pendingRequests.get(messageID); } void handleUnsolicitedNotification(final ExtendedResult result) { final List tmpListeners; synchronized (stateLock) { tmpListeners = listeners; } if (tmpListeners != null) { for (final ConnectionEventListener listener : tmpListeners) { listener.handleUnsolicitedNotification(result); } } } /** * Installs a new Grizzly filter (e.g. SSL/SASL) beneath the top-level LDAP * filter. * * @param filter * The filter to be installed. */ void installFilter(final Filter filter) { synchronized (stateLock) { GrizzlyUtils.addFilterToConnection(filter, connection); } } /** * Indicates whether TLS is enabled on this connection. * * @return {@code true} if TLS is enabled on this connection, otherwise * {@code false}. */ boolean isTLSEnabled() { synchronized (stateLock) { final FilterChain currentFilterChain = (FilterChain) connection.getProcessor(); for (final Filter filter : currentFilterChain) { if (filter instanceof SSLFilter) { return true; } } return false; } } ResultLdapPromiseImpl removePendingRequest(final Integer messageID) { return pendingRequests.remove(messageID); } void setBindOrStartTLSInProgress(final boolean state) { bindOrStartTLSInProgress.set(state); } @Override public Promise enableTLS( final SSLContext sslContext, final List sslEnabledProtocols, final List sslEnabledCipherSuites) { final PromiseImpl promise = PromiseImpl.create(); final EmptyCompletionHandler completionHandler = new EmptyCompletionHandler() { @Override public void completed(final SSLEngine result) { promise.handleResult(null); } @Override public void failed(final Throwable throwable) { final Result errorResult = newResult(CLIENT_SIDE_LOCAL_ERROR) .setCause(throwable).setDiagnosticMessage("SSL handshake failed"); connectionErrorOccurred(errorResult); promise.handleException(newLdapException(errorResult)); } }; try { startTLS(sslContext, sslEnabledProtocols, sslEnabledCipherSuites, completionHandler); } catch (final IOException e) { completionHandler.failed(e); } return promise; } void startTLS(final SSLContext sslContext, final List protocols, final List cipherSuites, final CompletionHandler completionHandler) throws IOException { synchronized (stateLock) { if (isTLSEnabled()) { throw new IllegalStateException("TLS already enabled"); } final SSLEngineConfigurator sslEngineConfigurator = new SSLEngineConfigurator(sslContext, true, false, false); sslEngineConfigurator.setEnabledProtocols(protocols.isEmpty() ? null : protocols .toArray(new String[protocols.size()])); sslEngineConfigurator.setProtocolConfigured(true); sslEngineConfigurator.setEnabledCipherSuites(cipherSuites.isEmpty() ? null : cipherSuites .toArray(new String[cipherSuites.size()])); sslEngineConfigurator.setCipherConfigured(true); final SSLFilter sslFilter = new SSLFilter(DUMMY_SSL_ENGINE_CONFIGURATOR, sslEngineConfigurator); installFilter(sslFilter); sslFilter.handshake(connection, completionHandler); } } private LdapException adaptRequestIOException(final IOException e) { // FIXME: what other sort of IOExceptions can be thrown? // FIXME: Is this the best result code? final Result errorResult = Responses.newResult(ResultCode.CLIENT_SIDE_ENCODING_ERROR).setCause(e); connectionErrorOccurred(errorResult); return newLdapException(errorResult); } private void checkBindOrStartTLSInProgress() throws LdapException { if (bindOrStartTLSInProgress.get()) { throw newLdapException(ResultCode.OPERATIONS_ERROR, "Bind or Start TLS operation in progress"); } } private void checkConnectionIsValid() throws LdapException { if (!isValid0()) { if (failedDueToDisconnect) { /* * Connection termination was triggered remotely. We don't want * to blindly pass on the result code to requests since it could * be confused for a genuine response. For example, if the * disconnect contained the invalidCredentials result code then * this could be misinterpreted as a genuine authentication * failure for subsequent bind requests. */ throw newLdapException(ResultCode.CLIENT_SIDE_SERVER_DOWN, "Connection closed by server"); } else { throw newLdapException(connectionInvalidReason); } } } private void connectionErrorOccurred(final Result reason) { close(null, false, reason); } private boolean isValid0() { return !isFailed && !isClosed; } }