001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2014 ForgeRock AS
026 */
027
028package org.forgerock.opendj.grizzly;
029
030import java.io.IOException;
031import java.net.InetSocketAddress;
032import java.util.concurrent.ExecutionException;
033import java.util.concurrent.TimeUnit;
034import java.util.concurrent.atomic.AtomicBoolean;
035import java.util.concurrent.atomic.AtomicInteger;
036
037import javax.net.ssl.SSLEngine;
038
039import org.forgerock.i18n.slf4j.LocalizedLogger;
040import org.forgerock.opendj.ldap.Connection;
041import org.forgerock.opendj.ldap.LDAPOptions;
042import org.forgerock.opendj.ldap.LdapException;
043import org.forgerock.opendj.ldap.ResultCode;
044import org.forgerock.opendj.ldap.TimeoutChecker;
045import org.forgerock.opendj.ldap.TimeoutEventListener;
046import org.forgerock.opendj.ldap.requests.Requests;
047import org.forgerock.opendj.ldap.requests.StartTLSExtendedRequest;
048import org.forgerock.opendj.ldap.responses.ExtendedResult;
049import org.forgerock.opendj.ldap.spi.LDAPConnectionFactoryImpl;
050import org.forgerock.util.promise.ExceptionHandler;
051import org.forgerock.util.promise.Promise;
052import org.forgerock.util.promise.PromiseImpl;
053import org.forgerock.util.promise.ResultHandler;
054import org.glassfish.grizzly.CompletionHandler;
055import org.glassfish.grizzly.EmptyCompletionHandler;
056import org.glassfish.grizzly.SocketConnectorHandler;
057import org.glassfish.grizzly.filterchain.FilterChain;
058import org.glassfish.grizzly.nio.transport.TCPNIOConnectorHandler;
059import org.glassfish.grizzly.nio.transport.TCPNIOTransport;
060
061import com.forgerock.opendj.util.ReferenceCountedObject;
062
063import static org.forgerock.opendj.grizzly.DefaultTCPNIOTransport.*;
064import static org.forgerock.opendj.grizzly.GrizzlyUtils.*;
065import static org.forgerock.opendj.ldap.LdapException.*;
066import static org.forgerock.opendj.ldap.TimeoutChecker.*;
067
068import static com.forgerock.opendj.grizzly.GrizzlyMessages.*;
069
070/**
071 * LDAP connection factory implementation using Grizzly for transport.
072 */
073public final class GrizzlyLDAPConnectionFactory implements LDAPConnectionFactoryImpl {
074    private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
075
076    /**
077     * Adapts a Grizzly connection completion handler to an LDAP connection promise.
078     */
079    @SuppressWarnings("rawtypes")
080    private final class CompletionHandlerAdapter implements
081            CompletionHandler<org.glassfish.grizzly.Connection>, TimeoutEventListener {
082        private final PromiseImpl<Connection, LdapException> promise;
083        private final long timeoutEndTime;
084
085        private CompletionHandlerAdapter(final PromiseImpl<Connection, LdapException> promise) {
086            this.promise = promise;
087            final long timeoutMS = getTimeout();
088            this.timeoutEndTime = timeoutMS > 0 ? System.currentTimeMillis() + timeoutMS : 0;
089            timeoutChecker.get().addListener(this);
090        }
091
092        @Override
093        public void cancelled() {
094            // Ignore this.
095        }
096
097        @Override
098        public void completed(final org.glassfish.grizzly.Connection result) {
099            // Adapt the connection.
100            final GrizzlyLDAPConnection connection = adaptConnection(result);
101
102            // Plain connection.
103            if (options.getSSLContext() == null) {
104                thenOnResult(connection);
105                return;
106            }
107
108            // Start TLS or install SSL layer asynchronously.
109
110            // Give up immediately if the promise has been cancelled or timed out.
111            if (promise.isDone()) {
112                timeoutChecker.get().removeListener(this);
113                connection.close();
114                return;
115            }
116
117            if (options.useStartTLS()) {
118                // Chain StartTLS extended request.
119                final StartTLSExtendedRequest startTLS =
120                        Requests.newStartTLSExtendedRequest(options.getSSLContext());
121                startTLS.addEnabledCipherSuite(options.getEnabledCipherSuites().toArray(
122                    new String[options.getEnabledCipherSuites().size()]));
123                startTLS.addEnabledProtocol(options.getEnabledProtocols().toArray(
124                    new String[options.getEnabledProtocols().size()]));
125
126                connection.extendedRequestAsync(startTLS).thenOnResult(new ResultHandler<ExtendedResult>() {
127                    @Override
128                    public void handleResult(final ExtendedResult result) {
129                        thenOnResult(connection);
130                    }
131                }).thenOnException(new ExceptionHandler<LdapException>() {
132                    @Override
133                    public void handleException(final LdapException error) {
134                        onException(connection, error);
135                    }
136                });
137            } else {
138                // Install SSL/TLS layer.
139                try {
140                    connection.startTLS(options.getSSLContext(), options.getEnabledProtocols(),
141                        options.getEnabledCipherSuites(), new EmptyCompletionHandler<SSLEngine>() {
142                            @Override
143                            public void completed(final SSLEngine result) {
144                                thenOnResult(connection);
145                            }
146
147                            @Override
148                            public void failed(final Throwable throwable) {
149                                onException(connection, throwable);
150                            }
151                        });
152                } catch (final IOException e) {
153                    onException(connection, e);
154                }
155            }
156        }
157
158        @Override
159        public void failed(final Throwable throwable) {
160            // Adapt and forward.
161            timeoutChecker.get().removeListener(this);
162            promise.handleException(adaptConnectionException(throwable));
163            releaseTransportAndTimeoutChecker();
164        }
165
166        @Override
167        public void updated(final org.glassfish.grizzly.Connection result) {
168            // Ignore this.
169        }
170
171        private GrizzlyLDAPConnection adaptConnection(
172                final org.glassfish.grizzly.Connection<?> connection) {
173            configureConnection(connection, options.isTCPNoDelay(), options.isKeepAlive(), options
174                    .isReuseAddress(), options.getLinger(), logger);
175
176            final GrizzlyLDAPConnection ldapConnection =
177                    new GrizzlyLDAPConnection(connection, GrizzlyLDAPConnectionFactory.this);
178            timeoutChecker.get().addListener(ldapConnection);
179            clientFilter.registerConnection(connection, ldapConnection);
180            return ldapConnection;
181        }
182
183        private LdapException adaptConnectionException(Throwable t) {
184            if (!(t instanceof LdapException) && t instanceof ExecutionException) {
185                t = t.getCause() != null ? t.getCause() : t;
186            }
187
188            if (t instanceof LdapException) {
189                return (LdapException) t;
190            } else {
191                return newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR, t.getMessage(), t);
192            }
193        }
194
195        private void onException(final GrizzlyLDAPConnection connection, final Throwable t) {
196            // Abort connection attempt due to error.
197            timeoutChecker.get().removeListener(this);
198            promise.handleException(adaptConnectionException(t));
199            connection.close();
200        }
201
202        private void thenOnResult(final GrizzlyLDAPConnection connection) {
203            timeoutChecker.get().removeListener(this);
204            if (!promise.tryHandleResult(connection)) {
205                // The connection has been either cancelled or it has timed out.
206                connection.close();
207            }
208        }
209
210        @Override
211        public long handleTimeout(final long currentTime) {
212            if (timeoutEndTime == 0) {
213                return 0;
214            } else if (timeoutEndTime > currentTime) {
215                return timeoutEndTime - currentTime;
216            } else {
217                promise.handleException(newLdapException(ResultCode.CLIENT_SIDE_CONNECT_ERROR,
218                        LDAP_CONNECTION_CONNECT_TIMEOUT.get(getSocketAddress(), getTimeout()).toString()));
219                return 0;
220            }
221        }
222
223        @Override
224        public long getTimeout() {
225            return options.getConnectTimeout(TimeUnit.MILLISECONDS);
226        }
227    }
228
229    private final LDAPClientFilter clientFilter;
230    private final FilterChain defaultFilterChain;
231    private final LDAPOptions options;
232    private final String host;
233    private final int port;
234
235    /**
236     * Prevents the transport and timeoutChecker being released when there are
237     * remaining references (this factory or any connections). It is initially
238     * set to 1 because this factory has a reference.
239     */
240    private final AtomicInteger referenceCount = new AtomicInteger(1);
241
242    /**
243     * Indicates whether this factory has been closed or not.
244     */
245    private final AtomicBoolean isClosed = new AtomicBoolean();
246
247    private final ReferenceCountedObject<TCPNIOTransport>.Reference transport;
248    private final ReferenceCountedObject<TimeoutChecker>.Reference timeoutChecker = TIMEOUT_CHECKER
249            .acquire();
250
251    /**
252     * Creates a new LDAP connection factory based on Grizzly which can be used
253     * to create connections to the Directory Server at the provided host and
254     * port address using provided connection options.
255     *
256     * @param host
257     *            The hostname of the Directory Server to connect to.
258     * @param port
259     *            The port number of the Directory Server to connect to.
260     * @param options
261     *            The LDAP connection options to use when creating connections.
262     */
263    public GrizzlyLDAPConnectionFactory(final String host, final int port, final LDAPOptions options) {
264        this(host, port, options, null);
265    }
266
267    /**
268     * Creates a new LDAP connection factory based on Grizzly which can be used
269     * to create connections to the Directory Server at the provided host and
270     * port address using provided connection options and provided TCP
271     * transport.
272     *
273     * @param host
274     *            The hostname of the Directory Server to connect to.
275     * @param port
276     *            The port number of the Directory Server to connect to.
277     * @param options
278     *            The LDAP connection options to use when creating connections.
279     * @param transport
280     *            Grizzly TCP Transport NIO implementation to use for
281     *            connections. If {@code null}, default transport will be used.
282     */
283    public GrizzlyLDAPConnectionFactory(final String host, final int port, final LDAPOptions options,
284                                        TCPNIOTransport transport) {
285        this.transport = DEFAULT_TRANSPORT.acquireIfNull(transport);
286        this.host = host;
287        this.port = port;
288        this.options = new LDAPOptions(options);
289        this.clientFilter = new LDAPClientFilter(this.options.getDecodeOptions(), 0);
290        this.defaultFilterChain =
291                buildFilterChain(this.transport.get().getProcessor(), clientFilter);
292    }
293
294    @Override
295    public void close() {
296        if (isClosed.compareAndSet(false, true)) {
297            releaseTransportAndTimeoutChecker();
298        }
299    }
300
301    @Override
302    public Connection getConnection() throws LdapException {
303        try {
304            return getConnectionAsync().getOrThrow();
305        } catch (final InterruptedException e) {
306            throw newLdapException(ResultCode.CLIENT_SIDE_USER_CANCELLED, e);
307        }
308    }
309
310    @Override
311    public Promise<Connection, LdapException> getConnectionAsync() {
312        acquireTransportAndTimeoutChecker(); // Protect resources.
313        final SocketConnectorHandler connectorHandler =
314                TCPNIOConnectorHandler.builder(transport.get()).processor(defaultFilterChain)
315                        .build();
316        final PromiseImpl<Connection, LdapException> promise = PromiseImpl.create();
317        connectorHandler.connect(getSocketAddress(), new CompletionHandlerAdapter(promise));
318        return promise;
319    }
320
321    @Override
322    public InetSocketAddress getSocketAddress() {
323        return new InetSocketAddress(host, port);
324    }
325
326    @Override
327    public String getHostName() {
328        return host;
329    }
330
331    @Override
332    public int getPort() {
333        return port;
334    }
335
336    @Override
337    public String toString() {
338        return getClass().getSimpleName() + "(" + host + ':' + port + ')';
339    }
340
341    TimeoutChecker getTimeoutChecker() {
342        return timeoutChecker.get();
343    }
344
345    LDAPOptions getLDAPOptions() {
346        return options;
347    }
348
349    void releaseTransportAndTimeoutChecker() {
350        if (referenceCount.decrementAndGet() == 0) {
351            transport.release();
352            timeoutChecker.release();
353        }
354    }
355
356    private void acquireTransportAndTimeoutChecker() {
357        /*
358         * If the factory is not closed then we need to prevent the resources
359         * (transport, timeout checker) from being released while the connection
360         * attempt is in progress.
361         */
362        referenceCount.incrementAndGet();
363        if (isClosed.get()) {
364            releaseTransportAndTimeoutChecker();
365            throw new IllegalStateException("Attempted to get a connection after factory close");
366        }
367    }
368}