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}