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