001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2008-2010 Sun Microsystems, Inc. 015 * Portions Copyright 2014-2016 ForgeRock AS. 016 */ 017package org.opends.guitools.controlpanel.browser; 018 019import java.util.ArrayList; 020import java.util.HashMap; 021 022import javax.naming.NamingException; 023import javax.naming.ldap.Control; 024import javax.naming.ldap.InitialLdapContext; 025import javax.net.ssl.KeyManager; 026 027import org.forgerock.opendj.ldap.DN; 028import org.forgerock.opendj.ldap.SearchScope; 029import org.opends.admin.ads.util.ApplicationTrustManager; 030import org.opends.admin.ads.util.ConnectionUtils; 031import org.opends.guitools.controlpanel.event.ReferralAuthenticationListener; 032import org.opends.server.types.HostPort; 033import org.opends.server.types.LDAPURL; 034 035import com.forgerock.opendj.cli.CliConstants; 036 037import static org.opends.admin.ads.util.ConnectionUtils.*; 038 039/** 040 * An LDAPConnectionPool is a pool of LDAPConnection. 041 * <BR><BR> 042 * When a client class needs to access an LDAPUrl, it simply passes 043 * this URL to getConnection() and gets an LDAPConnection back. 044 * When the client has finished with this LDAPConnection, it *must* 045 * pass it releaseConnection() which will take care of its disconnection 046 * or caching. 047 * <BR><BR> 048 * LDAPConnectionPool maintains a pool of authentications. This pool 049 * is populated using registerAuth(). When getConnection() has created 050 * a new connection for accessing a host:port, it looks in the authentication 051 * pool if any authentication is available for this host:port and, if yes, 052 * tries to bind the connection. If no authentication is available, the 053 * returned connection is simply connected (ie anonymous bind). 054 * <BR><BR> 055 * LDAPConnectionPool shares connections and maintains a usage counter 056 * for each connection: two calls to getConnection() with the same URL 057 * will return the same connection. Two calls to releaseConnection() will 058 * be needed to make the connection 'potentially disconnectable'. 059 * <BR><BR> 060 * releaseConnection() does not disconnect systematically a connection 061 * whose usage counter is null. It keeps it connected a while (TODO: 062 * to be implemented). 063 * <BR><BR> 064 * TODO: synchronization is a bit simplistic... 065 */ 066public class LDAPConnectionPool { 067 068 private final HashMap<String, AuthRecord> authTable = new HashMap<>(); 069 private final HashMap<String, ConnectionRecord> connectionTable = new HashMap<>(); 070 071 private ArrayList<ReferralAuthenticationListener> listeners; 072 073 private Control[] requestControls = new Control[] {}; 074 private ApplicationTrustManager trustManager; 075 private int connectTimeout = CliConstants.DEFAULT_LDAP_CONNECT_TIMEOUT; 076 077 /** 078 * Returns <CODE>true</CODE> if the connection passed is registered in the 079 * connection pool, <CODE>false</CODE> otherwise. 080 * @param ctx the connection. 081 * @return <CODE>true</CODE> if the connection passed is registered in the 082 * connection pool, <CODE>false</CODE> otherwise. 083 */ 084 public boolean isConnectionRegistered(InitialLdapContext ctx) { 085 for (String key : connectionTable.keySet()) 086 { 087 ConnectionRecord cr = connectionTable.get(key); 088 HostPort hostPort = getHostPort(ctx); 089 HostPort crHostPort = getHostPort(cr.ctx); 090 if (cr.ctx != null 091 && hostPort.equals(crHostPort) 092 && getBindDN(cr.ctx).equals(getBindDN(ctx)) 093 && getBindPassword(cr.ctx).equals(getBindPassword(ctx)) 094 && isSSL(cr.ctx) == isSSL(ctx) 095 && isStartTLS(cr.ctx) == isStartTLS(ctx)) { 096 return true; 097 } 098 } 099 return false; 100 } 101 102 /** 103 * Registers a connection in this connection pool. 104 * @param ctx the connection to be registered. 105 */ 106 public void registerConnection(InitialLdapContext ctx) { 107 registerAuth(ctx); 108 LDAPURL url = makeLDAPUrl(ctx); 109 String key = makeKeyFromLDAPUrl(url); 110 ConnectionRecord cr = new ConnectionRecord(); 111 cr.ctx = ctx; 112 cr.counter = 1; 113 cr.disconnectAfterUse = false; 114 connectionTable.put(key, cr); 115 } 116 117 /** 118 * Unregisters a connection from this connection pool. 119 * @param ctx the connection to be unregistered. 120 * @throws NamingException if there is a problem unregistering the connection. 121 */ 122 public void unregisterConnection(InitialLdapContext ctx) 123 throws NamingException 124 { 125 LDAPURL url = makeLDAPUrl(ctx); 126 unRegisterAuth(url); 127 String key = makeKeyFromLDAPUrl(url); 128 connectionTable.remove(key); 129 } 130 131 /** 132 * Adds a referral authentication listener. 133 * @param listener the referral authentication listener. 134 */ 135 public void addReferralAuthenticationListener( 136 ReferralAuthenticationListener listener) { 137 if (listeners == null) { 138 listeners = new ArrayList<>(); 139 } 140 listeners.add(listener); 141 } 142 143 /** 144 * Returns an LDAPConnection for accessing the specified url. 145 * If no connection are available for the protocol/host/port 146 * of the URL, getConnection() makes a new one and call connect(). 147 * If authentication data available for this protocol/host/port, 148 * getConnection() call bind() on the new connection. 149 * If connect() or bind() failed, getConnection() forward the 150 * NamingException. 151 * When getConnection() succeeds, the returned connection must 152 * be passed to releaseConnection() after use. 153 * @param ldapUrl the LDAP URL to which the connection must connect. 154 * @return a connection to the provided LDAP URL. 155 * @throws NamingException if there was an error connecting. 156 */ 157 public InitialLdapContext getConnection(LDAPURL ldapUrl) 158 throws NamingException { 159 String key = makeKeyFromLDAPUrl(ldapUrl); 160 ConnectionRecord cr; 161 162 synchronized(this) { 163 cr = connectionTable.get(key); 164 if (cr == null) { 165 cr = new ConnectionRecord(); 166 cr.ctx = null; 167 cr.counter = 1; 168 cr.disconnectAfterUse = false; 169 connectionTable.put(key, cr); 170 } 171 else { 172 cr.counter++; 173 } 174 } 175 176 synchronized(cr) { 177 try { 178 if (cr.ctx == null) { 179 boolean registerAuth = false; 180 AuthRecord authRecord = authTable.get(key); 181 if (authRecord == null) 182 { 183 // Best-effort: try with an already registered authentication 184 authRecord = authTable.values().iterator().next(); 185 registerAuth = true; 186 } 187 cr.ctx = createLDAPConnection(ldapUrl, authRecord); 188 cr.ctx.setRequestControls(requestControls); 189 if (registerAuth) 190 { 191 authTable.put(key, authRecord); 192 } 193 } 194 } 195 catch(NamingException x) { 196 synchronized (this) { 197 cr.counter--; 198 if (cr.counter == 0) { 199 connectionTable.remove(key); 200 } 201 } 202 throw x; 203 } 204 } 205 206 return cr.ctx; 207 } 208 209 /** 210 * Sets the request controls to be used by the connections of this connection 211 * pool. 212 * @param ctls the request controls. 213 * @throws NamingException if an error occurs updating the connections. 214 */ 215 public synchronized void setRequestControls(Control[] ctls) 216 throws NamingException 217 { 218 requestControls = ctls; 219 for (ConnectionRecord cr : connectionTable.values()) 220 { 221 if (cr.ctx != null) 222 { 223 cr.ctx.setRequestControls(requestControls); 224 } 225 } 226 } 227 228 229 /** 230 * Release an LDAPConnection created by getConnection(). 231 * The connection should be considered as virtually disconnected 232 * and not be used anymore. 233 * @param ctx the connection to be released. 234 */ 235 public synchronized void releaseConnection(InitialLdapContext ctx) { 236 237 String targetKey = null; 238 ConnectionRecord targetRecord = null; 239 synchronized(this) { 240 for (String key : connectionTable.keySet()) { 241 ConnectionRecord cr = connectionTable.get(key); 242 if (cr.ctx == ctx) { 243 targetKey = key; 244 targetRecord = cr; 245 if (targetKey != null) 246 { 247 break; 248 } 249 } 250 } 251 } 252 253 if (targetRecord == null) { // ldc is not in _connectionTable -> bug 254 throw new IllegalArgumentException("Invalid LDAP connection"); 255 } 256 257 synchronized (targetRecord) 258 { 259 targetRecord.counter--; 260 if (targetRecord.counter == 0 && targetRecord.disconnectAfterUse) 261 { 262 disconnectAndRemove(targetRecord); 263 } 264 } 265 } 266 267 /** 268 * Register authentication data. 269 * If authentication data are already available for the protocol/host/port 270 * specified in the LDAPURl, they are replaced by the new data. 271 * If true is passed as 'connect' parameter, registerAuth() creates the 272 * connection and attempts to connect() and bind() . If connect() or bind() 273 * fail, registerAuth() forwards the NamingException and does not register 274 * the authentication data. 275 * @param ldapUrl the LDAP URL of the server. 276 * @param dn the bind DN. 277 * @param pw the password. 278 * @param connect whether to connect or not to the server with the 279 * provided authentication (for testing purposes). 280 * @throws NamingException if an error occurs connecting. 281 */ 282 private void registerAuth(LDAPURL ldapUrl, String dn, String pw, 283 boolean connect) throws NamingException { 284 285 String key = makeKeyFromLDAPUrl(ldapUrl); 286 final AuthRecord ar = new AuthRecord(); 287 ar.dn = dn; 288 ar.password = pw; 289 290 if (connect) { 291 InitialLdapContext ctx = createLDAPConnection(ldapUrl, ar); 292 ctx.close(); 293 } 294 295 synchronized(this) { 296 authTable.put(key, ar); 297 ConnectionRecord cr = connectionTable.get(key); 298 if (cr != null) { 299 if (cr.counter <= 0) { 300 disconnectAndRemove(cr); 301 } 302 else { 303 cr.disconnectAfterUse = true; 304 } 305 } 306 } 307 notifyListeners(); 308 309 } 310 311 312 /** 313 * Register authentication data from an existing connection. 314 * This routine recreates the LDAP URL corresponding to 315 * the connection and passes it to registerAuth(LDAPURL). 316 * @param ctx the connection that we retrieve the authentication information 317 * from. 318 */ 319 private void registerAuth(InitialLdapContext ctx) { 320 LDAPURL url = makeLDAPUrl(ctx); 321 try { 322 registerAuth(url, getBindDN(ctx), getBindPassword(ctx), false); 323 } 324 catch (NamingException x) { 325 throw new RuntimeException("Bug"); 326 } 327 } 328 329 330 /** 331 * Unregister authentication data. 332 * If for the given url there's a connection, try to bind as anonymous. 333 * If unbind fails throw NamingException. 334 * @param ldapUrl the url associated with the authentication to be 335 * unregistered. 336 * @throws NamingException if the unbind fails. 337 */ 338 private void unRegisterAuth(LDAPURL ldapUrl) throws NamingException { 339 String key = makeKeyFromLDAPUrl(ldapUrl); 340 341 authTable.remove(key); 342 notifyListeners(); 343 } 344 345 /** 346 * Disconnect the connection associated to a record 347 * and remove the record from connectionTable. 348 * @param cr the ConnectionRecord to remove. 349 */ 350 private void disconnectAndRemove(ConnectionRecord cr) 351 { 352 String key = makeKeyFromRecord(cr); 353 connectionTable.remove(key); 354 try 355 { 356 cr.ctx.close(); 357 } 358 catch (NamingException x) 359 { 360 // Bizarre. However it's not really a problem here. 361 } 362 } 363 364 /** Notifies the listeners that a referral authentication change happened. */ 365 private void notifyListeners() 366 { 367 for (ReferralAuthenticationListener listener : listeners) 368 { 369 listener.notifyAuthDataChanged(); 370 } 371 } 372 373 /** 374 * Make the key string for an LDAP URL. 375 * @param url the LDAP URL. 376 * @return the key to be used in Maps for the provided LDAP URL. 377 */ 378 private static String makeKeyFromLDAPUrl(LDAPURL url) { 379 String protocol = isSecureLDAPUrl(url) ? "LDAPS" : "LDAP"; 380 return protocol + ":" + url.getHost() + ":" + url.getPort(); 381 } 382 383 384 /** 385 * Make the key string for an connection record. 386 * @param rec the connection record. 387 * @return the key to be used in Maps for the provided connection record. 388 */ 389 private static String makeKeyFromRecord(ConnectionRecord rec) { 390 String protocol = ConnectionUtils.isSSL(rec.ctx) ? "LDAPS" : "LDAP"; 391 return protocol + ":" + getHostPort(rec.ctx); 392 } 393 394 /** 395 * Creates an LDAP Connection for a given LDAP URL and using the 396 * authentication of a AuthRecord. 397 * @param ldapUrl the LDAP URL. 398 * @param ar the authentication information. 399 * @return a connection. 400 * @throws NamingException if an error occurs when connecting. 401 */ 402 private InitialLdapContext createLDAPConnection(LDAPURL ldapUrl, 403 AuthRecord ar) throws NamingException 404 { 405 // Take the base DN out of the URL and only keep the protocol, host and port 406 ldapUrl = new LDAPURL(ldapUrl.getScheme(), ldapUrl.getHost(), 407 ldapUrl.getPort(), (DN)null, null, null, null, null); 408 409 if (isSecureLDAPUrl(ldapUrl)) 410 { 411 return ConnectionUtils.createLdapsContext(ldapUrl.toString(), ar.dn, 412 ar.password, getConnectTimeout(), null, 413 getTrustManager(), getKeyManager()); 414 } 415 return ConnectionUtils.createLdapContext(ldapUrl.toString(), ar.dn, 416 ar.password, getConnectTimeout(), null); 417 } 418 419 /** 420 * Sets the ApplicationTrustManager used by the connection pool to 421 * connect to servers. 422 * @param trustManager the ApplicationTrustManager. 423 */ 424 public void setTrustManager(ApplicationTrustManager trustManager) 425 { 426 this.trustManager = trustManager; 427 } 428 429 /** 430 * Returns the ApplicationTrustManager used by the connection pool to 431 * connect to servers. 432 * @return the ApplicationTrustManager used by the connection pool to 433 * connect to servers. 434 */ 435 public ApplicationTrustManager getTrustManager() 436 { 437 return trustManager; 438 } 439 440 /** 441 * Returns the timeout to establish the connection in milliseconds. 442 * @return the timeout to establish the connection in milliseconds. 443 */ 444 public int getConnectTimeout() 445 { 446 return connectTimeout; 447 } 448 449 /** 450 * Sets the timeout to establish the connection in milliseconds. 451 * Use {@code 0} to express no timeout. 452 * @param connectTimeout the timeout to establish the connection in 453 * milliseconds. 454 * Use {@code 0} to express no timeout. 455 */ 456 public void setConnectTimeout(int connectTimeout) 457 { 458 this.connectTimeout = connectTimeout; 459 } 460 461 private KeyManager getKeyManager() 462 { 463// TODO: we should get it from ControlPanelInfo 464 return null; 465 } 466 467 /** 468 * Returns whether the URL is ldaps URL or not. 469 * @param url the URL. 470 * @return <CODE>true</CODE> if the LDAP URL is secure and <CODE>false</CODE> 471 * otherwise. 472 */ 473 private static boolean isSecureLDAPUrl(LDAPURL url) { 474 return !LDAPURL.DEFAULT_SCHEME.equalsIgnoreCase(url.getScheme()); 475 } 476 477 private LDAPURL makeLDAPUrl(InitialLdapContext ctx) { 478 return makeLDAPUrl(ConnectionUtils.getHostPort(ctx), "", isSSL(ctx)); 479 } 480 481 /** 482 * Make an url from the specified arguments. 483 * @param hostPort the host name and port of the server. 484 * @param dn the base DN of the URL. 485 * @param isSSL whether the connection uses SSL 486 * @return an LDAP URL from the specified arguments. 487 */ 488 public static LDAPURL makeLDAPUrl(HostPort hostPort, String dn, boolean isSSL) 489 { 490 return new LDAPURL( 491 isSSL ? "ldaps" : LDAPURL.DEFAULT_SCHEME, 492 hostPort.getHost(), 493 hostPort.getPort(), 494 dn, 495 null, // No attributes 496 SearchScope.BASE_OBJECT, 497 null, // No filter 498 null); // No extensions 499 } 500 501 502 /** 503 * Make an url from the specified arguments. 504 * @param url an LDAP URL to use as base of the new LDAP URL. 505 * @param dn the base DN for the new LDAP URL. 506 * @return an LDAP URL from the specified arguments. 507 */ 508 public static LDAPURL makeLDAPUrl(LDAPURL url, String dn) { 509 return new LDAPURL( 510 url.getScheme(), 511 url.getHost(), 512 url.getPort(), 513 dn, 514 null, // no attributes 515 SearchScope.BASE_OBJECT, 516 null, // No filter 517 null); // No extensions 518 } 519 520} 521 522/** A structure representing authentication data. */ 523class AuthRecord { 524 String dn; 525 String password; 526} 527 528/** A structure representing an active connection. */ 529class ConnectionRecord { 530 InitialLdapContext ctx; 531 int counter; 532 boolean disconnectAfterUse; 533}