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 2008-2010 Sun Microsystems, Inc.
025 *      Portions Copyright 2011-2015 ForgeRock AS
026 */
027package org.opends.server.util.args;
028
029import static org.opends.messages.ToolMessages.*;
030
031import static com.forgerock.opendj.cli.Utils.*;
032
033import java.io.PrintStream;
034import java.util.LinkedList;
035import java.util.Set;
036import java.util.concurrent.atomic.AtomicInteger;
037
038import javax.net.ssl.SSLException;
039
040import org.forgerock.i18n.LocalizableMessage;
041import org.opends.server.admin.client.cli.SecureConnectionCliArgs;
042import org.opends.server.core.DirectoryServer.DirectoryServerVersionHandler;
043import org.opends.server.tools.LDAPConnection;
044import org.opends.server.tools.LDAPConnectionException;
045import org.opends.server.tools.LDAPConnectionOptions;
046import org.opends.server.tools.SSLConnectionException;
047import org.opends.server.tools.SSLConnectionFactory;
048import org.opends.server.types.OpenDsException;
049import org.opends.server.util.cli.LDAPConnectionConsoleInteraction;
050
051import com.forgerock.opendj.cli.Argument;
052import com.forgerock.opendj.cli.ArgumentException;
053import com.forgerock.opendj.cli.ArgumentGroup;
054import com.forgerock.opendj.cli.ArgumentParser;
055import com.forgerock.opendj.cli.ClientException;
056import com.forgerock.opendj.cli.ConsoleApplication;
057import com.forgerock.opendj.cli.FileBasedArgument;
058import com.forgerock.opendj.cli.StringArgument;
059
060/**
061 * Creates an argument parser pre-populated with arguments for specifying
062 * information for opening and LDAPConnection an LDAP connection.
063 */
064public class LDAPConnectionArgumentParser extends ArgumentParser
065{
066
067  private SecureConnectionCliArgs args;
068
069  /**
070   * Creates a new instance of this argument parser with no arguments. Unnamed
071   * trailing arguments will not be allowed.
072   *
073   * @param mainClassName
074   *          The fully-qualified name of the Java class that should be invoked
075   *          to launch the program with which this argument parser is
076   *          associated.
077   * @param toolDescription
078   *          A human-readable description for the tool, which will be included
079   *          when displaying usage information.
080   * @param longArgumentsCaseSensitive
081   *          Indicates whether long arguments should
082   * @param argumentGroup
083   *          Group to which LDAP arguments will be added to the parser. May be
084   *          null to indicate that arguments should be added to the default
085   *          group
086   * @param alwaysSSL
087   *          If true, always use the SSL connection type. In this case, the
088   *          arguments useSSL and startTLS are not present.
089   */
090  public LDAPConnectionArgumentParser(String mainClassName, LocalizableMessage toolDescription,
091      boolean longArgumentsCaseSensitive, ArgumentGroup argumentGroup, boolean alwaysSSL)
092  {
093    super(mainClassName, toolDescription, longArgumentsCaseSensitive);
094    addLdapConnectionArguments(argumentGroup, alwaysSSL);
095    setVersionHandler(new DirectoryServerVersionHandler());
096  }
097
098  /**
099   * Indicates whether or not the user has indicated that they would like to
100   * perform a remote operation based on the arguments.
101   *
102   * @return true if the user wants to perform a remote operation; false
103   *         otherwise
104   */
105  public boolean connectionArgumentsPresent()
106  {
107    return args != null && args.argumentsPresent();
108  }
109
110  /**
111   * Creates a new LDAPConnection and invokes a connect operation using
112   * information provided in the parsed set of arguments that were provided by
113   * the user.
114   *
115   * @param out
116   *          stream to write messages
117   * @param err
118   *          stream to write error messages
119   * @return LDAPConnection created by this class from parsed arguments
120   * @throws LDAPConnectionException
121   *           if there was a problem connecting to the server indicated by the
122   *           input arguments
123   * @throws ArgumentException
124   *           if there was a problem processing the input arguments
125   */
126  public LDAPConnection connect(PrintStream out, PrintStream err) throws LDAPConnectionException, ArgumentException
127  {
128    return connect(this.args, out, err);
129  }
130
131  /**
132   * Creates a new LDAPConnection and invokes a connect operation using
133   * information provided in the parsed set of arguments that were provided by
134   * the user.
135   *
136   * @param args
137   *          with which to connect
138   * @param out
139   *          stream to write messages
140   * @param err
141   *          stream to write error messages
142   * @return LDAPConnection created by this class from parsed arguments
143   * @throws LDAPConnectionException
144   *           if there was a problem connecting to the server indicated by the
145   *           input arguments
146   * @throws ArgumentException
147   *           if there was a problem processing the input arguments
148   */
149  private LDAPConnection connect(SecureConnectionCliArgs args, PrintStream out, PrintStream err)
150      throws LDAPConnectionException, ArgumentException
151  {
152    // If both a bind password and bind password file were provided, then return
153    // an error.
154    if (args.bindPasswordArg.isPresent() && args.bindPasswordFileArg.isPresent())
155    {
156      printAndThrowException(err, ERR_LDAP_CONN_MUTUALLY_EXCLUSIVE_ARGUMENTS.get(
157          args.bindPasswordArg.getLongIdentifier(), args.bindPasswordFileArg.getLongIdentifier()));
158    }
159
160    // If both a key store password and key store password file were provided,
161    // then return an error.
162    if (args.keyStorePasswordArg.isPresent() && args.keyStorePasswordFileArg.isPresent())
163    {
164      printAndThrowException(err, ERR_LDAP_CONN_MUTUALLY_EXCLUSIVE_ARGUMENTS.get(
165          args.keyStorePasswordArg.getLongIdentifier(), args.keyStorePasswordFileArg.getLongIdentifier()));
166    }
167
168    // If both a trust store password and trust store password file were
169    // provided, then return an error.
170    if (args.trustStorePasswordArg.isPresent() && args.trustStorePasswordFileArg.isPresent())
171    {
172      printAndThrowException(err, ERR_LDAP_CONN_MUTUALLY_EXCLUSIVE_ARGUMENTS.get(
173          args.trustStorePasswordArg.getLongIdentifier(), args.trustStorePasswordFileArg.getLongIdentifier()));
174    }
175
176    // Create the LDAP connection options object, which will be used to
177    // customize the way that we connect to the server and specify a set of
178    // basic defaults.
179    LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions();
180    connectionOptions.setVersionNumber(3);
181
182    // See if we should use SSL or StartTLS when establishing the connection.
183    // If so, then make sure only one of them was specified.
184    if (args.useSSLArg.isPresent())
185    {
186      if (args.useStartTLSArg.isPresent())
187      {
188        printAndThrowException(err, ERR_LDAP_CONN_MUTUALLY_EXCLUSIVE_ARGUMENTS.get(
189            args.useSSLArg.getLongIdentifier(), args.useSSLArg.getLongIdentifier()));
190      }
191      connectionOptions.setUseSSL(true);
192    }
193    else if (args.useStartTLSArg.isPresent())
194    {
195      connectionOptions.setStartTLS(true);
196    }
197
198    // If we should blindly trust any certificate, then install the appropriate
199    // SSL connection factory.
200    if (args.useSSLArg.isPresent() || args.useStartTLSArg.isPresent())
201    {
202      try
203      {
204        String clientAlias;
205        if (args.certNicknameArg.isPresent())
206        {
207          clientAlias = args.certNicknameArg.getValue();
208        }
209        else
210        {
211          clientAlias = null;
212        }
213
214        SSLConnectionFactory sslConnectionFactory = new SSLConnectionFactory();
215        sslConnectionFactory.init(args.trustAllArg.isPresent(),
216                                  args.keyStorePathArg.getValue(),
217                                  args.keyStorePasswordArg.getValue(),
218                                  clientAlias,
219                                  args.trustStorePathArg.getValue(),
220                                  args.trustStorePasswordArg.getValue());
221        connectionOptions.setSSLConnectionFactory(sslConnectionFactory);
222      }
223      catch (SSLConnectionException sce)
224      {
225        printWrappedText(err, ERR_LDAP_CONN_CANNOT_INITIALIZE_SSL.get(sce.getMessage()));
226      }
227    }
228
229    // If one or more SASL options were provided, then make sure that one of
230    // them was "mech" and specified a valid SASL mechanism.
231    if (args.saslOptionArg.isPresent())
232    {
233      String mechanism = null;
234      LinkedList<String> options = new LinkedList<>();
235
236      for (String s : args.saslOptionArg.getValues())
237      {
238        int equalPos = s.indexOf('=');
239        if (equalPos <= 0)
240        {
241          printAndThrowException(err, ERR_LDAP_CONN_CANNOT_PARSE_SASL_OPTION.get(s));
242        }
243        else
244        {
245          String name = s.substring(0, equalPos);
246          if ("mech".equalsIgnoreCase(name))
247          {
248            mechanism = s;
249          }
250          else
251          {
252            options.add(s);
253          }
254        }
255      }
256
257      if (mechanism == null)
258      {
259        printAndThrowException(err, ERR_LDAP_CONN_NO_SASL_MECHANISM.get());
260      }
261
262      connectionOptions.setSASLMechanism(mechanism);
263      for (String option : options)
264      {
265        connectionOptions.addSASLProperty(option);
266      }
267    }
268
269    int timeout = args.connectTimeoutArg.getIntValue();
270
271    final String passwordValue = getPasswordValue(
272        args.bindPasswordArg, args.bindPasswordFileArg, args.bindDnArg, out, err);
273    return connect(
274            args.hostNameArg.getValue(),
275            args.portArg.getIntValue(),
276            args.bindDnArg.getValue(),
277            passwordValue,
278            connectionOptions, timeout, out, err);
279  }
280
281  private void printAndThrowException(PrintStream err, LocalizableMessage message) throws ArgumentException
282  {
283    printWrappedText(err, message);
284    throw new ArgumentException(message);
285  }
286
287  /**
288   * Creates a connection using a console interaction that will be used to
289   * potentially interact with the user to prompt for necessary information for
290   * establishing the connection.
291   *
292   * @param ui
293   *          user interaction for prompting the user
294   * @param out
295   *          stream to write messages
296   * @param err
297   *          stream to write error messages
298   * @return LDAPConnection created by this class from parsed arguments
299   * @throws LDAPConnectionException
300   *           if there was a problem connecting to the server
301   * @throws ArgumentException
302   *           if there was a problem indicated by the input arguments
303   */
304  public LDAPConnection connect(LDAPConnectionConsoleInteraction ui, PrintStream out, PrintStream err)
305      throws LDAPConnectionException, ArgumentException
306  {
307    try
308    {
309      ui.run();
310      LDAPConnectionOptions options = new LDAPConnectionOptions();
311      options.setVersionNumber(3);
312      return connect(ui.getHostName(), ui.getPortNumber(), ui.getBindDN(),
313          ui.getBindPassword(), ui.populateLDAPOptions(options), ui.getConnectTimeout(), out, err);
314    }
315    catch (OpenDsException e)
316    {
317      err.println(isSSLException(e) ?
318          ERR_TASKINFO_LDAP_EXCEPTION_SSL.get(ui.getHostName(), ui.getPortNumber()) : e.getMessageObject());
319      return null;
320    }
321  }
322
323  private boolean isSSLException(Exception e)
324  {
325    return e.getCause() != null
326        && e.getCause().getCause() != null
327        && e.getCause().getCause() instanceof SSLException;
328  }
329
330  /**
331   * Creates a connection from information provided.
332   *
333   * @param host
334   *          of the server
335   * @param port
336   *          of the server
337   * @param bindDN
338   *          with which to connect
339   * @param bindPw
340   *          with which to connect
341   * @param options
342   *          with which to connect
343   * @param out
344   *          stream to write messages
345   * @param err
346   *          stream to write error messages
347   * @return LDAPConnection created by this class from parsed arguments
348   * @throws LDAPConnectionException
349   *           if there was a problem connecting to the server indicated by the
350   *           input arguments
351   */
352  public LDAPConnection connect(String host, int port, String bindDN, String bindPw, LDAPConnectionOptions options,
353      PrintStream out, PrintStream err) throws LDAPConnectionException
354  {
355    return connect(host, port, bindDN, bindPw, options, 0, out, err);
356  }
357
358  /**
359   * Creates a connection from information provided.
360   *
361   * @param host
362   *          of the server
363   * @param port
364   *          of the server
365   * @param bindDN
366   *          with which to connect
367   * @param bindPw
368   *          with which to connect
369   * @param options
370   *          with which to connect
371   * @param timeout
372   *          the timeout to establish the connection in milliseconds. Use
373   *          {@code 0} to express no timeout
374   * @param out
375   *          stream to write messages
376   * @param err
377   *          stream to write error messages
378   * @return LDAPConnection created by this class from parsed arguments
379   * @throws LDAPConnectionException
380   *           if there was a problem connecting to the server indicated by the
381   *           input arguments
382   */
383  public LDAPConnection connect(String host, int port, String bindDN, String bindPw, LDAPConnectionOptions options,
384      int timeout, PrintStream out, PrintStream err) throws LDAPConnectionException
385  {
386    // Attempt to connect and authenticate to the Directory Server.
387    AtomicInteger nextMessageID = new AtomicInteger(1);
388    LDAPConnection connection = new LDAPConnection(host, port, options, out, err);
389    connection.connectToHost(bindDN, bindPw, nextMessageID, timeout);
390    return connection;
391  }
392
393  /**
394   * Gets the arguments associated with this parser.
395   *
396   * @return arguments for this parser.
397   */
398  public SecureConnectionCliArgs getArguments()
399  {
400    return args;
401  }
402
403  /**
404   * Commodity method that retrieves the password value analyzing the contents
405   * of a string argument and of a file based argument. It assumes that the
406   * arguments have already been parsed and validated. If the string is a dash,
407   * or no password is available, it will prompt for it on the command line.
408   *
409   * @param bindPwdArg
410   *          the string argument for the password.
411   * @param bindPwdFileArg
412   *          the file based argument for the password.
413   * @param bindDnArg
414   *          the string argument for the bindDN.
415   * @param out
416   *          stream to write message.
417   * @param err
418   *          stream to write error message.
419   * @return the password value.
420   */
421  public static String getPasswordValue(StringArgument bindPwdArg, FileBasedArgument bindPwdFileArg,
422      StringArgument bindDnArg, PrintStream out, PrintStream err)
423  {
424    try
425    {
426      return getPasswordValue(bindPwdArg, bindPwdFileArg, bindDnArg.getValue(), out, err);
427    }
428    catch (Exception ex)
429    {
430      printWrappedText(err, ex.getMessage());
431      return null;
432    }
433  }
434
435  /**
436   * Commodity method that retrieves the password value analyzing the contents
437   * of a string argument and of a file based argument. It assumes that the
438   * arguments have already been parsed and validated. If the string is a dash,
439   * or no password is available, it will prompt for it on the command line.
440   *
441   * @param bindPassword
442   *          the string argument for the password.
443   * @param bindPasswordFile
444   *          the file based argument for the password.
445   * @param bindDNValue
446   *          the string value for the bindDN.
447   * @param out
448   *          stream to write message.
449   * @param err
450   *          stream to write error message.
451   * @return the password value.
452   * @throws ClientException
453   *           if the password cannot be read
454   */
455  public static String getPasswordValue(StringArgument bindPassword, FileBasedArgument bindPasswordFile,
456      String bindDNValue, PrintStream out, PrintStream err) throws ClientException
457  {
458    String bindPasswordValue = bindPassword.getValue();
459    if ("-".equals(bindPasswordValue)
460        || (!bindPasswordFile.isPresent() && bindDNValue != null && bindPasswordValue == null))
461    {
462      // read the password from the stdin.
463      out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(bindDNValue));
464      char[] pwChars = ConsoleApplication.readPassword();
465      // As per rfc 4513(section-5.1.2) a client should avoid sending
466      // an empty password to the server.
467      while (pwChars.length == 0)
468      {
469        printWrappedText(err, INFO_LDAPAUTH_NON_EMPTY_PASSWORD.get());
470        out.print(INFO_LDAPAUTH_PASSWORD_PROMPT.get(bindDNValue));
471        pwChars = ConsoleApplication.readPassword();
472      }
473      return new String(pwChars);
474    }
475    else if (bindPasswordValue == null)
476    {
477      // Read from file if it exists.
478      return bindPasswordFile.getValue();
479    }
480    return bindPasswordValue;
481  }
482
483  private void addLdapConnectionArguments(ArgumentGroup argGroup, boolean alwaysSSL)
484  {
485    args = new SecureConnectionCliArgs(alwaysSSL);
486    try
487    {
488      Set<Argument> argSet = args.createGlobalArguments();
489      for (Argument arg : argSet)
490      {
491        addArgument(arg, argGroup);
492      }
493    }
494    catch (ArgumentException ae)
495    {
496      ae.printStackTrace(); // Should never happen
497    }
498  }
499}