/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2008-2010 Sun Microsystems, Inc. */ package org.opends.guitools.controlpanel.browser; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import javax.naming.NamingException; import javax.naming.ldap.Control; import javax.naming.ldap.InitialLdapContext; import javax.net.ssl.KeyManager; import org.opends.admin.ads.util.ApplicationTrustManager; import org.opends.admin.ads.util.ConnectionUtils; import org.opends.guitools.controlpanel.event.ReferralAuthenticationListener; import org.opends.server.types.DN; import org.opends.server.types.LDAPURL; import org.opends.server.types.SearchScope; /** * An LDAPConnectionPool is a pool of LDAPConnection. * <BR><BR> * When a client class needs to access an LDAPUrl, it simply passes * this URL to getConnection() and gets an LDAPConnection back. * When the client has finished with this LDAPConnection, it *must* * pass it releaseConnection() which will take care of its disconnection * or caching. * <BR><BR> * LDAPConnectionPool maintains a pool of authentications. This pool * is populated using registerAuth(). When getConnection() has created * a new connection for accessing a host:port, it looks in the authentication * pool if any authentication is available for this host:port and, if yes, * tries to bind the connection. If no authentication is available, the * returned connection is simply connected (ie anonymous bind). * <BR><BR> * LDAPConnectionPool shares connections and maintains a usage counter * for each connection: two calls to getConnection() with the same URL * will return the same connection. Two calls to releaseConnection() will * be needed to make the connection 'potentially disconnectable'. * <BR><BR> * releaseConnection() does not disconnect systematically a connection * whose usage counter is null. It keeps it connected a while (TODO: * to be implemented). * <BR><BR> * TODO: synchronization is a bit simplistic... */ public class LDAPConnectionPool { HashMap<String, AuthRecord> authTable = new HashMap<String, AuthRecord>(); HashMap<String, ConnectionRecord> connectionTable = new HashMap<String, ConnectionRecord>(); ArrayList<ReferralAuthenticationListener> listeners; private Control[] requestControls = new Control[] {}; private ApplicationTrustManager trustManager; private int connectTimeout = ConnectionUtils.getDefaultLDAPTimeout(); /** * Returns <CODE>true</CODE> if the connection passed is registered in the * connection pool, <CODE>false</CODE> otherwise. * @param ctx the connection. * @return <CODE>true</CODE> if the connection passed is registered in the * connection pool, <CODE>false</CODE> otherwise. */ public boolean isConnectionRegistered(InitialLdapContext ctx) { boolean isConnectionRegistered = false; for (String key : connectionTable.keySet()) { ConnectionRecord cr = connectionTable.get(key); if (cr.ctx != null) { isConnectionRegistered = ConnectionUtils.getHostName(cr.ctx).equals( ConnectionUtils.getHostName(ctx)) && (ConnectionUtils.getPort(cr.ctx) == ConnectionUtils.getPort(ctx)) && ConnectionUtils.getBindDN(cr.ctx).equals( ConnectionUtils.getBindDN(ctx)) && ConnectionUtils.getBindPassword(cr.ctx).equals( ConnectionUtils.getBindPassword(ctx)) && (ConnectionUtils.isSSL(cr.ctx) == ConnectionUtils.isSSL(ctx)) && (ConnectionUtils.isStartTLS(cr.ctx) == ConnectionUtils.isStartTLS(ctx)); } if (isConnectionRegistered) { break; } } return isConnectionRegistered; } /** * Registers a connection in this connection pool. * @param ctx the connection to be registered. */ public void registerConnection(InitialLdapContext ctx) { registerAuth(ctx); LDAPURL url = makeLDAPUrl( ConnectionUtils.getHostName(ctx), ConnectionUtils.getPort(ctx), "", ConnectionUtils.isSSL(ctx) ); String key = makeKeyFromLDAPUrl(url); ConnectionRecord cr = new ConnectionRecord(); cr.ctx = ctx; cr.counter = 1; cr.disconnectAfterUse = false; connectionTable.put(key, cr); } /** * Unregisters a connection from this connection pool. * @param ctx the connection to be unregistered. * @throws NamingException if there is a problem unregistering the connection. */ public void unregisterConnection(InitialLdapContext ctx) throws NamingException { LDAPURL url = makeLDAPUrl( ConnectionUtils.getHostName(ctx), ConnectionUtils.getPort(ctx), "", ConnectionUtils.isSSL(ctx)); unRegisterAuth(url); String key = makeKeyFromLDAPUrl(url); connectionTable.remove(key); } /** * Adds a referral authentication listener. * @param listener the referral authentication listener. */ public void addReferralAuthenticationListener( ReferralAuthenticationListener listener) { if (listeners == null) { listeners = new ArrayList<ReferralAuthenticationListener>(); } listeners.add(listener); } /** * Removes a referral authentication listener. * @param listener the referral authentication listener. */ public void removeReferralAuthenticationListener( ReferralAuthenticationListener listener) { if (listeners != null) { listeners.remove(listener); } } /** * Returns an LDAPConnection for accessing the specified url. * If no connection are available for the protocol/host/port * of the URL, getConnection() makes a new one and call connect(). * If authentication data available for this protocol/host/port, * getConnection() call bind() on the new connection. * If connect() or bind() failed, getConnection() forward the * NamingException. * When getConnection() succeeds, the returned connection must * be passed to releaseConnection() after use. * @param ldapUrl the LDAP URL to which the connection must connect. * @return a connection to the provided LDAP URL. * @throws NamingException if there was an error connecting. */ public InitialLdapContext getConnection(LDAPURL ldapUrl) throws NamingException { String key = makeKeyFromLDAPUrl(ldapUrl); ConnectionRecord cr; synchronized(this) { cr = connectionTable.get(key); if (cr == null) { cr = new ConnectionRecord(); cr.ctx = null; cr.counter = 1; cr.disconnectAfterUse = false; connectionTable.put(key, cr); } else { cr.counter++; } } synchronized(cr) { try { if (cr.ctx == null) { boolean registerAuth = false; AuthRecord authRecord = authTable.get(key); if (authRecord == null) { // Best-effort: try with an already registered authentication authRecord = authTable.values().iterator().next(); registerAuth = true; } cr.ctx = createLDAPConnection(ldapUrl, authRecord); cr.ctx.setRequestControls(requestControls); if (registerAuth) { authTable.put(key, authRecord); } } } catch(NamingException x) { synchronized (this) { cr.counter--; if (cr.counter == 0) { connectionTable.remove(key); } } throw x; } } return cr.ctx; } /** * Sets the request controls to be used by the connections of this connection * pool. * @param ctls the request controls. * @throws NamingException if an error occurs updating the connections. */ public synchronized void setRequestControls(Control[] ctls) throws NamingException { requestControls = ctls; for (ConnectionRecord cr : connectionTable.values()) { if (cr.ctx != null) { cr.ctx.setRequestControls(requestControls); } } } /** * Release an LDAPConnection created by getConnection(). * The connection should be considered as virtually disconnected * and not be used anymore. * @param ctx the connection to be released. */ public synchronized void releaseConnection(InitialLdapContext ctx) { String targetKey = null; ConnectionRecord targetRecord = null; synchronized(this) { for (String key : connectionTable.keySet()) { ConnectionRecord cr = connectionTable.get(key); if (cr.ctx == ctx) { targetKey = key; targetRecord = cr; if (targetKey != null) { break; } } } } if (targetRecord == null) { // ldc is not in _connectionTable -> bug throw new IllegalArgumentException("Invalid LDAP connection"); } else { synchronized(targetRecord) { targetRecord.counter--; if ((targetRecord.counter == 0) && targetRecord.disconnectAfterUse) { disconnectAndRemove(targetRecord); } } } } /** * Disconnect the connections which are not being used. * Connections being used will be disconnected as soon * as they are released. */ public synchronized void flush() { for (ConnectionRecord cr : connectionTable.values()) { if (cr.counter <= 0) { disconnectAndRemove(cr); } else { cr.disconnectAfterUse = true; } } } /** * Register authentication data. * If authentication data are already available for the protocol/host/port * specified in the LDAPURl, they are replaced by the new data. * If true is passed as 'connect' parameter, registerAuth() creates the * connection and attempts to connect() and bind() . If connect() or bind() * fail, registerAuth() forwards the NamingException and does not register * the authentication data. * @param ldapUrl the LDAP URL of the server. * @param dn the bind DN. * @param pw the password. * @param connect whether to connect or not to the server with the * provided authentication (for testing purposes). * @throws NamingException if an error occurs connecting. */ public void registerAuth(LDAPURL ldapUrl, String dn, String pw, boolean connect) throws NamingException { String key = makeKeyFromLDAPUrl(ldapUrl); AuthRecord ar; ar = new AuthRecord(); ar.ldapUrl = ldapUrl; ar.dn = dn; ar.password = pw; if (connect) { InitialLdapContext ctx = createLDAPConnection(ldapUrl, ar); ctx.close(); } synchronized(this) { authTable.put(key, ar); ConnectionRecord cr = connectionTable.get(key); if (cr != null) { if (cr.counter <= 0) { disconnectAndRemove(cr); } else { cr.disconnectAfterUse = true; } } } notifyListeners(); } /** * Register authentication data from an existing connection. * This routine recreates the LDAP URL corresponding to * the connection and passes it to registerAuth(LDAPURL). * @param ctx the connection that we retrieve the authentication information * from. */ public void registerAuth(InitialLdapContext ctx) { LDAPURL url = makeLDAPUrl( ConnectionUtils.getHostName(ctx), ConnectionUtils.getPort(ctx), "", ConnectionUtils.isSSL(ctx)); try { registerAuth(url, ConnectionUtils.getBindDN(ctx), ConnectionUtils.getBindPassword(ctx), false); } catch (NamingException x) { throw new RuntimeException("Bug"); } } /** * Unregister authentication data. * If for the given url there's a connection, try to bind as anonymous. * If unbind fails throw NamingException. * @param ldapUrl the url associated with the authentication to be * unregistered. * @throws NamingException if the unbind fails. */ public void unRegisterAuth(LDAPURL ldapUrl) throws NamingException { String key = makeKeyFromLDAPUrl(ldapUrl); authTable.remove(key); notifyListeners(); } /** * Get authentication DN registered for this url. * @param ldapUrl the LDAP URL for which we want to get authentication DN. * @return the bind DN of the authentication. */ public synchronized String getAuthDN(LDAPURL ldapUrl) { String result; String key = makeKeyFromLDAPUrl(ldapUrl); AuthRecord ar = authTable.get(key); if (ar == null) { result = null; } else { result = ar.dn; } return result; } /** * Get authentication password registered for this url. * @param ldapUrl the LDAP URL for which we want to get authentication * password. * @return the password of the authentication. */ public synchronized String getAuthPassword(LDAPURL ldapUrl) { String result; String key = makeKeyFromLDAPUrl(ldapUrl); AuthRecord ar = authTable.get(key); if (ar == null) { result = null; } else { result = ar.password; } return result; } /** * Disconnect the connection associated to a record * and remove the record from connectionTable. * @param cr the ConnectionRecord to remove. */ private void disconnectAndRemove(ConnectionRecord cr) { String key = makeKeyFromRecord(cr); connectionTable.remove(key); try { cr.ctx.close(); } catch (NamingException x) { // Bizarre. However it's not really a problem here. } } /** * Notifies the listeners that a referral authentication change happened. * */ private void notifyListeners() { for (ReferralAuthenticationListener listener : listeners) { listener.notifyAuthDataChanged(); } } /** * Make the key string for an LDAP URL. * @param url the LDAP URL. * @return the key to be used in Maps for the provided LDAP URL. */ private static String makeKeyFromLDAPUrl(LDAPURL url) { String protocol = isSecureLDAPUrl(url) ? "LDAPS" : "LDAP"; return protocol + ":" + url.getHost() + ":" + url.getPort(); } /** * Make the key string for an connection record. * @param rec the connection record. * @return the key to be used in Maps for the provided connection record. */ private static String makeKeyFromRecord(ConnectionRecord rec) { String protocol = ConnectionUtils.isSSL(rec.ctx) ? "LDAPS" : "LDAP"; return protocol + ":" + ConnectionUtils.getHostName(rec.ctx) + ":" + ConnectionUtils.getPort(rec.ctx); } /** * Creates an LDAP Connection for a given LDAP URL and using the * authentication of a AuthRecord. * @param ldapUrl the LDAP URL. * @param ar the authentication information. * @return a connection. * @throws NamingException if an error occurs when connecting. */ private InitialLdapContext createLDAPConnection(LDAPURL ldapUrl, AuthRecord ar) throws NamingException { InitialLdapContext ctx; // Take the base DN out of the URL and only keep the protocol, host and port ldapUrl = new LDAPURL(ldapUrl.getScheme(), ldapUrl.getHost(), ldapUrl.getPort(), (DN)null, null, null, null, null); if (isSecureLDAPUrl(ldapUrl)) { ctx = ConnectionUtils.createLdapsContext(ldapUrl.toString(), ar.dn, ar.password, getConnectTimeout(), null, getTrustManager() , getKeyManager()); } else { ctx = ConnectionUtils.createLdapContext(ldapUrl.toString(), ar.dn, ar.password, getConnectTimeout(), null); } return ctx; } /** * Sets the ApplicationTrustManager used by the connection pool to * connect to servers. * @param trustManager the ApplicationTrustManager. */ public void setTrustManager(ApplicationTrustManager trustManager) { this.trustManager = trustManager; } /** * Returns the ApplicationTrustManager used by the connection pool to * connect to servers. * @return the ApplicationTrustManager used by the connection pool to * connect to servers. */ public ApplicationTrustManager getTrustManager() { return trustManager; } /** * Returns the timeout to establish the connection in milliseconds. * @return the timeout to establish the connection in milliseconds. */ public int getConnectTimeout() { return connectTimeout; } /** * Sets the timeout to establish the connection in milliseconds. * Use {@code 0} to express no timeout. * @param connectTimeout the timeout to establish the connection in * milliseconds. * Use {@code 0} to express no timeout. */ public void setConnectTimeout(int connectTimeout) { this.connectTimeout = connectTimeout; } private KeyManager getKeyManager() { // TODO: we should get it from ControlPanelInfo return null; } /** * Returns whether the URL is ldaps URL or not. * @param url the URL. * @return <CODE>true</CODE> if the LDAP URL is secure and <CODE>false</CODE> * otherwise. */ public static boolean isSecureLDAPUrl(LDAPURL url) { return !LDAPURL.DEFAULT_SCHEME.equalsIgnoreCase(url.getScheme()); } /** * Make an url from the specified arguments. * @param host the host. * @param port the port. * @param dn the dn. * @param secure whether it is a secure URL or not. * @return an LDAP URL from the specified arguments. */ public static LDAPURL makeLDAPUrl(String host, int port, String dn, boolean secure) { return new LDAPURL( secure ? "ldaps" : LDAPURL.DEFAULT_SCHEME, host, port, dn, null, // no attributes SearchScope.BASE_OBJECT, null, // No filter null); // No extensions } /** * Make an url from the specified arguments. * @param ctx the connection to the server. * @param dn the base DN of the URL. * @return an LDAP URL from the specified arguments. */ public static LDAPURL makeLDAPUrl(InitialLdapContext ctx, String dn) { return new LDAPURL( ConnectionUtils.isSSL(ctx) ? "ldaps" : LDAPURL.DEFAULT_SCHEME, ConnectionUtils.getHostName(ctx), ConnectionUtils.getPort(ctx), dn, null, // No attributes SearchScope.BASE_OBJECT, null, null); // No filter } /** * Make an url from the specified arguments. * @param url an LDAP URL to use as base of the new LDAP URL. * @param dn the base DN for the new LDAP URL. * @return an LDAP URL from the specified arguments. */ public static LDAPURL makeLDAPUrl(LDAPURL url, String dn) { return new LDAPURL( url.getScheme(), url.getHost(), url.getPort(), dn, null, // no attributes SearchScope.BASE_OBJECT, null, // No filter null); // No extensions } /** * Returns a collection of AuthRecord. * @return a collection of AuthRecord. */ Collection<?> getRegisteredAuthentication() { return authTable.values(); } } /** * A structure representing authentication data. */ class AuthRecord { LDAPURL ldapUrl; String dn; String password; } /** * A structure representing an active connection. */ class ConnectionRecord { InitialLdapContext ctx; int counter; boolean disconnectAfterUse; }