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 2012-2016 ForgeRock AS.
016 */
017package org.opends.admin.ads.util;
018
019import java.io.IOException;
020import java.net.ConnectException;
021import java.net.URI;
022import java.util.HashSet;
023import java.util.Hashtable;
024import java.util.Set;
025
026import javax.naming.CommunicationException;
027import javax.naming.Context;
028import javax.naming.NamingEnumeration;
029import javax.naming.NamingException;
030import javax.naming.directory.Attribute;
031import javax.naming.directory.Attributes;
032import javax.naming.directory.SearchControls;
033import javax.naming.directory.SearchResult;
034import javax.naming.ldap.Control;
035import javax.naming.ldap.InitialLdapContext;
036import javax.naming.ldap.StartTlsRequest;
037import javax.naming.ldap.StartTlsResponse;
038import javax.net.ssl.HostnameVerifier;
039import javax.net.ssl.KeyManager;
040import javax.net.ssl.TrustManager;
041
042import org.forgerock.i18n.LocalizableMessage;
043import org.forgerock.i18n.slf4j.LocalizedLogger;
044import org.opends.server.replication.plugin.EntryHistorical;
045import org.opends.server.schema.SchemaConstants;
046import org.opends.server.types.HostPort;
047
048import com.forgerock.opendj.cli.Utils;
049
050/**
051 * Class providing some utilities to create LDAP connections using JNDI and
052 * to manage entries retrieved using JNDI.
053 *
054 */
055public class ConnectionUtils
056{
057  private static final String STARTTLS_PROPERTY =
058    "org.opends.connectionutils.isstarttls";
059
060  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
061
062  /**
063   * Private constructor: this class cannot be instantiated.
064   */
065  private ConnectionUtils()
066  {
067  }
068
069  /**
070   * Creates a clear LDAP connection and returns the corresponding LdapContext.
071   * This methods uses the specified parameters to create a JNDI environment
072   * hashtable and creates an InitialLdapContext instance.
073   *
074   * @param ldapURL
075   *          the target LDAP URL
076   * @param dn
077   *          passed as Context.SECURITY_PRINCIPAL if not null
078   * @param pwd
079   *          passed as Context.SECURITY_CREDENTIALS if not null
080   * @param timeout
081   *          passed as com.sun.jndi.ldap.connect.timeout if > 0
082   * @param env
083   *          null or additional environment properties
084   *
085   * @throws NamingException
086   *           the exception thrown when instantiating InitialLdapContext
087   *
088   * @return the created InitialLdapContext.
089   * @see javax.naming.Context
090   * @see javax.naming.ldap.InitialLdapContext
091   */
092  public static InitialLdapContext createLdapContext(String ldapURL, String dn,
093      String pwd, int timeout, Hashtable<String, String> env)
094      throws NamingException
095  {
096    env = copy(env);
097    env.put(Context.INITIAL_CONTEXT_FACTORY,
098        "com.sun.jndi.ldap.LdapCtxFactory");
099    env.put("java.naming.ldap.attributes.binary",
100        EntryHistorical.HISTORICAL_ATTRIBUTE_NAME);
101    env.put(Context.PROVIDER_URL, ldapURL);
102    if (timeout >= 1)
103    {
104      env.put("com.sun.jndi.ldap.connect.timeout", String.valueOf(timeout));
105    }
106    if (dn != null)
107    {
108      env.put(Context.SECURITY_PRINCIPAL, dn);
109    }
110    if (pwd != null)
111    {
112      env.put(Context.SECURITY_CREDENTIALS, pwd);
113    }
114
115    /* Contains the DirContext and the Exception if any */
116    final Object[] pair = new Object[]
117      { null, null };
118    final Hashtable<String, String> fEnv = env;
119    Thread t = new Thread(new Runnable()
120    {
121      @Override
122      public void run()
123      {
124        try
125        {
126          pair[0] = new InitialLdapContext(fEnv, null);
127
128        } catch (NamingException ne)
129        {
130          pair[1] = ne;
131
132        } catch (Throwable t)
133        {
134          t.printStackTrace();
135          pair[1] = t;
136        }
137      }
138    });
139    t.setDaemon(true);
140    return getInitialLdapContext(t, pair, timeout);
141  }
142
143  /**
144   * Creates an LDAPS connection and returns the corresponding LdapContext.
145   * This method uses the TrusteSocketFactory class so that the specified
146   * trust manager gets called during the SSL handshake. If trust manager is
147   * null, certificates are not verified during SSL handshake.
148   *
149   * @param ldapsURL      the target *LDAPS* URL.
150   * @param dn            passed as Context.SECURITY_PRINCIPAL if not null.
151   * @param pwd           passed as Context.SECURITY_CREDENTIALS if not null.
152   * @param timeout       passed as com.sun.jndi.ldap.connect.timeout if > 0.
153   * @param env           null or additional environment properties.
154   * @param trustManager  null or the trust manager to be invoked during SSL
155   * negotiation.
156   * @param keyManager    null or the key manager to be invoked during SSL
157   * negotiation.
158   * @return the established connection with the given parameters.
159   *
160   * @throws NamingException the exception thrown when instantiating
161   * InitialLdapContext.
162   *
163   * @see javax.naming.Context
164   * @see javax.naming.ldap.InitialLdapContext
165   * @see TrustedSocketFactory
166   */
167  public static InitialLdapContext createLdapsContext(String ldapsURL,
168      String dn, String pwd, int timeout, Hashtable<String, String> env,
169      TrustManager trustManager, KeyManager keyManager) throws NamingException {
170    env = copy(env);
171    env.put(Context.INITIAL_CONTEXT_FACTORY,
172        "com.sun.jndi.ldap.LdapCtxFactory");
173    env.put("java.naming.ldap.attributes.binary",
174        EntryHistorical.HISTORICAL_ATTRIBUTE_NAME);
175    env.put(Context.PROVIDER_URL, ldapsURL);
176    env.put("java.naming.ldap.factory.socket",
177        org.opends.admin.ads.util.TrustedSocketFactory.class.getName());
178
179    if (dn != null && pwd != null)
180    {
181      env.put(Context.SECURITY_PRINCIPAL, dn);
182      env.put(Context.SECURITY_CREDENTIALS, pwd);
183    }
184
185    if (trustManager == null)
186    {
187      trustManager = new BlindTrustManager();
188    }
189
190    /* Contains the DirContext and the Exception if any */
191    final Object[] pair = new Object[] {null, null};
192    final Hashtable<String, String> fEnv = env;
193    final TrustManager fTrustManager = trustManager;
194    final KeyManager   fKeyManager   = keyManager;
195
196    Thread t = new Thread(new Runnable() {
197      @Override
198      public void run() {
199        try {
200          TrustedSocketFactory.setCurrentThreadTrustManager(fTrustManager,
201              fKeyManager);
202          pair[0] = new InitialLdapContext(fEnv, null);
203        } catch (NamingException | RuntimeException ne) {
204          pair[1] = ne;
205        }
206      }
207    });
208    t.setDaemon(true);
209    return getInitialLdapContext(t, pair, timeout);
210  }
211
212  /**
213   * Clones the provided InitialLdapContext and returns a connection using
214   * the same parameters.
215   * @param ctx the connection to be cloned.
216   * @param timeout the timeout to establish the connection in milliseconds.
217   * Use {@code 0} to express no timeout.
218   * @param trustManager the trust manager to be used to connect.
219   * @param keyManager the key manager to be used to connect.
220   * @return the new InitialLdapContext connected to the server.
221   * @throws NamingException if there was an error creating the new connection.
222   */
223  public static InitialLdapContext cloneInitialLdapContext(
224      final InitialLdapContext ctx, int timeout, TrustManager trustManager,
225      KeyManager keyManager) throws NamingException
226  {
227    Hashtable<?, ?> env = ctx.getEnvironment();
228    Control[] ctls = ctx.getConnectControls();
229    Control[] newCtls = null;
230    if (ctls != null)
231    {
232      newCtls = new Control[ctls.length];
233      System.arraycopy(ctls, 0, newCtls, 0, ctls.length);
234    }
235    /* Contains the DirContext and the Exception if any */
236    final Object[] pair = new Object[] {null, null};
237    final Hashtable<?, ?> fEnv = env;
238    final TrustManager fTrustManager = trustManager;
239    final KeyManager   fKeyManager   = keyManager;
240    final Control[] fNewCtls = newCtls;
241
242    Thread t = new Thread(new Runnable() {
243      @Override
244      public void run() {
245        try {
246          if (isSSL(ctx) || isStartTLS(ctx))
247          {
248            TrustedSocketFactory.setCurrentThreadTrustManager(fTrustManager,
249                fKeyManager);
250          }
251          pair[0] = new InitialLdapContext(fEnv, fNewCtls);
252        } catch (NamingException | RuntimeException ne) {
253          pair[1] = ne;
254        }
255      }
256    });
257    return getInitialLdapContext(t, pair, timeout);
258  }
259
260  /**
261   * Creates an LDAP+StartTLS connection and returns the corresponding
262   * LdapContext.
263   * This method first creates an LdapContext with anonymous bind. Then it
264   * requests a StartTlsRequest extended operation. The StartTlsResponse is
265   * setup with the specified hostname verifier. Negotiation is done using a
266   * TrustSocketFactory so that the specified TrustManager gets called during
267   * the SSL handshake.
268   * If trust manager is null, certificates are not checked during SSL
269   * handshake.
270   *
271   * @param ldapURL       the target *LDAP* URL.
272   * @param dn            passed as Context.SECURITY_PRINCIPAL if not null.
273   * @param pwd           passed as Context.SECURITY_CREDENTIALS if not null.
274   * @param timeout       passed as com.sun.jndi.ldap.connect.timeout if > 0.
275   * @param env           null or additional environment properties.
276   * @param trustManager  null or the trust manager to be invoked during SSL
277   * negotiation.
278   * @param keyManager    null or the key manager to be invoked during SSL
279   * negotiation.
280   * @param verifier      null or the hostname verifier to be setup in the
281   * StartTlsResponse.
282   * @return the established connection with the given parameters.
283   *
284   * @throws NamingException the exception thrown when instantiating
285   * InitialLdapContext.
286   *
287   * @see javax.naming.Context
288   * @see javax.naming.ldap.InitialLdapContext
289   * @see javax.naming.ldap.StartTlsRequest
290   * @see javax.naming.ldap.StartTlsResponse
291   * @see TrustedSocketFactory
292   */
293
294  public static InitialLdapContext createStartTLSContext(String ldapURL,
295      String dn, String pwd, int timeout, Hashtable<String, String> env,
296      TrustManager trustManager, KeyManager keyManager,
297      HostnameVerifier verifier)
298  throws NamingException
299  {
300    if (trustManager == null)
301    {
302      trustManager = new BlindTrustManager();
303    }
304    if (verifier == null) {
305      verifier = new BlindHostnameVerifier();
306    }
307
308    env = copy(env);
309    env.put(Context.INITIAL_CONTEXT_FACTORY,
310        "com.sun.jndi.ldap.LdapCtxFactory");
311    env.put("java.naming.ldap.attributes.binary",
312        EntryHistorical.HISTORICAL_ATTRIBUTE_NAME);
313    env.put(Context.PROVIDER_URL, ldapURL);
314    env.put(Context.SECURITY_AUTHENTICATION , "none");
315
316    /* Contains the DirContext and the Exception if any */
317    final Object[] pair = new Object[] {null, null};
318    final Hashtable<?, ?> fEnv = env;
319    final String fDn = dn;
320    final String fPwd = pwd;
321    final TrustManager fTrustManager = trustManager;
322    final KeyManager fKeyManager     = keyManager;
323    final HostnameVerifier fVerifier = verifier;
324
325    Thread t = new Thread(new Runnable() {
326      @Override
327      public void run() {
328        try {
329          StartTlsResponse tls;
330
331          InitialLdapContext result = new InitialLdapContext(fEnv, null);
332
333          tls = (StartTlsResponse) result.extendedOperation(
334              new StartTlsRequest());
335          tls.setHostnameVerifier(fVerifier);
336          try
337          {
338            tls.negotiate(new TrustedSocketFactory(fTrustManager,fKeyManager));
339          }
340          catch(IOException x) {
341            NamingException xx;
342            xx = new CommunicationException(
343                "Failed to negotiate Start TLS operation");
344            xx.initCause(x);
345            result.close();
346            throw xx;
347          }
348
349          result.addToEnvironment(STARTTLS_PROPERTY, "true");
350          if (fDn != null)
351          {
352            result.addToEnvironment(Context.SECURITY_AUTHENTICATION , "simple");
353            result.addToEnvironment(Context.SECURITY_PRINCIPAL, fDn);
354            if (fPwd != null)
355            {
356              result.addToEnvironment(Context.SECURITY_CREDENTIALS, fPwd);
357            }
358            result.reconnect(null);
359          }
360          pair[0] = result;
361        } catch (NamingException | RuntimeException ne)
362        {
363          pair[1] = ne;
364        }
365      }
366    });
367    t.setDaemon(true);
368    return getInitialLdapContext(t, pair, timeout);
369  }
370
371  private static Hashtable<String, String> copy(Hashtable<String, String> env) {
372    return env != null ? new Hashtable<>(env) : new Hashtable<String, String>();
373  }
374
375  /**
376   * Returns the LDAP URL used in the provided InitialLdapContext.
377   * @param ctx the context to analyze.
378   * @return the LDAP URL used in the provided InitialLdapContext.
379   */
380  public static String getLdapUrl(InitialLdapContext ctx)
381  {
382    return getEnvProperty(ctx, Context.PROVIDER_URL);
383  }
384
385  /**
386   * Returns the host name used in the provided InitialLdapContext.
387   * @param ctx the context to analyze.
388   * @return the host name used in the provided InitialLdapContext.
389   */
390  public static String getHostName(InitialLdapContext ctx)
391  {
392    HostPort hp = getHostPort(ctx);
393    return hp != null ? hp.getHost() : null;
394  }
395
396  /**
397   * Returns the host port representation of the server to which this
398   * context is connected.
399   * @param ctx the context to analyze.
400   * @return the host port representation of the server to which this
401   * context is connected.
402   */
403  public static HostPort getHostPort(InitialLdapContext ctx)
404  {
405    try
406    {
407      URI ldapURL = new URI(getLdapUrl(ctx));
408      return new HostPort(ldapURL.getHost(), ldapURL.getPort());
409    }
410    catch (Throwable t)
411    {
412      // This is really strange.  Seems like a bug somewhere.
413      logger.warn(LocalizableMessage.raw("Error getting host: "+t, t));
414      return null;
415    }
416  }
417
418  /**
419   * Returns the bind DN used in the provided InitialLdapContext.
420   * @param ctx the context to analyze.
421   * @return the bind DN used in the provided InitialLdapContext.
422   */
423  public static String getBindDN(InitialLdapContext ctx)
424  {
425    return getEnvProperty(ctx, Context.SECURITY_PRINCIPAL);
426  }
427
428  /**
429   * Returns the password used in the provided InitialLdapContext.
430   * @param ctx the context to analyze.
431   * @return the password used in the provided InitialLdapContext.
432   */
433  public static String getBindPassword(InitialLdapContext ctx)
434  {
435    return getEnvProperty(ctx, Context.SECURITY_CREDENTIALS);
436  }
437
438  private static String getEnvProperty(InitialLdapContext ctx, String property) {
439    try {
440      return (String) ctx.getEnvironment().get(property);
441    } catch (NamingException ne) {
442      // This is really strange.  Seems like a bug somewhere.
443      logger.warn(LocalizableMessage.raw("Naming exception getting environment of " + ctx, ne));
444      return null;
445    }
446  }
447
448  /**
449   * Tells whether we are using SSL in the provided InitialLdapContext.
450   * @param ctx the context to analyze.
451   * @return <CODE>true</CODE> if we are using SSL and <CODE>false</CODE>
452   * otherwise.
453   */
454  public static boolean isSSL(InitialLdapContext ctx)
455  {
456    try
457    {
458      return getLdapUrl(ctx).toLowerCase().startsWith("ldaps");
459    }
460    catch (Throwable t)
461    {
462      // This is really strange.  Seems like a bug somewhere.
463      logger.warn(LocalizableMessage.raw("Error getting if is SSL "+t, t));
464      return false;
465    }
466  }
467
468  /**
469   * Tells whether we are using StartTLS in the provided InitialLdapContext.
470   * @param ctx the context to analyze.
471   * @return <CODE>true</CODE> if we are using StartTLS and <CODE>false</CODE>
472   * otherwise.
473   */
474  public static boolean isStartTLS(InitialLdapContext ctx)
475  {
476    return "true".equalsIgnoreCase(getEnvProperty(ctx, STARTTLS_PROPERTY));
477  }
478
479
480
481  /**
482   * Method used to know if we are connected as administrator in a server with a
483   * given InitialLdapContext.
484   * @param ctx the context.
485   * @return <CODE>true</CODE> if we are connected and read the configuration
486   * and <CODE>false</CODE> otherwise.
487   */
488  static boolean connectedAsAdministrativeUser(InitialLdapContext ctx)
489  {
490    try
491    {
492      // Search for the config to check that it is the directory manager.
493      SearchControls searchControls = new SearchControls();
494      searchControls.setSearchScope(
495          SearchControls. OBJECT_SCOPE);
496      searchControls.setReturningAttributes(
497          new String[] { SchemaConstants.NO_ATTRIBUTES });
498      NamingEnumeration<SearchResult> sr =
499       ctx.search("cn=config", "objectclass=*", searchControls);
500      try
501      {
502        while (sr.hasMore())
503        {
504          sr.next();
505        }
506      }
507      finally
508      {
509        try
510        {
511          sr.close();
512        }
513        catch(Exception ex)
514        {
515          logger.warn(LocalizableMessage.raw(
516              "Unexpected error closing enumeration on cn=Config entry", ex));
517        }
518      }
519      return true;
520    } catch (NamingException ne)
521    {
522      // Nothing to do.
523      return false;
524    } catch (Throwable t)
525    {
526      throw new IllegalStateException("Unexpected throwable.", t);
527    }
528  }
529
530  /**
531   * This is just a commodity method used to try to get an InitialLdapContext.
532   * @param t the Thread to be used to create the InitialLdapContext.
533   * @param pair an Object[] array that contains the InitialLdapContext and the
534   * Throwable if any occurred.
535   * @param timeout the timeout in milliseconds.  If we do not get to create the
536   * connection before the timeout a CommunicationException will be thrown.
537   * @return the created InitialLdapContext
538   * @throws NamingException if something goes wrong during the creation.
539   */
540  private static InitialLdapContext getInitialLdapContext(Thread t,
541      Object[] pair, int timeout) throws NamingException
542  {
543    try
544    {
545      if (timeout > 0)
546      {
547        t.start();
548        t.join(timeout);
549      } else
550      {
551        t.run();
552      }
553
554    } catch (InterruptedException x)
555    {
556      // This might happen for problems in sockets
557      // so it does not necessarily imply a bug
558    }
559
560    boolean throwException = false;
561
562    if (timeout > 0 && t.isAlive())
563    {
564      t.interrupt();
565      try
566      {
567        t.join(2000);
568      } catch (InterruptedException x)
569      {
570        // This might happen for problems in sockets
571        // so it does not necessarily imply a bug
572      }
573      throwException = true;
574    }
575
576    if (pair[0] == null && pair[1] == null)
577    {
578      throwException = true;
579    }
580
581    if (throwException)
582    {
583      NamingException xx = new CommunicationException("Connection timed out");
584      xx.initCause(new ConnectException("Connection timed out"));
585      throw xx;
586    }
587
588    if (pair[1] != null)
589    {
590      if (pair[1] instanceof NamingException)
591      {
592        throw (NamingException) pair[1];
593
594      } else if (pair[1] instanceof RuntimeException)
595      {
596        throw (RuntimeException) pair[1];
597
598      } else if (pair[1] instanceof Throwable)
599      {
600        throw new IllegalStateException("Unexpected throwable occurred",
601            (Throwable) pair[1]);
602      }
603    }
604    return (InitialLdapContext) pair[0];
605  }
606
607  /**
608   * Returns the LDAP URL for the provided parameters.
609   * @param hostPort the host name and LDAP port.
610   * @param useSSL whether to use SSL or not.
611   * @return the LDAP URL for the provided parameters.
612   */
613  public static String getLDAPUrl(HostPort hostPort, boolean useSSL)
614  {
615    return getLDAPUrl(hostPort.getHost(), hostPort.getPort(), useSSL);
616  }
617
618  /**
619   * Returns the LDAP URL for the provided parameters.
620   * @param host the host name.
621   * @param port the LDAP port.
622   * @param useSSL whether to use SSL or not.
623   * @return the LDAP URL for the provided parameters.
624   */
625  public static String getLDAPUrl(String host, int port, boolean useSSL)
626  {
627    host = Utils.getHostNameForLdapUrl(host);
628    return (useSSL ? "ldaps://" : "ldap://") + host + ":" + port;
629  }
630
631  /**
632   * Returns the String representation of the first value of an attribute in a
633   * LDAP entry.
634   * @param entry the entry.
635   * @param attrName the attribute name.
636   * @return the String representation of the first value of an attribute in a
637   * LDAP entry.
638   * @throws NamingException if there is an error processing the entry.
639   */
640  public static String getFirstValue(SearchResult entry, String attrName)
641  throws NamingException
642  {
643    String v = null;
644    Attributes attrs = entry.getAttributes();
645    if (attrs != null)
646    {
647      Attribute attr = attrs.get(attrName);
648      if (attr != null && attr.size() > 0)
649      {
650        Object o = attr.get();
651        if (o instanceof String)
652        {
653          v = (String)o;
654        }
655        else
656        {
657          v = String.valueOf(o);
658        }
659      }
660    }
661    return v;
662  }
663
664  /**
665   * Returns a Set with the String representation of the values of an attribute
666   * in a LDAP entry.  The returned Set will never be null.
667   * @param entry the entry.
668   * @param attrName the attribute name.
669   * @return a Set with the String representation of the values of an attribute
670   * in a LDAP entry.
671   * @throws NamingException if there is an error processing the entry.
672   */
673  public static Set<String> getValues(SearchResult entry, String attrName)
674  throws NamingException
675  {
676    Set<String> values = new HashSet<>();
677    Attributes attrs = entry.getAttributes();
678    if (attrs != null)
679    {
680      Attribute attr = attrs.get(attrName);
681      if (attr != null)
682      {
683        for (int i=0; i<attr.size(); i++)
684        {
685          values.add((String)attr.get(i));
686        }
687      }
688    }
689    return values;
690  }
691}