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