/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2012-2016 ForgeRock AS. */ package org.opends.admin.ads.util; import java.io.IOException; import java.net.ConnectException; import java.util.HashSet; import java.util.Hashtable; import java.util.Set; 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.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.TrustManager; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.ldap.responses.SearchResultEntry; import org.opends.server.replication.plugin.EntryHistorical; import org.opends.server.schema.SchemaConstants; import org.opends.server.types.HostPort; import com.forgerock.opendj.cli.Utils; /** * Class providing some utilities to create LDAP connections using JNDI and * to manage entries retrieved using JNDI. * */ public class ConnectionUtils { private static final String STARTTLS_PROPERTY = "org.opends.connectionutils.isstarttls"; private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** * 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 */ static InitialLdapContext createLdapContext(String ldapURL, String dn, String pwd, int timeout, Hashtable env) throws NamingException { env = newEnvironmentFrom(ldapURL, env); if (timeout >= 1) { env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(timeout)); } if (dn != null && pwd != null) { env.put(Context.SECURITY_PRINCIPAL, dn); env.put(Context.SECURITY_CREDENTIALS, pwd); } /* Contains the DirContext and the Exception if any */ final Object[] pair = { null, null }; final Hashtable 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 */ static InitialLdapContext createLdapsContext(String ldapsURL, String dn, String pwd, int timeout, Hashtable env, TrustManager trustManager, final KeyManager keyManager) throws NamingException { final Hashtable newEnv = newEnvironmentFrom(ldapsURL, env); newEnv.put("java.naming.ldap.factory.socket", TrustedSocketFactory.class.getName()); if (dn != null && pwd != null) { newEnv.put(Context.SECURITY_PRINCIPAL, dn); newEnv.put(Context.SECURITY_CREDENTIALS, pwd); } if (trustManager == null) { trustManager = new BlindTrustManager(); } /* Contains the DirContext and the Exception if any */ final Object[] pair = { null, null }; final TrustManager fTrustManager = trustManager; Thread t = new Thread(new Runnable() { @Override public void run() { try { TrustedSocketFactory.setCurrentThreadTrustManager(fTrustManager, keyManager); pair[0] = new InitialLdapContext(newEnv, null); } catch (NamingException | RuntimeException ne) { pair[1] = ne; } } }); t.setDaemon(true); return getInitialLdapContext(t, pair, timeout); } /** * Clones the provided InitialLdapContext and returns a connection using * the same parameters. * @param conn 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 ConnectionWrapper cloneConnectionWrapper( final ConnectionWrapper conn, int timeout, final TrustManager trustManager, final KeyManager keyManager) throws NamingException { final Object[] pair = { null, null }; Thread t = new Thread(new Runnable() { @Override public void run() { try { if (conn.isSSL() || conn.isStartTLS()) { TrustedSocketFactory.setCurrentThreadTrustManager(trustManager, keyManager); } pair[0] = new ConnectionWrapper(conn); } catch (NamingException | RuntimeException ne) { pair[1] = ne; } } }); return ConnectionUtils. getConnection(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 */ static InitialLdapContext createStartTLSContext(String ldapURL, final String dn, final String pwd, int timeout, Hashtable env, TrustManager trustManager, final KeyManager keyManager, HostnameVerifier verifier) throws NamingException { if (trustManager == null) { trustManager = new BlindTrustManager(); } if (verifier == null) { verifier = new BlindHostnameVerifier(); } final Hashtable newEnv = newEnvironmentFrom(ldapURL, env); newEnv.put(Context.SECURITY_AUTHENTICATION, "none"); /* Contains the DirContext and the Exception if any */ final Object[] pair = { null, null }; final TrustManager fTrustManager = trustManager; final HostnameVerifier fVerifier = verifier; Thread t = new Thread(new Runnable() { @Override public void run() { try { InitialLdapContext result = new InitialLdapContext(newEnv, null); StartTlsResponse tls = (StartTlsResponse) result.extendedOperation(new StartTlsRequest()); tls.setHostnameVerifier(fVerifier); try { tls.negotiate(new TrustedSocketFactory(fTrustManager, keyManager)); } catch(IOException x) { NamingException xx = new CommunicationException( "Failed to negotiate Start TLS operation"); xx.initCause(x); result.close(); throw xx; } result.addToEnvironment(STARTTLS_PROPERTY, "true"); if (dn != null) { result.addToEnvironment(Context.SECURITY_AUTHENTICATION , "simple"); result.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); if (pwd != null) { result.addToEnvironment(Context.SECURITY_CREDENTIALS, pwd); } result.reconnect(null); } pair[0] = result; } catch (NamingException | RuntimeException ne) { pair[1] = ne; } } }); t.setDaemon(true); return getInitialLdapContext(t, pair, timeout); } private static Hashtable copy(Hashtable env) { return env != null ? new Hashtable<>(env) : new Hashtable(); } private static Hashtable newEnvironmentFrom(String ldapURL, Hashtable env) { final Hashtable copy = copy(env); copy.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); copy.put("java.naming.ldap.attributes.binary", EntryHistorical.HISTORICAL_ATTRIBUTE_NAME); copy.put(Context.PROVIDER_URL, ldapURL); return copy; } /** * Method used to know if we are connected as administrator in a server with a * given InitialLdapContext. * @param ctx the context. * @return {@code true} if we are connected and read the configuration * and {@code false} otherwise. */ static boolean connectedAsAdministrativeUser(InitialLdapContext ctx) { 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 sr = ctx.search("cn=config", "objectclass=*", searchControls); try { while (sr.hasMore()) { sr.next(); } } finally { try { sr.close(); } catch(Exception ex) { logger.warn(LocalizableMessage.raw( "Unexpected error closing enumeration on cn=Config entry", ex)); } } return true; } catch (NamingException ne) { // Nothing to do. return false; } catch (Throwable t) { throw new IllegalStateException("Unexpected throwable.", t); } } /** * 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 { return ConnectionUtils. getConnection(t, pair, timeout); } private static T getConnection(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 } 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 } throw connectionTimedOut(); } Object connection = pair[0]; Object ex = pair[1]; if (connection == null && ex == null) { throw connectionTimedOut(); } if (ex != null) { if (ex instanceof NamingException) { throw (NamingException) ex; } else if (ex instanceof RuntimeException) { throw (RuntimeException) ex; } else if (ex instanceof Throwable) { throw new IllegalStateException("Unexpected throwable occurred", (Throwable) ex); } } return (T) connection; } private static NamingException connectionTimedOut() { NamingException xx = new CommunicationException("Connection timed out"); xx.initCause(new ConnectException("Connection timed out")); return xx; } /** * Returns the LDAP URL for the provided parameters. * @param hostPort the host name and LDAP port. * @param useSSL whether to use SSL or not. * @return the LDAP URL for the provided parameters. */ public static String getLDAPUrl(HostPort hostPort, boolean useSSL) { return getLDAPUrl(hostPort.getHost(), hostPort.getPort(), useSSL); } /** * 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) { host = Utils.getHostNameForLdapUrl(host); return (useSSL ? "ldaps://" : "ldap://") + host + ":" + port; } /** * 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. */ public static 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. */ public static Set getValues(SearchResult entry, String attrName) throws NamingException { Set values = new HashSet<>(); Attributes attrs = entry.getAttributes(); if (attrs != null) { Attribute attr = attrs.get(attrName); if (attr != null) { for (int i=0; i asSetOfString(SearchResultEntry sr, String attrDesc) { org.forgerock.opendj.ldap.Attribute attr = sr.getAttribute(attrDesc); return attr != null ? attr.parse().asSetOfString() : null; } /** * Returns the values decoded as a set of DNs. * * @param sr * the search result entry * @param attrDesc * the attribute description * @return The values decoded as a set of DNs. Never {@code null} and never contains {@code null} * values. */ public static Set asSetOfDN(SearchResultEntry sr, String attrDesc) { org.forgerock.opendj.ldap.Attribute attr = sr.getAttribute(attrDesc); return attr != null ? attr.parse().asSetOfDN() : null; } }