/* * 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. * Portions Copyright 2012-2013 ForgeRock AS */ package org.opends.admin.ads.util; import java.io.IOException; import java.net.ConnectException; import java.net.URI; import java.security.GeneralSecurityException; import java.util.HashSet; import java.util.Hashtable; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.naming.CommunicationException; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.Attributes; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; import javax.naming.ldap.Control; import javax.naming.ldap.InitialLdapContext; import javax.naming.ldap.StartTlsRequest; import javax.naming.ldap.StartTlsResponse; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLHandshakeException; import javax.net.ssl.TrustManager; import org.opends.server.schema.SchemaConstants; /** * Class providing some utilities to create LDAP connections using JNDI and * to manage entries retrieved using JNDI. * */ public class ConnectionUtils { private static final int DEFAULT_LDAP_CONNECT_TIMEOUT = 30000; private static final String STARTTLS_PROPERTY = "org.opends.connectionutils.isstarttls"; static private final Logger LOG = Logger.getLogger(ConnectionUtils.class.getName()); /** * Private constructor: this class cannot be instantiated. */ private ConnectionUtils() { } /** * Creates a clear LDAP connection and returns the corresponding LdapContext. * This methods uses the specified parameters to create a JNDI environment * hashtable and creates an InitialLdapContext instance. * * @param ldapURL * the target LDAP URL * @param dn * passed as Context.SECURITY_PRINCIPAL if not null * @param pwd * passed as Context.SECURITY_CREDENTIALS if not null * @param timeout * passed as com.sun.jndi.ldap.connect.timeout if > 0 * @param env * null or additional environment properties * * @throws NamingException * the exception thrown when instantiating InitialLdapContext * * @return the created InitialLdapContext. * @see javax.naming.Context * @see javax.naming.ldap.InitialLdapContext */ public static InitialLdapContext createLdapContext(String ldapURL, String dn, String pwd, int timeout, Hashtable<String, String> env) throws NamingException { if (env != null) { // We clone 'env' so that we can modify it freely env = new Hashtable<String, String>(env); } else { env = new Hashtable<String, String>(); } env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, ldapURL); if (timeout >= 1) { env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(timeout)); } if (dn != null) { env.put(Context.SECURITY_PRINCIPAL, dn); } if (pwd != null) { env.put(Context.SECURITY_CREDENTIALS, pwd); } /* Contains the DirContext and the Exception if any */ final Object[] pair = new Object[] { null, null }; final Hashtable<String, String> fEnv = env; Thread t = new Thread(new Runnable() { @Override public void run() { try { pair[0] = new InitialLdapContext(fEnv, null); } catch (NamingException ne) { pair[1] = ne; } catch (Throwable t) { t.printStackTrace(); pair[1] = t; } } }); t.setDaemon(true); return getInitialLdapContext(t, pair, timeout); } /** * Creates an LDAPS connection and returns the corresponding LdapContext. * This method uses the TrusteSocketFactory class so that the specified * trust manager gets called during the SSL handshake. If trust manager is * null, certificates are not verified during SSL handshake. * * @param ldapsURL the target *LDAPS* URL. * @param dn passed as Context.SECURITY_PRINCIPAL if not null. * @param pwd passed as Context.SECURITY_CREDENTIALS if not null. * @param timeout passed as com.sun.jndi.ldap.connect.timeout if > 0. * @param env null or additional environment properties. * @param trustManager null or the trust manager to be invoked during SSL * negotiation. * @param keyManager null or the key manager to be invoked during SSL * negotiation. * @return the established connection with the given parameters. * * @throws NamingException the exception thrown when instantiating * InitialLdapContext. * * @see javax.naming.Context * @see javax.naming.ldap.InitialLdapContext * @see TrustedSocketFactory */ public static InitialLdapContext createLdapsContext(String ldapsURL, String dn, String pwd, int timeout, Hashtable<String, String> env, TrustManager trustManager, KeyManager keyManager) throws NamingException { if (env != null) { // We clone 'env' so that we can modify it freely env = new Hashtable<String, String>(env); } else { env = new Hashtable<String, String>(); } env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, ldapsURL); env.put("java.naming.ldap.factory.socket", org.opends.admin.ads.util.TrustedSocketFactory.class.getName()); if (dn != null) { env.put(Context.SECURITY_PRINCIPAL, dn); } if (pwd != null) { env.put(Context.SECURITY_CREDENTIALS, pwd); } if (trustManager == null) { trustManager = new BlindTrustManager(); } /* Contains the DirContext and the Exception if any */ final Object[] pair = new Object[] {null, null}; final Hashtable<String, String> fEnv = env; final TrustManager fTrustManager = trustManager; final KeyManager fKeyManager = keyManager; Thread t = new Thread(new Runnable() { public void run() { try { TrustedSocketFactory.setCurrentThreadTrustManager(fTrustManager, fKeyManager); pair[0] = new InitialLdapContext(fEnv, null); } catch (NamingException ne) { pair[1] = ne; } catch (RuntimeException re) { pair[1] = re; } } }); t.setDaemon(true); return getInitialLdapContext(t, pair, timeout); } /** * Clones the provided InitialLdapContext and returns a connection using * the same parameters. * @param ctx the connection to be cloned. * @param timeout the timeout to establish the connection in milliseconds. * Use {@code 0} to express no timeout. * @param trustManager the trust manager to be used to connect. * @param keyManager the key manager to be used to connect. * @return the new InitialLdapContext connected to the server. * @throws NamingException if there was an error creating the new connection. */ public static InitialLdapContext cloneInitialLdapContext( final InitialLdapContext ctx, int timeout, TrustManager trustManager, KeyManager keyManager) throws NamingException { Hashtable<?, ?> env = ctx.getEnvironment(); Control[] ctls = ctx.getConnectControls(); Control[] newCtls = null; if (ctls != null) { newCtls = new Control[ctls.length]; System.arraycopy(ctls, 0, newCtls, 0, ctls.length); } /* Contains the DirContext and the Exception if any */ final Object[] pair = new Object[] {null, null}; final Hashtable<?, ?> fEnv = env; final TrustManager fTrustManager = trustManager; final KeyManager fKeyManager = keyManager; final Control[] fNewCtls = newCtls; Thread t = new Thread(new Runnable() { @Override public void run() { try { if (isSSL(ctx) || isStartTLS(ctx)) { TrustedSocketFactory.setCurrentThreadTrustManager(fTrustManager, fKeyManager); } pair[0] = new InitialLdapContext(fEnv, fNewCtls); } catch (NamingException ne) { pair[1] = ne; } catch (RuntimeException re) { pair[1] = re; } } }); return getInitialLdapContext(t, pair, timeout); } /** * Creates an LDAP+StartTLS connection and returns the corresponding * LdapContext. * This method first creates an LdapContext with anonymous bind. Then it * requests a StartTlsRequest extended operation. The StartTlsResponse is * setup with the specified hostname verifier. Negotiation is done using a * TrustSocketFactory so that the specified TrustManager gets called during * the SSL handshake. * If trust manager is null, certificates are not checked during SSL * handshake. * * @param ldapURL the target *LDAP* URL. * @param dn passed as Context.SECURITY_PRINCIPAL if not null. * @param pwd passed as Context.SECURITY_CREDENTIALS if not null. * @param timeout passed as com.sun.jndi.ldap.connect.timeout if > 0. * @param env null or additional environment properties. * @param trustManager null or the trust manager to be invoked during SSL * negotiation. * @param keyManager null or the key manager to be invoked during SSL * negotiation. * @param verifier null or the hostname verifier to be setup in the * StartTlsResponse. * @return the established connection with the given parameters. * * @throws NamingException the exception thrown when instantiating * InitialLdapContext. * * @see javax.naming.Context * @see javax.naming.ldap.InitialLdapContext * @see javax.naming.ldap.StartTlsRequest * @see javax.naming.ldap.StartTlsResponse * @see TrustedSocketFactory */ public static InitialLdapContext createStartTLSContext(String ldapURL, String dn, String pwd, int timeout, Hashtable<String, String> env, TrustManager trustManager, KeyManager keyManager, HostnameVerifier verifier) throws NamingException { if (trustManager == null) { trustManager = new BlindTrustManager(); } if (verifier == null) { verifier = new BlindHostnameVerifier(); } if (env != null) { // We clone 'env' to modify it freely env = new Hashtable<String, String>(env); } else { env = new Hashtable<String, String>(); } env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, ldapURL); env.put(Context.SECURITY_AUTHENTICATION , "none"); /* Contains the DirContext and the Exception if any */ final Object[] pair = new Object[] {null, null}; final Hashtable<?, ?> fEnv = env; final String fDn = dn; final String fPwd = pwd; final TrustManager fTrustManager = trustManager; final KeyManager fKeyManager = keyManager; final HostnameVerifier fVerifier = verifier; Thread t = new Thread(new Runnable() { @Override public void run() { try { StartTlsResponse tls; InitialLdapContext result = new InitialLdapContext(fEnv, null); tls = (StartTlsResponse) result.extendedOperation( new StartTlsRequest()); tls.setHostnameVerifier(fVerifier); try { tls.negotiate(new TrustedSocketFactory(fTrustManager,fKeyManager)); } catch(IOException x) { NamingException xx; xx = new CommunicationException( "Failed to negotiate Start TLS operation"); xx.initCause(x); result.close(); throw xx; } result.addToEnvironment(STARTTLS_PROPERTY, "true"); if (fDn != null) { result.addToEnvironment(Context.SECURITY_AUTHENTICATION , "simple"); result.addToEnvironment(Context.SECURITY_PRINCIPAL, fDn); if (fPwd != null) { result.addToEnvironment(Context.SECURITY_CREDENTIALS, fPwd); } result.reconnect(null); } pair[0] = result; } catch (NamingException ne) { pair[1] = ne; } catch (RuntimeException re) { pair[1] = re; } } }); t.setDaemon(true); return getInitialLdapContext(t, pair, timeout); } /** * Returns the LDAP URL used in the provided InitialLdapContext. * @param ctx the context to analyze. * @return the LDAP URL used in the provided InitialLdapContext. */ public static String getLdapUrl(InitialLdapContext ctx) { String s = null; try { s = (String)ctx.getEnvironment().get(Context.PROVIDER_URL); } catch (NamingException ne) { // This is really strange. Seems like a bug somewhere. LOG.log(Level.WARNING, "Naming exception getting environment of "+ctx, ne); } return s; } /** * Returns the host name used in the provided InitialLdapContext. * @param ctx the context to analyze. * @return the host name used in the provided InitialLdapContext. */ public static String getHostName(InitialLdapContext ctx) { String s = null; try { URI ldapURL = new URI(getLdapUrl(ctx)); s = ldapURL.getHost(); } catch (Throwable t) { // This is really strange. Seems like a bug somewhere. LOG.log(Level.WARNING, "Error getting host: "+t, t); } return s; } /** * Returns the port number used in the provided InitialLdapContext. * @param ctx the context to analyze. * @return the port number used in the provided InitialLdapContext. */ public static int getPort(InitialLdapContext ctx) { int port = -1; try { URI ldapURL = new URI(getLdapUrl(ctx)); port = ldapURL.getPort(); } catch (Throwable t) { // This is really strange. Seems like a bug somewhere. LOG.log(Level.WARNING, "Error getting port: "+t, t); } return port; } /** * Returns the host port representation of the server to which this * context is connected. * @param ctx the context to analyze. * @return the host port representation of the server to which this * context is connected. */ public static String getHostPort(InitialLdapContext ctx) { return getHostName(ctx)+":"+getPort(ctx); } /** * Returns the bind DN used in the provided InitialLdapContext. * @param ctx the context to analyze. * @return the bind DN used in the provided InitialLdapContext. */ public static String getBindDN(InitialLdapContext ctx) { String bindDN = null; try { bindDN = (String)ctx.getEnvironment().get(Context.SECURITY_PRINCIPAL); } catch (NamingException ne) { // This is really strange. Seems like a bug somewhere. LOG.log(Level.WARNING, "Naming exception getting environment of "+ctx, ne); } return bindDN; } /** * Returns the password used in the provided InitialLdapContext. * @param ctx the context to analyze. * @return the password used in the provided InitialLdapContext. */ public static String getBindPassword(InitialLdapContext ctx) { String bindPwd = null; try { bindPwd = (String)ctx.getEnvironment().get(Context.SECURITY_CREDENTIALS); } catch (NamingException ne) { // This is really strange. Seems like a bug somewhere. LOG.log(Level.WARNING, "Naming exception getting environment of "+ctx, ne); } return bindPwd; } /** * Tells whether we are using SSL in the provided InitialLdapContext. * @param ctx the context to analyze. * @return <CODE>true</CODE> if we are using SSL and <CODE>false</CODE> * otherwise. */ public static boolean isSSL(InitialLdapContext ctx) { boolean isSSL = false; try { isSSL = getLdapUrl(ctx).toLowerCase().startsWith("ldaps"); } catch (Throwable t) { // This is really strange. Seems like a bug somewhere. LOG.log(Level.WARNING, "Error getting if is SSL "+t, t); } return isSSL; } /** * Tells whether we are using StartTLS in the provided InitialLdapContext. * @param ctx the context to analyze. * @return <CODE>true</CODE> if we are using StartTLS and <CODE>false</CODE> * otherwise. */ public static boolean isStartTLS(InitialLdapContext ctx) { boolean isStartTLS = false; try { isStartTLS = "true".equalsIgnoreCase((String)ctx.getEnvironment().get( STARTTLS_PROPERTY)); } catch (NamingException ne) { // This is really strange. Seems like a bug somewhere. LOG.log(Level.WARNING, "Naming exception getting environment of "+ctx, ne); } return isStartTLS; } /** * Method used to know if we can connect as administrator in a server with a * given password and dn. * @param ldapUrl the LDAP URL of the server. * @param dn the dn to be used. * @param pwd the password to be used. * @param timeout the timeout to establish the connection in milliseconds. * Use {@code 0} to express no timeout. * @return <CODE>true</CODE> if we can connect and read the configuration and * <CODE>false</CODE> otherwise. */ public static boolean canConnectAsAdministrativeUser(String ldapUrl, String dn, String pwd, int timeout) { boolean canConnectAsAdministrativeUser = false; try { InitialLdapContext ctx; if (ldapUrl.toLowerCase().startsWith("ldap:")) { ctx = createLdapContext(ldapUrl, dn, pwd, timeout, null); } else { ctx = createLdapsContext(ldapUrl, dn, pwd, timeout, null, null, null); } canConnectAsAdministrativeUser = connectedAsAdministrativeUser(ctx); } catch (NamingException ne) { // Nothing to do. } catch (Throwable t) { throw new IllegalStateException("Unexpected throwable.", t); } return canConnectAsAdministrativeUser; } /** * Method used to know if we are connected as administrator in a server with a * given InitialLdapContext. * @param ctx the context. * @return <CODE>true</CODE> if we are connected and read the configuration * and <CODE>false</CODE> otherwise. */ public static boolean connectedAsAdministrativeUser(InitialLdapContext ctx) { boolean connectedAsAdministrativeUser = false; try { /* * Search for the config to check that it is the directory manager. */ SearchControls searchControls = new SearchControls(); searchControls.setSearchScope( SearchControls. OBJECT_SCOPE); searchControls.setReturningAttributes( new String[] { SchemaConstants.NO_ATTRIBUTES }); NamingEnumeration<SearchResult> sr = ctx.search("cn=config", "objectclass=*", searchControls); try { while (sr.hasMore()) { sr.next(); } } finally { try { sr.close(); } catch(Exception ex) { LOG.log(Level.WARNING, "Unexpected error closing enumeration on cn=Config entry", ex); } } connectedAsAdministrativeUser = true; } catch (NamingException ne) { // Nothing to do. } catch (Throwable t) { throw new IllegalStateException("Unexpected throwable.", t); } return connectedAsAdministrativeUser; } /** * This is just a commodity method used to try to get an InitialLdapContext. * @param t the Thread to be used to create the InitialLdapContext. * @param pair an Object[] array that contains the InitialLdapContext and the * Throwable if any occurred. * @param timeout the timeout in milliseconds. If we do not get to create the * connection before the timeout a CommunicationException will be thrown. * @return the created InitialLdapContext * @throws NamingException if something goes wrong during the creation. */ private static InitialLdapContext getInitialLdapContext(Thread t, Object[] pair, int timeout) throws NamingException { try { if (timeout > 0) { t.start(); t.join(timeout); } else { t.run(); } } catch (InterruptedException x) { // This might happen for problems in sockets // so it does not necessarily imply a bug } boolean throwException = false; if ((timeout > 0) && t.isAlive()) { t.interrupt(); try { t.join(2000); } catch (InterruptedException x) { // This might happen for problems in sockets // so it does not necessarily imply a bug } throwException = true; } if ((pair[0] == null) && (pair[1] == null)) { throwException = true; } if (throwException) { NamingException xx; ConnectException x = new ConnectException("Connection timed out"); xx = new CommunicationException("Connection timed out"); xx.initCause(x); throw xx; } if (pair[1] != null) { if (pair[1] instanceof NamingException) { throw (NamingException) pair[1]; } else if (pair[1] instanceof RuntimeException) { throw (RuntimeException) pair[1]; } else if (pair[1] instanceof Throwable) { throw new IllegalStateException("Unexpected throwable occurred", (Throwable) pair[1]); } } return (InitialLdapContext) pair[0]; } /** * Returns the default LDAP timeout in milliseconds when we try to connect to * a server. * @return the default LDAP timeout in milliseconds when we try to connect to * a server. */ public static int getDefaultLDAPTimeout() { return DEFAULT_LDAP_CONNECT_TIMEOUT; } /** * Returns the String that can be used to represent a given host name in a * LDAP URL. * This method must be used when we have IPv6 addresses (the address in the * LDAP URL must be enclosed with brackets). * @param host the host name. * @return the String that can be used to represent a given host name in a * LDAP URL. */ public static String getHostNameForLdapUrl(String host) { if ((host != null) && host.indexOf(":") != -1) { // Assume an IPv6 address has been specified and adds the brackets // for the URL. host = host.trim(); if (!host.startsWith("[")) { host = "["+host; } if (!host.endsWith("]")) { host = host + "]"; } } return host; } /** * Returns the LDAP URL for the provided parameters. * @param host the host name. * @param port the LDAP port. * @param useSSL whether to use SSL or not. * @return the LDAP URL for the provided parameters. */ public static String getLDAPUrl(String host, int port, boolean useSSL) { String ldapUrl; host = getHostNameForLdapUrl(host); if (useSSL) { ldapUrl = "ldaps://"+host+":"+port; } else { ldapUrl = "ldap://"+host+":"+port; } return ldapUrl; } /** * Tells whether the provided Throwable was caused because of a problem with * a certificate while trying to establish a connection. * @param t the Throwable to analyze. * @return <CODE>true</CODE> if the provided Throwable was caused because of a * problem with a certificate while trying to establish a connection and * <CODE>false</CODE> otherwise. */ public static boolean isCertificateException(Throwable t) { boolean returnValue = false; while (!returnValue && (t != null)) { returnValue = (t instanceof SSLHandshakeException) || (t instanceof GeneralSecurityException); t = t.getCause(); } return returnValue; } /** * Returns the String representation of the first value of an attribute in a * LDAP entry. * @param entry the entry. * @param attrName the attribute name. * @return the String representation of the first value of an attribute in a * LDAP entry. * @throws NamingException if there is an error processing the entry. */ static public String getFirstValue(SearchResult entry, String attrName) throws NamingException { String v = null; Attributes attrs = entry.getAttributes(); if (attrs != null) { Attribute attr = attrs.get(attrName); if ((attr != null) && (attr.size() > 0)) { Object o = attr.get(); if (o instanceof String) { v = (String)o; } else { v = String.valueOf(o); } } } return v; } /** * Returns a Set with the String representation of the values of an attribute * in a LDAP entry. The returned Set will never be null. * @param entry the entry. * @param attrName the attribute name. * @return a Set with the String representation of the values of an attribute * in a LDAP entry. * @throws NamingException if there is an error processing the entry. */ static public Set<String> getValues(SearchResult entry, String attrName) throws NamingException { Set<String> values = new HashSet<String>(); Attributes attrs = entry.getAttributes(); if (attrs != null) { Attribute attr = attrs.get(attrName); if (attr != null) { for (int i=0; i<attr.size(); i++) { values.add((String)attr.get(i)); } } } return values; } }