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}