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 2009-2010 Sun Microsystems, Inc. 015 * Portions Copyright 2013-2016 ForgeRock AS. 016 */ 017package org.opends.server.tools; 018 019import java.io.IOException; 020import java.io.PrintStream; 021import java.net.ConnectException; 022import java.net.InetAddress; 023import java.net.Socket; 024import java.net.SocketException; 025import java.net.UnknownHostException; 026import java.util.ArrayList; 027import java.util.concurrent.atomic.AtomicInteger; 028 029import org.forgerock.i18n.LocalizableMessage; 030import org.forgerock.i18n.slf4j.LocalizedLogger; 031import org.forgerock.opendj.ldap.ByteString; 032import org.opends.server.controls.AuthorizationIdentityResponseControl; 033import org.opends.server.controls.ControlDecoder; 034import org.opends.server.controls.PasswordExpiringControl; 035import org.opends.server.controls.PasswordPolicyErrorType; 036import org.opends.server.controls.PasswordPolicyResponseControl; 037import org.opends.server.controls.PasswordPolicyWarningType; 038import org.opends.server.loggers.JDKLogging; 039import org.opends.server.protocols.ldap.ExtendedRequestProtocolOp; 040import org.opends.server.protocols.ldap.ExtendedResponseProtocolOp; 041import org.opends.server.protocols.ldap.LDAPControl; 042import org.opends.server.protocols.ldap.LDAPMessage; 043import org.opends.server.protocols.ldap.UnbindRequestProtocolOp; 044import org.opends.server.types.Control; 045import org.opends.server.types.DirectoryException; 046import org.opends.server.types.LDAPException; 047 048import com.forgerock.opendj.cli.ClientException; 049 050import static org.opends.messages.CoreMessages.*; 051import static org.opends.messages.ToolMessages.*; 052import static org.opends.server.protocols.ldap.LDAPResultCode.*; 053import static org.opends.server.util.ServerConstants.*; 054import static org.opends.server.util.StaticUtils.*; 055 056/** 057 * This class provides a tool that can be used to issue search requests to the 058 * Directory Server. 059 */ 060public class LDAPConnection 061{ 062 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 063 064 /** The hostname to connect to. */ 065 private String hostName; 066 067 /** The port number on which the directory server is accepting requests. */ 068 private int portNumber = 389; 069 070 private LDAPConnectionOptions connectionOptions; 071 private LDAPWriter ldapWriter; 072 private LDAPReader ldapReader; 073 private int versionNumber = 3; 074 075 private final PrintStream out; 076 private final PrintStream err; 077 078 /** 079 * Constructor for the LDAPConnection object. 080 * 081 * @param host The hostname to send the request to. 082 * @param port The port number on which the directory server is accepting 083 * requests. 084 * @param options The set of options for this connection. 085 */ 086 public LDAPConnection(String host, int port, LDAPConnectionOptions options) 087 { 088 this(host, port, options, System.out, System.err); 089 } 090 091 /** 092 * Constructor for the LDAPConnection object. 093 * 094 * @param host The hostname to send the request to. 095 * @param port The port number on which the directory server is accepting 096 * requests. 097 * @param options The set of options for this connection. 098 * @param out The print stream to use for standard output. 099 * @param err The print stream to use for standard error. 100 */ 101 public LDAPConnection(String host, int port, LDAPConnectionOptions options, 102 PrintStream out, PrintStream err) 103 { 104 this.hostName = host; 105 this.portNumber = port; 106 this.connectionOptions = options; 107 this.versionNumber = options.getVersionNumber(); 108 this.out = out; 109 this.err = err; 110 } 111 112 /** 113 * Connects to the directory server instance running on specified hostname 114 * and port number. 115 * 116 * @param bindDN The DN to bind with. 117 * @param bindPassword The password to bind with. 118 * 119 * @throws LDAPConnectionException If a problem occurs while attempting to 120 * establish the connection to the server. 121 */ 122 public void connectToHost(String bindDN, String bindPassword) 123 throws LDAPConnectionException 124 { 125 connectToHost(bindDN, bindPassword, new AtomicInteger(1), 0); 126 } 127 128 /** 129 * Connects to the directory server instance running on specified hostname 130 * and port number. 131 * 132 * @param bindDN The DN to bind with. 133 * @param bindPassword The password to bind with. 134 * @param nextMessageID The message ID counter that should be used for 135 * operations performed while establishing the 136 * connection. 137 * 138 * @throws LDAPConnectionException If a problem occurs while attempting to 139 * establish the connection to the server. 140 */ 141 public void connectToHost(String bindDN, String bindPassword, 142 AtomicInteger nextMessageID) 143 throws LDAPConnectionException 144 { 145 connectToHost(bindDN, bindPassword, nextMessageID, 0); 146 } 147 148 /** 149 * Connects to the directory server instance running on specified hostname 150 * and port number. 151 * 152 * @param bindDN The DN to bind with. 153 * @param bindPassword The password to bind with. 154 * @param nextMessageID The message ID counter that should be used for 155 * operations performed while establishing the 156 * connection. 157 * @param timeout The timeout to connect to the specified host. The 158 * timeout is the timeout at the socket level in 159 * milliseconds. If the timeout value is {@code 0}, 160 * no timeout is used. 161 * 162 * @throws LDAPConnectionException If a problem occurs while attempting to 163 * establish the connection to the server. 164 */ 165 public void connectToHost(String bindDN, String bindPassword, 166 AtomicInteger nextMessageID, int timeout) 167 throws LDAPConnectionException 168 { 169 Socket socket; 170 Socket startTLSSocket = null; 171 int resultCode; 172 ArrayList<Control> requestControls = new ArrayList<> (); 173 ArrayList<Control> responseControls = new ArrayList<> (); 174 175 if (connectionOptions.isVerbose()) 176 { 177 JDKLogging.enableVerboseConsoleLoggingForOpenDJ(); 178 } 179 else 180 { 181 JDKLogging.disableLogging(); 182 } 183 184 if(connectionOptions.useStartTLS()) 185 { 186 try 187 { 188 startTLSSocket = createSocket(); 189 ldapWriter = new LDAPWriter(startTLSSocket); 190 ldapReader = new LDAPReader(startTLSSocket); 191 } 192 catch (LDAPConnectionException e) 193 { 194 throw e; 195 } 196 catch (Exception ex) 197 { 198 logger.traceException(ex); 199 throw new LDAPConnectionException(LocalizableMessage.raw(ex.getMessage()), ex); 200 } 201 202 // Send the StartTLS extended request. 203 ExtendedRequestProtocolOp extendedRequest = 204 new ExtendedRequestProtocolOp(OID_START_TLS_REQUEST); 205 206 LDAPMessage msg = new LDAPMessage(nextMessageID.getAndIncrement(), 207 extendedRequest); 208 try 209 { 210 ldapWriter.writeMessage(msg); 211 msg = ldapReader.readMessage(); 212 } 213 catch (LDAPException e) 214 { 215 logger.traceException(e); 216 throw new LDAPConnectionException(e.getMessageObject(), e.getResultCode(), null, e); 217 } 218 catch (Exception e) 219 { 220 logger.traceException(e); 221 throw new LDAPConnectionException(LocalizableMessage.raw(e.getMessage()), e); 222 } 223 if (msg == null) 224 { 225 throw new LDAPConnectionException(ERR_STARTTLS_FAILED.get(), CLIENT_SIDE_CONNECT_ERROR, null); 226 } 227 ExtendedResponseProtocolOp res = msg.getExtendedResponseProtocolOp(); 228 resultCode = res.getResultCode(); 229 if(resultCode != SUCCESS) 230 { 231 throw new LDAPConnectionException(res.getErrorMessage(), 232 resultCode, 233 res.getErrorMessage(), 234 res.getMatchedDN(), null); 235 } 236 } 237 SSLConnectionFactory sslConnectionFactory = 238 connectionOptions.getSSLConnectionFactory(); 239 try 240 { 241 socket = createSSLOrBasicSocket(startTLSSocket, sslConnectionFactory); 242 ldapWriter = new LDAPWriter(socket); 243 ldapReader = new LDAPReader(socket); 244 } catch(UnknownHostException | ConnectException e) 245 { 246 LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get(); 247 throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null, e); 248 } catch (LDAPConnectionException e) 249 { 250 throw e; 251 } catch(Exception ex2) 252 { 253 logger.traceException(ex2); 254 throw new LDAPConnectionException(LocalizableMessage.raw(ex2.getMessage()), ex2); 255 } 256 257 // We need this so that we don't run out of addresses when the tool 258 // commands are called A LOT, as in the unit tests. 259 try 260 { 261 socket.setSoLinger(true, 1); 262 socket.setReuseAddress(true); 263 if (timeout > 0) 264 { 265 socket.setSoTimeout(timeout); 266 } 267 } catch(IOException e) 268 { 269 logger.traceException(e); 270 // It doesn't matter too much if this throws, so ignore it. 271 } 272 273 if (connectionOptions.getReportAuthzID()) 274 { 275 requestControls.add(new LDAPControl(OID_AUTHZID_REQUEST)); 276 } 277 if (connectionOptions.usePasswordPolicyControl()) 278 { 279 requestControls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL)); 280 } 281 282 LDAPAuthenticationHandler handler = new LDAPAuthenticationHandler( 283 ldapReader, ldapWriter, hostName, nextMessageID); 284 try 285 { 286 ByteString bindDNBytes = bindDN != null ? ByteString.valueOfUtf8(bindDN) : ByteString.empty(); 287 ByteString bindPW = bindPassword != null ? ByteString.valueOfUtf8(bindPassword) : null; 288 289 String result = null; 290 if (connectionOptions.useSASLExternal()) 291 { 292 result = handler.doSASLExternal(bindDNBytes, 293 connectionOptions.getSASLProperties(), 294 requestControls, responseControls); 295 } 296 else if (connectionOptions.getSASLMechanism() != null) 297 { 298 result = handler.doSASLBind(bindDNBytes, 299 bindPW, 300 connectionOptions.getSASLMechanism(), 301 connectionOptions.getSASLProperties(), 302 requestControls, responseControls); 303 } 304 else if(bindDN != null) 305 { 306 result = handler.doSimpleBind(versionNumber, bindDNBytes, bindPW, 307 requestControls, responseControls); 308 } 309 if(result != null) 310 { 311 out.println(result); 312 } 313 314 for (Control c : responseControls) 315 { 316 if (c.getOID().equals(OID_AUTHZID_RESPONSE)) 317 { 318 AuthorizationIdentityResponseControl control = decode(c, AuthorizationIdentityResponseControl.DECODER); 319 out.println(INFO_BIND_AUTHZID_RETURNED.get(control.getAuthorizationID())); 320 } 321 else if (c.getOID().equals(OID_NS_PASSWORD_EXPIRED)) 322 { 323 out.println(INFO_BIND_PASSWORD_EXPIRED.get()); 324 } 325 else if (c.getOID().equals(OID_NS_PASSWORD_EXPIRING)) 326 { 327 PasswordExpiringControl control = decode(c, PasswordExpiringControl.DECODER); 328 LocalizableMessage timeString = secondsToTimeString(control.getSecondsUntilExpiration()); 329 out.println(INFO_BIND_PASSWORD_EXPIRING.get(timeString)); 330 } 331 else if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL)) 332 { 333 PasswordPolicyResponseControl pwPolicyControl = decode(c, PasswordPolicyResponseControl.DECODER); 334 335 PasswordPolicyErrorType errorType = pwPolicyControl.getErrorType(); 336 if (errorType != null) 337 { 338 switch (errorType) 339 { 340 case PASSWORD_EXPIRED: 341 out.println(INFO_BIND_PASSWORD_EXPIRED.get()); 342 break; 343 case ACCOUNT_LOCKED: 344 out.println(INFO_BIND_ACCOUNT_LOCKED.get()); 345 break; 346 case CHANGE_AFTER_RESET: 347 out.println(INFO_BIND_MUST_CHANGE_PASSWORD.get()); 348 break; 349 } 350 } 351 352 PasswordPolicyWarningType warningType = 353 pwPolicyControl.getWarningType(); 354 if (warningType != null) 355 { 356 switch (warningType) 357 { 358 case TIME_BEFORE_EXPIRATION: 359 LocalizableMessage timeString = 360 secondsToTimeString(pwPolicyControl.getWarningValue()); 361 out.println(INFO_BIND_PASSWORD_EXPIRING.get(timeString)); 362 break; 363 case GRACE_LOGINS_REMAINING: 364 out.println(INFO_BIND_GRACE_LOGINS_REMAINING.get(pwPolicyControl.getWarningValue())); 365 break; 366 } 367 } 368 } 369 } 370 } catch(ClientException ce) 371 { 372 logger.traceException(ce); 373 throw new LDAPConnectionException(ce.getMessageObject(), ce.getReturnCode(), 374 null, ce); 375 } catch (LDAPException le) { 376 throw new LDAPConnectionException(le.getMessageObject(), 377 le.getResultCode(), 378 le.getErrorMessage(), 379 le.getMatchedDN(), 380 le.getCause()); 381 } catch (DirectoryException de) 382 { 383 throw new LDAPConnectionException(de.getMessageObject(), 384 de.getResultCode().intValue(), null, de.getMatchedDN(), de.getCause()); 385 } catch(Exception ex) 386 { 387 logger.traceException(ex); 388 throw new LDAPConnectionException( 389 LocalizableMessage.raw(ex.getLocalizedMessage()),ex); 390 } 391 finally 392 { 393 if (timeout > 0) 394 { 395 try 396 { 397 socket.setSoTimeout(0); 398 } 399 catch (SocketException e) 400 { 401 e.printStackTrace(); 402 logger.traceException(e); 403 } 404 } 405 } 406 } 407 408 private <T extends Control> T decode(Control c, ControlDecoder<T> decoder) throws DirectoryException 409 { 410 if (c instanceof LDAPControl) 411 { 412 // We have to decode this control. 413 return decoder.decode(c.isCritical(), ((LDAPControl) c).getValue()); 414 } 415 // Control should already have been decoded. 416 return (T) c; 417 } 418 419 /** 420 * Creates a socket using the hostName and portNumber encapsulated in the 421 * current object. For each IP address associated to this host name, 422 * createSocket() will try to open a socket and it will return the first 423 * socket for which we successfully establish a connection. 424 * <p> 425 * This method can never return null because it will receive 426 * UnknownHostException before and then throw LDAPConnectionException. 427 * </p> 428 * 429 * @return a new {@link Socket}. 430 * @throws LDAPConnectionException 431 * if any exception occurs including UnknownHostException 432 */ 433 private Socket createSocket() throws LDAPConnectionException 434 { 435 ConnectException ce = null; 436 try 437 { 438 for (InetAddress inetAddress : InetAddress.getAllByName(hostName)) 439 { 440 try 441 { 442 return new Socket(inetAddress, portNumber); 443 } 444 catch (ConnectException ce2) 445 { 446 if (ce == null) 447 { 448 ce = ce2; 449 } 450 } 451 } 452 } 453 catch (UnknownHostException uhe) 454 { 455 LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get(); 456 throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null, 457 uhe); 458 } 459 catch (Exception ex) 460 { 461 // if we get there, something went awfully wrong while creatng one socket, 462 // no need to continue the for loop. 463 logger.traceException(ex); 464 throw new LDAPConnectionException(LocalizableMessage.raw(ex.getMessage()), ex); 465 } 466 if (ce != null) 467 { 468 LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get(); 469 throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null, 470 ce); 471 } 472 return null; 473 } 474 475 /** 476 * Creates an SSL socket using the hostName and portNumber encapsulated in the 477 * current object. For each IP address associated to this host name, 478 * createSSLSocket() will try to open a socket and it will return the first 479 * socket for which we successfully establish a connection. 480 * <p> 481 * This method can never return null because it will receive 482 * UnknownHostException before and then throw LDAPConnectionException. 483 * </p> 484 * 485 * @return a new {@link Socket}. 486 * @throws LDAPConnectionException 487 * if any exception occurs including UnknownHostException 488 */ 489 private Socket createSSLSocket(SSLConnectionFactory sslConnectionFactory) 490 throws SSLConnectionException, LDAPConnectionException 491 { 492 ConnectException ce = null; 493 try 494 { 495 for (InetAddress inetAddress : InetAddress.getAllByName(hostName)) 496 { 497 try 498 { 499 return sslConnectionFactory.createSocket(inetAddress, portNumber); 500 } 501 catch (ConnectException ce2) 502 { 503 if (ce == null) 504 { 505 ce = ce2; 506 } 507 } 508 } 509 } 510 catch (UnknownHostException uhe) 511 { 512 LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get(); 513 throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null, 514 uhe); 515 } 516 catch (Exception ex) 517 { 518 // if we get there, something went awfully wrong while creatng one socket, 519 // no need to continue the for loop. 520 logger.traceException(ex); 521 throw new LDAPConnectionException(LocalizableMessage.raw(ex.getMessage()), ex); 522 } 523 if (ce != null) 524 { 525 LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get(); 526 throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null, 527 ce); 528 } 529 return null; 530 } 531 532 /** 533 * Creates an SSL socket or a normal/basic socket using the hostName and 534 * portNumber encapsulated in the current object, or with the passed in socket 535 * if it needs to use start TLS. 536 * 537 * @param startTLSSocket 538 * the Socket to use if it needs to use start TLS. 539 * @param sslConnectionFactory 540 * the {@link SSLConnectionFactory} for creating SSL sockets 541 * @return a new {@link Socket} 542 * @throws SSLConnectionException 543 * if the SSL socket creation fails 544 * @throws LDAPConnectionException 545 * if any other error occurs 546 */ 547 private Socket createSSLOrBasicSocket(Socket startTLSSocket, 548 SSLConnectionFactory sslConnectionFactory) throws SSLConnectionException, 549 LDAPConnectionException 550 { 551 if (sslConnectionFactory == null) 552 { 553 return createSocket(); 554 } 555 else if (!connectionOptions.useStartTLS()) 556 { 557 return createSSLSocket(sslConnectionFactory); 558 } 559 else 560 { 561 try 562 { 563 // Use existing socket. 564 return sslConnectionFactory.createSocket(startTLSSocket, hostName, 565 portNumber, true); 566 } 567 catch (IOException e) 568 { 569 LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get(); 570 throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null, 571 e); 572 } 573 } 574 } 575 576 /** 577 * Close the underlying ASN1 reader and writer, optionally sending an unbind 578 * request before disconnecting. 579 * 580 * @param nextMessageID The message ID counter that should be used for 581 * the unbind request, or {@code null} if the 582 * connection should be closed without an unbind 583 * request. 584 */ 585 public void close(AtomicInteger nextMessageID) 586 { 587 if(ldapWriter != null) 588 { 589 if (nextMessageID != null) 590 { 591 try 592 { 593 LDAPMessage message = new LDAPMessage(nextMessageID.getAndIncrement(), 594 new UnbindRequestProtocolOp()); 595 ldapWriter.writeMessage(message); 596 } catch (Exception e) {} 597 } 598 599 ldapWriter.close(); 600 } 601 if(ldapReader != null) 602 { 603 ldapReader.close(); 604 } 605 } 606 607 /** 608 * Get the underlying LDAP writer. 609 * 610 * @return The underlying LDAP writer. 611 */ 612 public LDAPWriter getLDAPWriter() 613 { 614 return ldapWriter; 615 } 616 617 /** 618 * Get the underlying LDAP reader. 619 * 620 * @return The underlying LDAP reader. 621 */ 622 public LDAPReader getLDAPReader() 623 { 624 return ldapReader; 625 } 626}