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