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