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}