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-2015 ForgeRock AS.
016 */
017package org.opends.server.tools;
018import java.io.IOException;
019import java.io.PrintStream;
020import java.net.ConnectException;
021import java.net.InetAddress;
022import java.net.Socket;
023import java.net.SocketException;
024import java.net.UnknownHostException;
025import java.util.ArrayList;
026import java.util.concurrent.atomic.AtomicInteger;
027import java.util.logging.Level;
028
029import org.forgerock.i18n.LocalizableMessage;
030import org.opends.server.controls.AuthorizationIdentityResponseControl;
031import org.opends.server.controls.PasswordExpiringControl;
032import org.opends.server.controls.PasswordPolicyErrorType;
033import org.opends.server.controls.PasswordPolicyResponseControl;
034import org.opends.server.controls.PasswordPolicyWarningType;
035import org.opends.server.loggers.JDKLogging;
036import org.forgerock.i18n.slf4j.LocalizedLogger;
037import org.opends.server.protocols.ldap.ExtendedRequestProtocolOp;
038import org.opends.server.protocols.ldap.ExtendedResponseProtocolOp;
039import org.opends.server.protocols.ldap.LDAPControl;
040import org.opends.server.protocols.ldap.LDAPMessage;
041import org.opends.server.protocols.ldap.UnbindRequestProtocolOp;
042import org.forgerock.opendj.ldap.ByteString;
043import org.opends.server.types.Control;
044import org.opends.server.types.DirectoryException;
045import org.opends.server.types.LDAPException;
046
047import com.forgerock.opendj.cli.ClientException;
048import static org.opends.messages.CoreMessages.*;
049import static org.opends.messages.ToolMessages.*;
050import static org.opends.server.protocols.ldap.LDAPResultCode.*;
051import static org.opends.server.util.ServerConstants.*;
052import static org.opends.server.util.StaticUtils.*;
053
054
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));
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.enableConsoleLoggingForOpenDJ(Level.ALL);
178    }
179    else
180    {
181      JDKLogging.disableLogging();
182    }
183
184
185    if(connectionOptions.useStartTLS())
186    {
187      try
188      {
189        startTLSSocket = createSocket();
190        ldapWriter = new LDAPWriter(startTLSSocket);
191        ldapReader = new LDAPReader(startTLSSocket);
192      }
193      catch (LDAPConnectionException e)
194      {
195        throw e;
196      }
197      catch (Exception ex)
198      {
199        logger.traceException(ex);
200        throw new LDAPConnectionException(LocalizableMessage.raw(ex.getMessage()), ex);
201      }
202
203      // Send the StartTLS extended request.
204      ExtendedRequestProtocolOp extendedRequest =
205           new ExtendedRequestProtocolOp(OID_START_TLS_REQUEST);
206
207      LDAPMessage msg = new LDAPMessage(nextMessageID.getAndIncrement(),
208                                        extendedRequest);
209      try
210      {
211        ldapWriter.writeMessage(msg);
212
213        // Read the response from the server.
214        msg = ldapReader.readMessage();
215      }catch (LDAPException ex1)
216      {
217        logger.traceException(ex1);
218        throw new LDAPConnectionException(LocalizableMessage.raw(ex1.getMessage()), ex1
219            .getResultCode(), null, ex1);
220      } catch (Exception ex1)
221      {
222        logger.traceException(ex1);
223        throw new LDAPConnectionException(LocalizableMessage.raw(ex1.getMessage()), ex1);
224      }
225      ExtendedResponseProtocolOp res = msg.getExtendedResponseProtocolOp();
226      resultCode = res.getResultCode();
227      if(resultCode != SUCCESS)
228      {
229        throw new LDAPConnectionException(res.getErrorMessage(),
230                                          resultCode,
231                                          res.getErrorMessage(),
232                                          res.getMatchedDN(), null);
233      }
234    }
235    SSLConnectionFactory sslConnectionFactory =
236                         connectionOptions.getSSLConnectionFactory();
237    try
238    {
239      socket = createSSLOrBasicSocket(startTLSSocket, sslConnectionFactory);
240      ldapWriter = new LDAPWriter(socket);
241      ldapReader = new LDAPReader(socket);
242    } catch(UnknownHostException | ConnectException e)
243    {
244      LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get();
245      throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null, e);
246    } catch (LDAPConnectionException e)
247    {
248      throw e;
249    } catch(Exception ex2)
250    {
251      logger.traceException(ex2);
252      throw new LDAPConnectionException(LocalizableMessage.raw(ex2.getMessage()), ex2);
253    }
254
255    // We need this so that we don't run out of addresses when the tool
256    // commands are called A LOT, as in the unit tests.
257    try
258    {
259      socket.setSoLinger(true, 1);
260      socket.setReuseAddress(true);
261      if (timeout > 0)
262      {
263        socket.setSoTimeout(timeout);
264      }
265    } catch(IOException e)
266    {
267      logger.traceException(e);
268      // It doesn't matter too much if this throws, so ignore it.
269    }
270
271    if (connectionOptions.getReportAuthzID())
272    {
273      requestControls.add(new LDAPControl(OID_AUTHZID_REQUEST));
274    }
275
276    if (connectionOptions.usePasswordPolicyControl())
277    {
278      requestControls.add(new LDAPControl(OID_PASSWORD_POLICY_CONTROL));
279    }
280
281    LDAPAuthenticationHandler handler = new LDAPAuthenticationHandler(
282         ldapReader, ldapWriter, hostName, nextMessageID);
283    try
284    {
285      ByteString bindDNBytes;
286      if(bindDN == null)
287      {
288        bindDNBytes = ByteString.empty();
289      }
290      else
291      {
292        bindDNBytes = ByteString.valueOfUtf8(bindDN);
293      }
294
295      ByteString bindPW;
296      if (bindPassword == null)
297      {
298        bindPW =  null;
299      }
300      else
301      {
302        bindPW = ByteString.valueOfUtf8(bindPassword);
303      }
304
305      String result = null;
306      if (connectionOptions.useSASLExternal())
307      {
308        result = handler.doSASLExternal(bindDNBytes,
309                                        connectionOptions.getSASLProperties(),
310                                        requestControls, responseControls);
311      }
312      else if (connectionOptions.getSASLMechanism() != null)
313      {
314            result = handler.doSASLBind(bindDNBytes, bindPW,
315            connectionOptions.getSASLMechanism(),
316            connectionOptions.getSASLProperties(),
317            requestControls, responseControls);
318      }
319      else if(bindDN != null)
320      {
321              result = handler.doSimpleBind(versionNumber, bindDNBytes, bindPW,
322              requestControls, responseControls);
323      }
324      if(result != null)
325      {
326        out.println(result);
327      }
328
329      for (Control c : responseControls)
330      {
331        if (c.getOID().equals(OID_AUTHZID_RESPONSE))
332        {
333          AuthorizationIdentityResponseControl control;
334          if (c instanceof LDAPControl)
335          {
336            // We have to decode this control.
337            control = AuthorizationIdentityResponseControl.DECODER.decode(c
338                .isCritical(), ((LDAPControl) c).getValue());
339          }
340          else
341          {
342            // Control should already have been decoded.
343            control = (AuthorizationIdentityResponseControl)c;
344          }
345
346          LocalizableMessage message =
347              INFO_BIND_AUTHZID_RETURNED.get(
348                  control.getAuthorizationID());
349          out.println(message);
350        }
351        else if (c.getOID().equals(OID_NS_PASSWORD_EXPIRED))
352        {
353          LocalizableMessage message = INFO_BIND_PASSWORD_EXPIRED.get();
354          out.println(message);
355        }
356        else if (c.getOID().equals(OID_NS_PASSWORD_EXPIRING))
357        {
358          PasswordExpiringControl control;
359          if(c instanceof LDAPControl)
360          {
361            // We have to decode this control.
362            control = PasswordExpiringControl.DECODER.decode(c.isCritical(),
363                ((LDAPControl) c).getValue());
364          }
365          else
366          {
367            // Control should already have been decoded.
368            control = (PasswordExpiringControl)c;
369          }
370          LocalizableMessage timeString =
371               secondsToTimeString(control.getSecondsUntilExpiration());
372
373
374          LocalizableMessage message = INFO_BIND_PASSWORD_EXPIRING.get(timeString);
375          out.println(message);
376        }
377        else if (c.getOID().equals(OID_PASSWORD_POLICY_CONTROL))
378        {
379          PasswordPolicyResponseControl pwPolicyControl;
380          if(c instanceof LDAPControl)
381          {
382            pwPolicyControl = PasswordPolicyResponseControl.DECODER.decode(c
383                .isCritical(), ((LDAPControl) c).getValue());
384          }
385          else
386          {
387            pwPolicyControl = (PasswordPolicyResponseControl)c;
388          }
389
390
391          PasswordPolicyErrorType errorType = pwPolicyControl.getErrorType();
392          if (errorType != null)
393          {
394            switch (errorType)
395            {
396              case PASSWORD_EXPIRED:
397
398                LocalizableMessage message = INFO_BIND_PASSWORD_EXPIRED.get();
399                out.println(message);
400                break;
401              case ACCOUNT_LOCKED:
402
403                message = INFO_BIND_ACCOUNT_LOCKED.get();
404                out.println(message);
405                break;
406              case CHANGE_AFTER_RESET:
407
408                message = INFO_BIND_MUST_CHANGE_PASSWORD.get();
409                out.println(message);
410                break;
411            }
412          }
413
414          PasswordPolicyWarningType warningType =
415               pwPolicyControl.getWarningType();
416          if (warningType != null)
417          {
418            switch (warningType)
419            {
420              case TIME_BEFORE_EXPIRATION:
421                LocalizableMessage timeString =
422                     secondsToTimeString(pwPolicyControl.getWarningValue());
423
424
425                LocalizableMessage message = INFO_BIND_PASSWORD_EXPIRING.get(timeString);
426                out.println(message);
427                break;
428              case GRACE_LOGINS_REMAINING:
429
430                message = INFO_BIND_GRACE_LOGINS_REMAINING.get(
431                        pwPolicyControl.getWarningValue());
432                out.println(message);
433                break;
434            }
435          }
436        }
437      }
438    } catch(ClientException ce)
439    {
440      logger.traceException(ce);
441      throw new LDAPConnectionException(ce.getMessageObject(), ce.getReturnCode(),
442                                        null, ce);
443    } catch (LDAPException le) {
444        throw new LDAPConnectionException(le.getMessageObject(),
445                le.getResultCode(),
446                le.getErrorMessage(),
447                le.getMatchedDN(),
448                le.getCause());
449    } catch (DirectoryException de)
450    {
451      throw new LDAPConnectionException(de.getMessageObject(),
452          de.getResultCode().intValue(), null, de.getMatchedDN(), de.getCause());
453    } catch(Exception ex)
454    {
455      logger.traceException(ex);
456      throw new LDAPConnectionException(
457              LocalizableMessage.raw(ex.getLocalizedMessage()),ex);
458    }
459    finally
460    {
461      if (timeout > 0)
462      {
463        try
464        {
465          socket.setSoTimeout(0);
466        }
467        catch (SocketException e)
468        {
469          e.printStackTrace();
470          logger.traceException(e);
471        }
472      }
473    }
474
475  }
476
477  /**
478   * Creates a socket using the hostName and portNumber encapsulated in the
479   * current object. For each IP address associated to this host name,
480   * createSocket() will try to open a socket and it will return the first
481   * socket for which we successfully establish a connection.
482   * <p>
483   * This method can never return null because it will receive
484   * UnknownHostException before and then throw LDAPConnectionException.
485   * </p>
486   *
487   * @return a new {@link Socket}.
488   * @throws LDAPConnectionException
489   *           if any exception occurs including UnknownHostException
490   */
491  private Socket createSocket() throws LDAPConnectionException
492  {
493    ConnectException ce = null;
494    try
495    {
496      for (InetAddress inetAddress : InetAddress.getAllByName(hostName))
497      {
498        try
499        {
500          return new Socket(inetAddress, portNumber);
501        }
502        catch (ConnectException ce2)
503        {
504          if (ce == null)
505          {
506            ce = ce2;
507          }
508        }
509      }
510    }
511    catch (UnknownHostException uhe)
512    {
513      LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get();
514      throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null,
515          uhe);
516    }
517    catch (Exception ex)
518    {
519      // if we get there, something went awfully wrong while creatng one socket,
520      // no need to continue the for loop.
521      logger.traceException(ex);
522      throw new LDAPConnectionException(LocalizableMessage.raw(ex.getMessage()), ex);
523    }
524    if (ce != null)
525    {
526      LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get();
527      throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null,
528          ce);
529    }
530    return null;
531  }
532
533  /**
534   * Creates an SSL socket using the hostName and portNumber encapsulated in the
535   * current object. For each IP address associated to this host name,
536   * createSSLSocket() will try to open a socket and it will return the first
537   * socket for which we successfully establish a connection.
538   * <p>
539   * This method can never return null because it will receive
540   * UnknownHostException before and then throw LDAPConnectionException.
541   * </p>
542   *
543   * @return a new {@link Socket}.
544   * @throws LDAPConnectionException
545   *           if any exception occurs including UnknownHostException
546   */
547  private Socket createSSLSocket(SSLConnectionFactory sslConnectionFactory)
548      throws SSLConnectionException, LDAPConnectionException
549  {
550    ConnectException ce = null;
551    try
552    {
553      for (InetAddress inetAddress : InetAddress.getAllByName(hostName))
554      {
555        try
556        {
557          return sslConnectionFactory.createSocket(inetAddress, portNumber);
558        }
559        catch (ConnectException ce2)
560        {
561          if (ce == null)
562          {
563            ce = ce2;
564          }
565        }
566      }
567    }
568    catch (UnknownHostException uhe)
569    {
570      LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get();
571      throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null,
572          uhe);
573    }
574    catch (Exception ex)
575    {
576      // if we get there, something went awfully wrong while creatng one socket,
577      // no need to continue the for loop.
578      logger.traceException(ex);
579      throw new LDAPConnectionException(LocalizableMessage.raw(ex.getMessage()), ex);
580    }
581    if (ce != null)
582    {
583      LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get();
584      throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null,
585          ce);
586    }
587    return null;
588  }
589
590  /**
591   * Creates an SSL socket or a normal/basic socket using the hostName and
592   * portNumber encapsulated in the current object, or with the passed in socket
593   * if it needs to use start TLS.
594   *
595   * @param startTLSSocket
596   *          the Socket to use if it needs to use start TLS.
597   * @param sslConnectionFactory
598   *          the {@link SSLConnectionFactory} for creating SSL sockets
599   * @return a new {@link Socket}
600   * @throws SSLConnectionException
601   *           if the SSL socket creation fails
602   * @throws LDAPConnectionException
603   *           if any other error occurs
604   */
605  private Socket createSSLOrBasicSocket(Socket startTLSSocket,
606      SSLConnectionFactory sslConnectionFactory) throws SSLConnectionException,
607      LDAPConnectionException
608  {
609    if (sslConnectionFactory == null)
610    {
611      return createSocket();
612    }
613    else if (!connectionOptions.useStartTLS())
614    {
615      return createSSLSocket(sslConnectionFactory);
616    }
617    else
618    {
619      try
620      {
621        // Use existing socket.
622        return sslConnectionFactory.createSocket(startTLSSocket, hostName,
623            portNumber, true);
624      }
625      catch (IOException e)
626      {
627        LocalizableMessage msg = INFO_RESULT_CLIENT_SIDE_CONNECT_ERROR.get();
628        throw new LDAPConnectionException(msg, CLIENT_SIDE_CONNECT_ERROR, null,
629            e);
630      }
631    }
632  }
633
634  /**
635   * Close the underlying ASN1 reader and writer, optionally sending an unbind
636   * request before disconnecting.
637   *
638   * @param  nextMessageID  The message ID counter that should be used for
639   *                        the unbind request, or {@code null} if the
640   *                        connection should be closed without an unbind
641   *                        request.
642   */
643  public void close(AtomicInteger nextMessageID)
644  {
645    if(ldapWriter != null)
646    {
647      if (nextMessageID != null)
648      {
649        try
650        {
651          LDAPMessage message = new LDAPMessage(nextMessageID.getAndIncrement(),
652                                                new UnbindRequestProtocolOp());
653          ldapWriter.writeMessage(message);
654        } catch (Exception e) {}
655      }
656
657      ldapWriter.close();
658    }
659    if(ldapReader != null)
660    {
661      ldapReader.close();
662    }
663  }
664
665  /**
666   * Get the underlying LDAP writer.
667   *
668   * @return  The underlying LDAP writer.
669   */
670  public LDAPWriter getLDAPWriter()
671  {
672    return ldapWriter;
673  }
674
675  /**
676   * Get the underlying LDAP reader.
677   *
678   * @return  The underlying LDAP reader.
679   */
680  public LDAPReader getLDAPReader()
681  {
682    return ldapReader;
683  }
684
685}
686