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