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.portValidationCallback;
020import static com.forgerock.opendj.cli.Utils.isDN;
021import static com.forgerock.opendj.cli.Utils.getAdministratorDN;
022import static com.forgerock.opendj.cli.Utils.getThrowableMsg;
023import static org.opends.messages.ToolMessages.*;
024
025import java.io.File;
026import java.io.FileInputStream;
027import java.io.FileNotFoundException;
028import java.io.FileOutputStream;
029import java.net.InetAddress;
030import java.net.URI;
031import java.net.URISyntaxException;
032import java.net.UnknownHostException;
033import java.security.KeyStore;
034import java.security.KeyStoreException;
035import java.security.cert.X509Certificate;
036import java.util.Enumeration;
037import java.util.LinkedHashMap;
038import java.util.Map;
039
040import javax.net.ssl.KeyManager;
041
042import com.forgerock.opendj.cli.Argument;
043import com.forgerock.opendj.cli.FileBasedArgument;
044import org.forgerock.i18n.LocalizableMessage;
045import org.forgerock.i18n.slf4j.LocalizedLogger;
046import org.opends.admin.ads.util.ApplicationKeyManager;
047import org.opends.admin.ads.util.ApplicationTrustManager;
048import org.opends.server.admin.client.cli.SecureConnectionCliArgs;
049import org.opends.server.tools.LDAPConnectionOptions;
050import org.opends.server.tools.SSLConnectionException;
051import org.opends.server.tools.SSLConnectionFactory;
052import org.opends.server.util.CollectionUtils;
053import org.opends.server.util.SelectableCertificateKeyManager;
054
055import com.forgerock.opendj.cli.ArgumentException;
056import com.forgerock.opendj.cli.ClientException;
057import com.forgerock.opendj.cli.CommandBuilder;
058import com.forgerock.opendj.cli.ConsoleApplication;
059import com.forgerock.opendj.cli.Menu;
060import com.forgerock.opendj.cli.MenuBuilder;
061import com.forgerock.opendj.cli.MenuResult;
062import com.forgerock.opendj.cli.ValidationCallback;
063
064/**
065 * Supports interacting with a user through the command line to prompt for
066 * information necessary to create an LDAP connection.
067 *
068 * Actually the LDAPConnectionConsoleInteraction is used by UninstallCliHelper, StatusCli,
069 * LDAPManagementContextFactory and ReplicationCliMain.
070 */
071public class LDAPConnectionConsoleInteraction
072{
073
074  private static final Protocol DEFAULT_PROMPT_PROTOCOL = Protocol.SSL;
075  private static final TrustMethod DEFAULT_PROMPT_TRUST_METHOD = TrustMethod.DISPLAY_CERTIFICATE;
076  private static final TrustOption DEFAULT_PROMPT_TRUST_OPTION = TrustOption.SESSION;
077
078  private static final boolean ALLOW_EMPTY_PATH = true;
079  private static final boolean FILE_MUST_EXISTS = true;
080  private boolean allowAnonymousIfNonInteractive;
081
082  /**
083   * Information from the latest console interaction.
084   * TODO: should it extend MonoServerReplicationUserData or a subclass?
085   */
086  private static class State
087  {
088    private boolean useSSL;
089    private boolean useStartTLS;
090    private String hostName;
091    private String bindDN;
092    private String providedBindDN;
093    private String adminUID;
094    private String providedAdminUID;
095    private String bindPassword;
096    /** The timeout to be used to connect. */
097    private int connectTimeout;
098    /** Indicate if we need to display the heading. */
099    private boolean isHeadingDisplayed;
100
101    private ApplicationTrustManager trustManager;
102    /** Indicate if the trust store in in memory. */
103    private boolean trustStoreInMemory;
104    /** Indicate if the all certificates are accepted. */
105    private boolean trustAll;
106    /** Indicate that the trust manager was created with the parameters provided. */
107    private boolean trustManagerInitialized;
108    /** The trust store to use for the SSL or STARTTLS connection. */
109    private KeyStore truststore;
110    private String truststorePath;
111    private String truststorePassword;
112
113    private KeyManager keyManager;
114    private String keyStorePath;
115    private String keystorePassword;
116    private String certifNickname;
117
118    private State(SecureConnectionCliArgs secureArgs)
119    {
120      setSsl(secureArgs);
121      trustAll = secureArgs.getTrustAllArg().isPresent();
122    }
123
124    protected LocalizableMessage getPrompt()
125    {
126      if (providedAdminUID != null)
127      {
128        return INFO_LDAPAUTH_PASSWORD_PROMPT.get(providedAdminUID);
129      }
130      else if (providedBindDN != null)
131      {
132        return INFO_LDAPAUTH_PASSWORD_PROMPT.get(providedBindDN);
133      }
134      else if (bindDN != null)
135      {
136        return INFO_LDAPAUTH_PASSWORD_PROMPT.get(bindDN);
137      }
138
139      return INFO_LDAPAUTH_PASSWORD_PROMPT.get(adminUID);
140    }
141
142    protected String getAdminOrBindDN()
143    {
144      if (providedBindDN != null)
145      {
146        return providedBindDN;
147      }
148      else if (providedAdminUID != null)
149      {
150        return getAdministratorDN(providedAdminUID);
151      }
152      else if (bindDN != null)
153      {
154        return bindDN;
155      }
156      else if (adminUID != null)
157      {
158        return getAdministratorDN(adminUID);
159      }
160
161      return null;
162    }
163
164    private void setSsl(final SecureConnectionCliArgs secureArgs)
165    {
166      this.useSSL = secureArgs.alwaysSSL() || secureArgs.getUseSSLArg().isPresent();
167      this.useStartTLS = secureArgs.getUseStartTLSArg().isPresent();
168    }
169  }
170
171  /** The console application. */
172  private ConsoleApplication app;
173
174  private State state;
175
176  /** The SecureConnectionCliArgsList object. */
177  private final SecureConnectionCliArgs secureArgsList;
178
179  /** The command builder that we can return with the connection information. */
180  private CommandBuilder commandBuilder;
181
182  /** A copy of the secureArgList for convenience. */
183  private SecureConnectionCliArgs copySecureArgsList;
184
185  /**
186   * Boolean that tells if we must propose LDAP if it is available even if the
187   * user provided certificate parameters.
188   */
189  private boolean displayLdapIfSecureParameters;
190
191  private int portNumber;
192
193  private LocalizableMessage heading = INFO_LDAP_CONN_HEADING_CONNECTION_PARAMETERS.get();
194
195  /** Boolean that tells if we ask for bind DN or admin UID in the same prompt. */
196  private boolean useAdminOrBindDn;
197
198  /** Enumeration description protocols for interactive CLI choices. */
199  private enum Protocol
200  {
201    LDAP(INFO_LDAP_CONN_PROMPT_SECURITY_LDAP.get()),
202    SSL(INFO_LDAP_CONN_PROMPT_SECURITY_USE_SSL.get()),
203    START_TLS(INFO_LDAP_CONN_PROMPT_SECURITY_USE_START_TLS.get());
204
205    private final LocalizableMessage message;
206
207    Protocol(final LocalizableMessage message)
208    {
209      this.message = message;
210    }
211
212    private int getChoice()
213    {
214      return ordinal() + 1;
215    }
216  }
217
218  /** Enumeration description protocols for interactive CLI choices. */
219  private enum TrustMethod
220  {
221    TRUSTALL(INFO_LDAP_CONN_PROMPT_SECURITY_USE_TRUST_ALL.get()),
222    TRUSTSTORE(INFO_LDAP_CONN_PROMPT_SECURITY_TRUSTSTORE.get()),
223    DISPLAY_CERTIFICATE(INFO_LDAP_CONN_PROMPT_SECURITY_MANUAL_CHECK.get());
224
225    private LocalizableMessage message;
226
227    TrustMethod(final LocalizableMessage message)
228    {
229      this.message = message;
230    }
231
232    private int getChoice()
233    {
234      return ordinal() + 1;
235    }
236
237    private static TrustMethod getTrustMethodForIndex(final int value)
238    {
239      for (final TrustMethod trustMethod : TrustMethod.values())
240      {
241        if (trustMethod.getChoice() == value)
242        {
243          return trustMethod;
244        }
245      }
246      return null;
247    }
248  }
249
250  /** Enumeration description server certificate trust option. */
251  private enum TrustOption
252  {
253    UNTRUSTED(INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION_NO.get()),
254    SESSION(INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION_SESSION.get()),
255    PERMAMENT(INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION_ALWAYS.get()),
256    CERTIFICATE_DETAILS(INFO_LDAP_CONN_PROMPT_SECURITY_CERTIFICATE_DETAILS.get());
257
258    private LocalizableMessage message;
259
260    TrustOption(final LocalizableMessage message)
261    {
262      this.message = message;
263    }
264
265    private int getChoice()
266    {
267      return ordinal() + 1;
268    }
269
270    private static TrustOption getTrustOptionForIndex(final int value)
271    {
272      for (final TrustOption trustOption : TrustOption.values())
273      {
274        if (trustOption.getChoice() == value)
275        {
276          return trustOption;
277        }
278      }
279      return null;
280    }
281  }
282
283  /**
284   * Constructs a new console interaction.
285   *
286   * @param app
287   *          console application
288   * @param secureArgs
289   *          existing set of arguments that have already been parsed and
290   *          contain some potential command line specified LDAP arguments
291   */
292  public LDAPConnectionConsoleInteraction(ConsoleApplication app, SecureConnectionCliArgs secureArgs)
293  {
294    this(app, secureArgs, false);
295  }
296
297  /**
298   * Constructs a new console interaction.
299   *
300   * @param app
301   *          console application
302   * @param secureArgs
303   *          existing set of arguments that have already been parsed and
304   *          contain some potential command line specified LDAP arguments
305   * @param allowAnonymousIfNonInteractive
306   *          If this console interaction should allow anonymous user in non interactive mode.
307   *          If console application is interactive, the user will always be prompted for credentials.
308   */
309  public LDAPConnectionConsoleInteraction(
310      ConsoleApplication app, SecureConnectionCliArgs secureArgs, final boolean allowAnonymousIfNonInteractive)
311  {
312    this.app = app;
313    this.secureArgsList = secureArgs;
314    this.commandBuilder = new CommandBuilder();
315    this.allowAnonymousIfNonInteractive = allowAnonymousIfNonInteractive;
316    state = new State(secureArgs);
317    copySecureArgsList = new SecureConnectionCliArgs(secureArgs.alwaysSSL());
318    try
319    {
320      copySecureArgsList.createGlobalArguments();
321    }
322    catch (Throwable t)
323    {
324      // This is  a bug: we should always be able to create the global arguments
325      // no need to localize this one.
326      throw new RuntimeException("Unexpected error: " + t, t);
327    }
328  }
329
330  /**
331   * Interact with the user though the console to get information necessary to
332   * establish an LDAP connection.
333   *
334   * @throws ArgumentException
335   *           if there is a problem with the arguments
336   */
337  public void run() throws ArgumentException
338  {
339    run(true);
340  }
341
342  /**
343   * Interact with the user though the console to get information necessary to
344   * establish an LDAP connection.
345   *
346   * @param canUseStartTLS
347   *          whether we can propose to connect using Start TLS or not.
348   * @throws ArgumentException
349   *           if there is a problem with the arguments
350   */
351  public void run(boolean canUseStartTLS) throws ArgumentException
352  {
353    resetBeforeRun();
354    resolveHostName();
355    resolveConnectionType(canUseStartTLS);
356    resolvePortNumber();
357    resolveTrustAndKeyManagers();
358    resolveCredentialLogin();
359    resolveCredentialPassword();
360    resolveConnectTimeout();
361  }
362
363  private void resetBeforeRun() throws ArgumentException
364  {
365    commandBuilder.clearArguments();
366    copySecureArgsList.createGlobalArguments();
367    state.providedAdminUID = null;
368    state.providedBindDN = null;
369  }
370
371  private void resolveHostName() throws ArgumentException
372  {
373    state.hostName = secureArgsList.getHostNameArg().getValue();
374    promptForHostNameIfRequired();
375    addArgToCommandBuilder(copySecureArgsList.getHostNameArg(), state.hostName);
376  }
377
378  private void resolveConnectionType(boolean canUseStartTLS)
379  {
380    state.setSsl(secureArgsList);
381    promptForConnectionTypeIfRequired(canUseStartTLS);
382    addConnectionTypeToCommandBuilder();
383  }
384
385  private void resolvePortNumber() throws ArgumentException
386  {
387    portNumber = (state.useSSL && !secureArgsList.getPortArg().isPresent())
388        ? secureArgsList.getPortFromConfig()
389        : secureArgsList.getPortArg().getIntValue();
390    promptForPortNumberIfRequired();
391    addArgToCommandBuilder(copySecureArgsList.getPortArg(), String.valueOf(portNumber));
392  }
393
394  private void resolveTrustAndKeyManagers() throws ArgumentException {
395    if ((state.useSSL || state.useStartTLS) && state.trustManager == null)
396    {
397      initializeTrustAndKeyManagers();
398    }
399  }
400
401  private void resolveCredentialLogin() throws ArgumentException
402  {
403    setAdminUidAndBindDnFromArgs();
404    if (useKeyManager())
405    {
406      return;
407    }
408    promptForCredentialLoginIfRequired(secureArgsList.getBindDnArg().getValue(),
409                                       secureArgsList.getAdminUidArg().getValue());
410    final boolean onlyBindDnProvided = state.providedAdminUID != null || state.providedBindDN == null;
411    if ((useAdminOrBindDn && onlyBindDnProvided)
412     || (!useAdminOrBindDn && isAdminUidArgVisible()))
413    {
414      addArgToCommandBuilder(copySecureArgsList.getAdminUidArg(), getAdministratorUID());
415    }
416    else
417    {
418      addArgToCommandBuilder(copySecureArgsList.getBindDnArg(), getBindDN());
419    }
420  }
421
422  private void setAdminUidAndBindDnFromArgs()
423  {
424    final Argument adminUid = secureArgsList.getAdminUidArg();
425    final Argument bindDn = secureArgsList.getBindDnArg();
426
427    state.providedAdminUID = (isAdminUidArgVisible() && adminUid.isPresent()) ? adminUid.getValue() : null;
428    state.providedBindDN = ((useAdminOrBindDn || !isAdminUidArgVisible()) && bindDn.isPresent()) ? bindDn.getValue()
429                                                                                                 : null;
430    state.adminUID = !useKeyManager() ? adminUid.getValue() : null;
431    state.bindDN = !useKeyManager() ? bindDn.getValue() : null;
432  }
433
434  private void resolveCredentialPassword() throws ArgumentException
435  {
436    if (secureArgsList.getBindPasswordArg().isPresent())
437    {
438      state.bindPassword = secureArgsList.getBindPasswordArg().getValue();
439    }
440
441    if (useKeyManager())
442    {
443      return;
444    }
445
446    setBindPasswordFileFromArgs();
447    final boolean addedPasswordFileArgument = secureArgsList.getBindPasswordFileArg().isPresent();
448    if (!addedPasswordFileArgument && (state.bindPassword == null || "-".equals(state.bindPassword)))
449    {
450      promptForBindPasswordIfRequired();
451    }
452
453    final Argument bindPassword = copySecureArgsList.getBindPasswordArg();
454    bindPassword.clearValues();
455    bindPassword.addValue(state.bindPassword);
456    if (!addedPasswordFileArgument)
457    {
458      commandBuilder.addObfuscatedArgument(bindPassword);
459    }
460  }
461
462  private void setBindPasswordFileFromArgs() throws ArgumentException
463  {
464    final FileBasedArgument bindPasswordFile = secureArgsList.getBindPasswordFileArg();
465    if (bindPasswordFile.isPresent())
466    {
467      // Read from file if it exists.
468      state.bindPassword = bindPasswordFile.getValue();
469      if (state.bindPassword == null)
470      {
471        throw new ArgumentException(
472            ERR_ERROR_NO_ADMIN_PASSWORD.get(isAdminUidArgVisible() ? state.adminUID : state.bindDN));
473      }
474      addArgToCommandBuilder(copySecureArgsList.getBindPasswordFileArg(), bindPasswordFile.getNameToValueMap());
475    }
476  }
477
478  private void resolveConnectTimeout() throws ArgumentException
479  {
480    state.connectTimeout = secureArgsList.getConnectTimeoutArg().getIntValue();
481  }
482
483  private void promptForHostNameIfRequired() throws ArgumentException
484  {
485    if (!app.isInteractive() || secureArgsList.getHostNameArg().isPresent())
486    {
487      return;
488    }
489    checkHeadingDisplayed();
490    ValidationCallback<String> callback = new ValidationCallback<String>()
491    {
492      @Override
493      public String validate(ConsoleApplication app, String rawInput) throws ClientException
494      {
495        final String input = rawInput.trim();
496        if (input.length() == 0)
497        {
498          return state.hostName;
499        }
500
501        try
502        {
503          // Ensure that the prompted host is known
504          InetAddress.getByName(input);
505          return input;
506        }
507        catch (UnknownHostException e)
508        {
509          // Try again...
510          app.println();
511          app.println(ERR_LDAP_CONN_BAD_HOST_NAME.get(input));
512          app.println();
513          return null;
514        }
515      }
516    };
517
518    try
519    {
520      app.println();
521      state.hostName = app.readValidatedInput(INFO_LDAP_CONN_PROMPT_HOST_NAME.get(state.hostName), callback);
522    }
523    catch (ClientException e)
524    {
525      throw cannotReadConnectionParameters(e);
526    }
527  }
528
529  private void promptForConnectionTypeIfRequired(final boolean canUseStartTLS)
530  {
531    final boolean valuesSetByProperty = secureArgsList.getUseSSLArg().isValueSetByProperty()
532                                     && secureArgsList.getUseStartTLSArg().isValueSetByProperty();
533    if (!app.isInteractive() || state.useSSL || state.useStartTLS || valuesSetByProperty)
534    {
535      return;
536    }
537    checkHeadingDisplayed();
538    final MenuBuilder<Integer> builder = new MenuBuilder<>(app);
539    builder.setPrompt(INFO_LDAP_CONN_PROMPT_SECURITY_USE_SECURE_CTX.get());
540
541    for (Protocol p : Protocol.values())
542    {
543      if ((!displayLdapIfSecureParameters && Protocol.LDAP.equals(p))
544          || (!canUseStartTLS && Protocol.START_TLS.equals(p)))
545      {
546        continue;
547      }
548
549      final MenuResult<Integer> menuResult = MenuResult.success(p.getChoice());
550      final int i = builder.addNumberedOption(p.message, menuResult);
551      if (DEFAULT_PROMPT_PROTOCOL.equals(p))
552      {
553        builder.setDefault(INFO_LDAP_CONN_PROMPT_SECURITY_PROTOCOL_DEFAULT_CHOICE.get(i), menuResult);
554      }
555    }
556
557    Menu<Integer> menu = builder.toMenu();
558    try
559    {
560      final MenuResult<Integer> result = menu.run();
561      throwIfMenuResultNotSucceeded(result);
562      final int userChoice = result.getValue();
563      if (Protocol.SSL.getChoice() == userChoice)
564      {
565        state.useSSL = true;
566      }
567      else if (Protocol.START_TLS.getChoice() == userChoice)
568      {
569        state.useStartTLS = true;
570      }
571    }
572    catch (ClientException e)
573    {
574      throw new RuntimeException(e);
575    }
576  }
577
578  private void promptForPortNumberIfRequired() throws ArgumentException
579  {
580    if (!app.isInteractive() || secureArgsList.getPortArg().isPresent())
581    {
582      return;
583    }
584    checkHeadingDisplayed();
585    try
586    {
587      app.println();
588      final LocalizableMessage askPortNumberMsg = secureArgsList.alwaysSSL() ?
589          INFO_ADMIN_CONN_PROMPT_PORT_NUMBER.get(portNumber) :
590          INFO_LDAP_CONN_PROMPT_PORT_NUMBER.get(portNumber);
591      portNumber = app.readValidatedInput(askPortNumberMsg, portValidationCallback(portNumber));
592    }
593    catch (ClientException e)
594    {
595      throw cannotReadConnectionParameters(e);
596    }
597  }
598
599  private void promptForCredentialLoginIfRequired(final String defaultBindDN, final String defaultAdminUID)
600      throws ArgumentException
601  {
602    if (!app.isInteractive() || state.providedAdminUID != null || state.providedBindDN != null)
603    {
604      return;
605    }
606    checkHeadingDisplayed();
607    ValidationCallback<String> callback = new ValidationCallback<String>()
608    {
609      @Override public String validate(ConsoleApplication app, String rawInput) throws ClientException
610      {
611        final String input = rawInput.trim();
612        if (input.isEmpty())
613        {
614          return isAdminUidArgVisible() ? defaultAdminUID : defaultBindDN;
615        }
616
617        return input;
618      }
619    };
620
621    try
622    {
623      app.println();
624      if (useAdminOrBindDn)
625      {
626        String def = state.adminUID != null ? state.adminUID : state.bindDN;
627        String v = app.readValidatedInput(INFO_LDAP_CONN_GLOBAL_ADMINISTRATOR_OR_BINDDN_PROMPT.get(def), callback);
628        if (isDN(v))
629        {
630          state.bindDN = v;
631          state.providedBindDN = v;
632          state.adminUID = null;
633          state.providedAdminUID = null;
634        }
635        else
636        {
637          state.bindDN = null;
638          state.providedBindDN = null;
639          state.adminUID = v;
640          state.providedAdminUID = v;
641        }
642      }
643      else if (isAdminUidArgVisible())
644      {
645        state.adminUID = app.readValidatedInput(INFO_LDAP_CONN_PROMPT_ADMINISTRATOR_UID.get(state.adminUID), callback);
646        state.providedAdminUID = state.adminUID;
647      }
648      else
649      {
650        state.bindDN = app.readValidatedInput(INFO_LDAP_CONN_PROMPT_BIND_DN.get(state.bindDN), callback);
651        state.providedBindDN = state.bindDN;
652      }
653    }
654    catch (ClientException e)
655    {
656      throw cannotReadConnectionParameters(e);
657    }
658  }
659
660  private void promptForBindPasswordIfRequired() throws ArgumentException
661  {
662    if (!app.isInteractive())
663    {
664      if (allowAnonymousIfNonInteractive)
665      {
666        return;
667      }
668      throw new ArgumentException(ERR_ERROR_BIND_PASSWORD_NONINTERACTIVE.get());
669    }
670    checkHeadingDisplayed();
671    try
672    {
673      state.bindPassword = readPassword(state.getPrompt());
674    }
675    catch (Exception e)
676    {
677      throw new ArgumentException(ERR_ERROR_CANNOT_READ_CONNECTION_PARAMETERS.get(e.getMessage()), e.getCause());
678    }
679  }
680
681  /**
682   * Get the trust manager.
683   *
684   * @return The trust manager based on CLI args on interactive prompt.
685   * @throws ArgumentException
686   *           If an error occurs when getting args values.
687   */
688  private ApplicationTrustManager getTrustManagerInternal() throws ArgumentException
689  {
690    // Remove these arguments since this method might be called several times.
691    commandBuilder.removeArguments(copySecureArgsList.getTrustAllArg(),
692                                   copySecureArgsList.getTrustStorePathArg(),
693                                   copySecureArgsList.getTrustStorePasswordArg(),
694                                   copySecureArgsList.getTrustStorePasswordFileArg());
695
696    final TrustMethod trustMethod = resolveTrustMethod();
697    if (TrustMethod.TRUSTALL == trustMethod)
698    {
699      return null;
700    }
701
702    final boolean promptForTrustStore = TrustMethod.TRUSTSTORE == trustMethod;
703    resolveTrustStorePath(promptForTrustStore);
704    setTrustStorePassword();
705    setTrustStorePasswordFromFile();
706    if ("-".equals(state.truststorePassword))
707    {
708      // Read the password from the stdin.
709      promptForTrustStorePasswordIfRequired();
710    }
711
712    return resolveTrustStore();
713  }
714
715  /** As the most common case is to have no password for trust store, we do not ask it in the interactive mode.*/
716  private void setTrustStorePassword()
717  {
718    if (secureArgsList.getTrustStorePasswordArg().isPresent())
719    {
720      state.truststorePassword = secureArgsList.getTrustStorePasswordArg().getValue();
721    }
722  }
723
724  private void setTrustStorePasswordFromFile()
725  {
726    if (secureArgsList.getTrustStorePasswordFileArg().isPresent())
727    {
728      state.truststorePassword = secureArgsList.getTrustStorePasswordFileArg().getValue();
729    }
730  }
731
732  /** Return the trust method chosen by user or {@code null} if the information is not available. */
733  private TrustMethod resolveTrustMethod()
734  {
735    state.trustAll = secureArgsList.getTrustAllArg().isPresent();
736    // Check if some trust manager info are set
737    boolean needPromptForTrustMethod = !state.trustAll
738        && !secureArgsList.getTrustStorePathArg().isPresent()
739        && !secureArgsList.getTrustStorePasswordArg().isPresent()
740        && !secureArgsList.getTrustStorePasswordFileArg().isPresent();
741
742    TrustMethod trustMethod = state.trustAll ? TrustMethod.TRUSTALL : null;
743    // Try to use the local instance trust store, to avoid certificate
744    // validation when both the CLI and the server are in the same instance.
745    if (needPromptForTrustMethod && !useLocalTrustStoreIfPossible())
746    {
747      trustMethod = promptForTrustMethodIfRequired();
748    }
749
750    if (trustMethod != TrustMethod.TRUSTSTORE)
751    {
752      // There is no direct equivalent for the display certificate option,
753      // so propose trust all option as command-line argument.
754      commandBuilder.addArgument(copySecureArgsList.getTrustAllArg());
755    }
756
757    return trustMethod;
758  }
759
760  private void resolveTrustStorePath(final boolean promptForTrustStore) throws ArgumentException
761  {
762    state.truststorePath = secureArgsList.getTrustStorePathArg().getValue();
763    if (promptForTrustStore)
764    {
765      promptForTrustStorePathIfRequired();
766    }
767    addArgToCommandBuilder(copySecureArgsList.getTrustStorePathArg(), state.truststorePath);
768  }
769
770  private ApplicationTrustManager resolveTrustStore() throws ArgumentException
771  {
772    try
773    {
774      state.truststore = KeyStore.getInstance(KeyStore.getDefaultType());
775      if (state.truststorePath != null)
776      {
777        try (FileInputStream fos = new FileInputStream(state.truststorePath))
778        {
779          state.truststore.load(fos, state.truststorePassword != null ? state.truststorePassword.toCharArray() : null);
780        }
781      }
782      else
783      {
784        state.truststore.load(null, null);
785      }
786
787      if (secureArgsList.getTrustStorePasswordFileArg().isPresent() && state.truststorePath != null)
788      {
789        addArgToCommandBuilder(copySecureArgsList.getTrustStorePasswordFileArg(),
790            secureArgsList.getTrustStorePasswordFileArg().getNameToValueMap());
791      }
792      else if (state.truststorePassword != null && state.truststorePath != null)
793      {
794        addObfuscatedArgToCommandBuilder(copySecureArgsList.getTrustStorePasswordArg(), state.truststorePassword);
795      }
796
797      return new ApplicationTrustManager(state.truststore);
798    }
799    catch (Exception e)
800    {
801      throw new ArgumentException(ERR_ERROR_CANNOT_READ_CONNECTION_PARAMETERS.get(e.getMessage()), e.getCause());
802    }
803  }
804
805  private TrustMethod promptForTrustMethodIfRequired()
806  {
807    if (!app.isInteractive())
808    {
809      return null;
810    }
811
812    checkHeadingDisplayed();
813    app.println();
814    MenuBuilder<Integer> builder = new MenuBuilder<>(app);
815    builder.setPrompt(INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_METHOD.get());
816
817    for (TrustMethod t : TrustMethod.values())
818    {
819      int i = builder.addNumberedOption(t.message, MenuResult.success(t.getChoice()));
820      if (DEFAULT_PROMPT_TRUST_METHOD.equals(t))
821      {
822        builder.setDefault(
823            INFO_LDAP_CONN_PROMPT_SECURITY_PROTOCOL_DEFAULT_CHOICE.get(i), MenuResult.success(t.getChoice()));
824      }
825    }
826
827    Menu<Integer> menu = builder.toMenu();
828    state.trustStoreInMemory = false;
829    try
830    {
831      final MenuResult<Integer> result = menu.run();
832      throwIfMenuResultNotSucceeded(result);
833      final int userChoice = result.getValue();
834      if (TrustMethod.TRUSTALL.getChoice() == userChoice)
835      {
836        state.trustAll = true;
837      }
838      else if (TrustMethod.DISPLAY_CERTIFICATE.getChoice() == userChoice)
839      {
840        state.trustStoreInMemory = true;
841      }
842      return TrustMethod.getTrustMethodForIndex(userChoice);
843    }
844    catch (ClientException e)
845    {
846      throw new RuntimeException(e);
847    }
848  }
849
850  private void promptForTrustStorePathIfRequired() throws ArgumentException
851  {
852    if (!app.isInteractive() || secureArgsList.getTrustStorePathArg().isPresent())
853    {
854      return;
855    }
856
857    checkHeadingDisplayed();
858    try
859    {
860      app.println();
861      state.truststorePath = app.readValidatedInput(
862          INFO_LDAP_CONN_PROMPT_SECURITY_TRUSTSTORE_PATH.get(),
863          filePathValidationCallback(!ALLOW_EMPTY_PATH, FILE_MUST_EXISTS));
864    }
865    catch (ClientException e)
866    {
867      throw cannotReadConnectionParameters(e);
868    }
869  }
870
871  private void promptForTrustStorePasswordIfRequired() throws ArgumentException
872  {
873    if (!app.isInteractive())
874    {
875      return;
876    }
877
878    checkHeadingDisplayed();
879    try
880    {
881      state.truststorePassword = readPassword(
882          INFO_LDAP_CONN_PROMPT_SECURITY_TRUSTSTORE_PASSWORD.get(state.truststorePath));
883    }
884    catch (Exception e)
885    {
886      throw new ArgumentException(ERR_ERROR_CANNOT_READ_CONNECTION_PARAMETERS.get(e.getMessage()), e.getCause());
887    }
888  }
889
890  /**
891   * Get the key manager.
892   *
893   * @return The key manager based on CLI args on interactive prompt.
894   * @throws ArgumentException
895   *           If an error occurs when getting args values.
896   */
897  private KeyManager getKeyManagerInternal() throws ArgumentException
898  {
899    //  Remove these arguments since this method might be called several times.
900    commandBuilder.removeArguments(copySecureArgsList.getCertNicknameArg(),
901                                   copySecureArgsList.getKeyStorePathArg(),
902                                   copySecureArgsList.getKeyStorePasswordArg(),
903                                   copySecureArgsList.getKeyStorePasswordFileArg());
904
905    if (!secureArgsList.getKeyStorePathArg().isPresent()
906     && !secureArgsList.getKeyStorePasswordArg().isPresent()
907     && !secureArgsList.getKeyStorePasswordFileArg().isPresent()
908     && !secureArgsList.getCertNicknameArg().isPresent())
909    {
910      // If no one of these parameters above are set, we assume that we do not need client side authentication.
911      // Client side authentication is not the common use case so interactive mode doesn't add an extra question.
912      return null;
913    }
914
915    resolveKeyStorePath();
916    resolveKeyStorePassword();
917
918    final KeyStore keystore = createKeyStore();
919    resolveCertificateNickname(keystore);
920
921    final ApplicationKeyManager keyManager = new ApplicationKeyManager(keystore, state.keystorePassword.toCharArray());
922    addKeyStorePasswordArgToCommandBuilder();
923    if (state.certifNickname != null)
924    {
925      addArgToCommandBuilder(copySecureArgsList.getCertNicknameArg(), state.certifNickname);
926      return SelectableCertificateKeyManager.wrap(
927          new KeyManager[] { keyManager }, CollectionUtils.newTreeSet(state.certifNickname))[0];
928    }
929
930    return keyManager;
931  }
932
933  private void resolveKeyStorePath() throws ArgumentException
934  {
935    state.keyStorePath = secureArgsList.getKeyStorePathArg().getValue();
936    promptForKeyStorePathIfRequired();
937
938    if (state.keyStorePath == null)
939    {
940      throw new ArgumentException(ERR_ERROR_INCOMPATIBLE_PROPERTY_MOD.get("null keystorePath"));
941    }
942    addArgToCommandBuilder(copySecureArgsList.getKeyStorePathArg(), state.keyStorePath);
943  }
944
945  private void resolveKeyStorePassword() throws ArgumentException
946  {
947    state.keystorePassword = secureArgsList.getKeyStorePasswordArg().getValue();
948
949    if (secureArgsList.getKeyStorePasswordFileArg().isPresent())
950    {
951      state.keystorePassword = secureArgsList.getKeyStorePasswordFileArg().getValue();
952      if (state.keystorePassword == null)
953      {
954        throw new ArgumentException(ERR_INSTALLDS_NO_KEYSTORE_PASSWORD.get(
955            secureArgsList.getKeyStorePathArg().getLongIdentifier(),
956            secureArgsList.getKeyStorePasswordFileArg().getLongIdentifier()));
957      }
958    }
959    else if (state.keystorePassword == null || "-".equals(state.keystorePassword))
960    {
961      promptForKeyStorePasswordIfRequired();
962    }
963  }
964
965  private KeyStore createKeyStore() throws ArgumentException
966  {
967    try (FileInputStream fos = new FileInputStream(state.keyStorePath))
968    {
969      final KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
970      keystore.load(fos, state.keystorePassword.toCharArray());
971      return keystore;
972    }
973    catch (Exception e)
974    {
975      throw new ArgumentException(ERR_ERROR_CANNOT_READ_CONNECTION_PARAMETERS.get(e.getMessage()), e.getCause());
976    }
977  }
978
979  private void resolveCertificateNickname(final KeyStore keystore) throws ArgumentException
980  {
981    state.certifNickname = secureArgsList.getCertNicknameArg().getValue();
982    try
983    {
984      promptForCertificateNicknameIfRequired(keystore, keystore.aliases());
985    }
986    catch (final KeyStoreException e)
987    {
988      throw new ArgumentException(ERR_RESOLVE_KEYSTORE_ALIASES.get(e.getMessage()), e);
989    }
990  }
991
992  private void promptForKeyStorePathIfRequired() throws ArgumentException
993  {
994    if (!app.isInteractive() || secureArgsList.getKeyStorePathArg().isPresent())
995    {
996      return;
997    }
998    checkHeadingDisplayed();
999    try
1000    {
1001      app.println();
1002      state.keyStorePath = app.readValidatedInput(INFO_LDAP_CONN_PROMPT_SECURITY_KEYSTORE_PATH.get(),
1003                                                  filePathValidationCallback(ALLOW_EMPTY_PATH, FILE_MUST_EXISTS));
1004    }
1005    catch (ClientException e)
1006    {
1007      throw cannotReadConnectionParameters(e);
1008    }
1009  }
1010
1011  private void promptForKeyStorePasswordIfRequired() throws ArgumentException
1012  {
1013    if (!app.isInteractive())
1014    {
1015      throw new ArgumentException(ERR_ERROR_BIND_PASSWORD_NONINTERACTIVE.get());
1016    }
1017    checkHeadingDisplayed();
1018    try
1019    {
1020      state.keystorePassword = readPassword(INFO_LDAP_CONN_PROMPT_SECURITY_KEYSTORE_PASSWORD.get(state.keyStorePath));
1021    }
1022    catch (Exception e)
1023    {
1024      throw new ArgumentException(ERR_ERROR_CANNOT_READ_CONNECTION_PARAMETERS.get(e.getMessage()), e.getCause());
1025    }
1026  }
1027
1028  private void promptForCertificateNicknameIfRequired(KeyStore keystore, Enumeration<String> aliasesEnum)
1029      throws ArgumentException
1030  {
1031    if (!app.isInteractive() || secureArgsList.getCertNicknameArg().isPresent() || !aliasesEnum.hasMoreElements())
1032    {
1033      return;
1034    }
1035    state.certifNickname = null;
1036    checkHeadingDisplayed();
1037    try
1038    {
1039      MenuBuilder<String> builder = new MenuBuilder<>(app);
1040      builder.setPrompt(INFO_LDAP_CONN_PROMPT_SECURITY_CERTIFICATE_ALIASES.get());
1041      int certificateNumber = 0;
1042      while (aliasesEnum.hasMoreElements())
1043      {
1044        final String alias = aliasesEnum.nextElement();
1045        if (keystore.isKeyEntry(alias))
1046        {
1047          certificateNumber++;
1048          X509Certificate certif = (X509Certificate) keystore.getCertificate(alias);
1049          builder.addNumberedOption(INFO_LDAP_CONN_PROMPT_SECURITY_CERTIFICATE_ALIAS.get(
1050                                                                                alias, certif.getSubjectDN().getName()),
1051                                    MenuResult.success(alias));
1052        }
1053      }
1054
1055      if (certificateNumber > 1)
1056      {
1057        app.println();
1058        Menu<String> menu = builder.toMenu();
1059        final MenuResult<String> result = menu.run();
1060        throwIfMenuResultNotSucceeded(result);
1061        state.certifNickname = result.getValue();
1062      }
1063    }
1064    catch (KeyStoreException e)
1065    {
1066      throw new ArgumentException(ERR_ERROR_CANNOT_READ_CONNECTION_PARAMETERS.get(e.getMessage()), e.getCause());
1067    }
1068    catch (ClientException e)
1069    {
1070      throw cannotReadConnectionParameters(e);
1071    }
1072  }
1073
1074  private void addKeyStorePasswordArgToCommandBuilder()
1075  {
1076    if (secureArgsList.getKeyStorePasswordFileArg().isPresent())
1077    {
1078      addArgToCommandBuilder(copySecureArgsList.getKeyStorePasswordFileArg(),
1079          secureArgsList.getKeyStorePasswordFileArg().getNameToValueMap());
1080    }
1081    else if (state.keystorePassword != null)
1082    {
1083      addObfuscatedArgToCommandBuilder(copySecureArgsList.getKeyStorePasswordArg(), state.keystorePassword);
1084    }
1085  }
1086
1087  private void addConnectionTypeToCommandBuilder()
1088  {
1089    if (state.useSSL)
1090    {
1091      commandBuilder.addArgument(copySecureArgsList.getUseSSLArg());
1092    }
1093    else if (state.useStartTLS)
1094    {
1095      commandBuilder.addArgument(copySecureArgsList.getUseStartTLSArg());
1096    }
1097  }
1098
1099  private void addArgToCommandBuilder(final Argument arg, final String value)
1100  {
1101    addArgToCommandBuilder(arg, value, false);
1102  }
1103
1104  private void addObfuscatedArgToCommandBuilder(final Argument arg, final String value)
1105  {
1106    addArgToCommandBuilder(arg, value, true);
1107  }
1108
1109  private void addArgToCommandBuilder(final Argument arg, final String value, final boolean obfuscated)
1110  {
1111    if (value != null)
1112    {
1113      arg.clearValues();
1114      arg.addValue(value);
1115      commandBuilder.addArgument(arg);
1116    }
1117  }
1118
1119  private void addArgToCommandBuilder(final FileBasedArgument arg, final Map<String, String> nameToValueMap)
1120  {
1121    arg.clearValues();
1122    arg.getNameToValueMap().putAll(nameToValueMap);
1123    commandBuilder.addArgument(arg);
1124  }
1125
1126  private ArgumentException cannotReadConnectionParameters(ClientException e)
1127  {
1128    return new ArgumentException(ERR_ERROR_CANNOT_READ_CONNECTION_PARAMETERS.get(e.getMessage()), e.getCause());
1129  }
1130
1131  private String readPassword(LocalizableMessage prompt) throws ClientException
1132  {
1133    app.println();
1134    final char[] pwd = app.readPassword(prompt);
1135    if (pwd != null)
1136    {
1137      return String.valueOf(pwd);
1138    }
1139    return null;
1140  }
1141
1142  private ValidationCallback<String> filePathValidationCallback(
1143      final boolean allowEmptyPath, final boolean checkExistenceAndReadability)
1144  {
1145    return new ValidationCallback<String>()
1146    {
1147      @Override
1148      public String validate(final ConsoleApplication app, final String filePathUserInput) throws ClientException
1149      {
1150        final String filePath = filePathUserInput.trim();
1151        final File f = new File(filePath);
1152
1153        if ((!allowEmptyPath && filePath.isEmpty())
1154            || f.isDirectory()
1155            || (checkExistenceAndReadability && !(f.exists() && f.canRead())))
1156        {
1157          app.println();
1158          app.println(ERR_LDAP_CONN_PROMPT_SECURITY_INVALID_FILE_PATH.get());
1159          app.println();
1160          return null;
1161        }
1162
1163        return filePath;
1164      }
1165    };
1166  }
1167
1168  /** Returns {@code true} if client script uses the adminUID argument. */
1169  private boolean isAdminUidArgVisible()
1170  {
1171    return !secureArgsList.getAdminUidArg().isHidden();
1172  }
1173
1174  private boolean useKeyManager()
1175  {
1176    return state.keyManager != null;
1177  }
1178
1179  /**
1180   * Indicates whether a connection should use SSL based on this interaction.
1181   *
1182   * @return boolean where true means use SSL
1183   */
1184  public boolean useSSL()
1185  {
1186    return state.useSSL;
1187  }
1188
1189  /**
1190   * Indicates whether a connection should use StartTLS based on this interaction.
1191   *
1192   * @return boolean where true means use StartTLS
1193   */
1194  public boolean useStartTLS()
1195  {
1196    return state.useStartTLS;
1197  }
1198
1199  /**
1200   * Gets the host name that should be used for connections based on this
1201   * interaction.
1202   *
1203   * @return host name for connections
1204   */
1205  public String getHostName()
1206  {
1207    return state.hostName;
1208  }
1209
1210  /**
1211   * Gets the port number name that should be used for connections based on this
1212   * interaction.
1213   *
1214   * @return port number for connections
1215   */
1216  public int getPortNumber()
1217  {
1218    return portNumber;
1219  }
1220
1221  /**
1222   * Sets the port number name that should be used for connections based on this
1223   * interaction.
1224   *
1225   * @param portNumber
1226   *          port number for connections
1227   */
1228  public void setPortNumber(int portNumber)
1229  {
1230    this.portNumber = portNumber;
1231  }
1232
1233  /**
1234   * Gets the bind DN name that should be used for connections based on this
1235   * interaction.
1236   *
1237   * @return bind DN for connections
1238   */
1239  public String getBindDN()
1240  {
1241    if (useAdminOrBindDn)
1242    {
1243      return state.getAdminOrBindDN();
1244    }
1245    else if (isAdminUidArgVisible())
1246    {
1247      return getAdministratorDN(state.adminUID);
1248    }
1249    else
1250    {
1251      return state.bindDN;
1252    }
1253  }
1254
1255  /**
1256   * Gets the administrator UID name that should be used for connections based
1257   * on this interaction.
1258   *
1259   * @return administrator UID for connections
1260   */
1261  public String getAdministratorUID()
1262  {
1263    return state.adminUID;
1264  }
1265
1266  /**
1267   * Gets the bind password that should be used for connections based on this
1268   * interaction.
1269   *
1270   * @return bind password for connections
1271   */
1272  public String getBindPassword()
1273  {
1274    return state.bindPassword;
1275  }
1276
1277  /**
1278   * Gets the trust manager that should be used for connections based on this
1279   * interaction.
1280   *
1281   * @return trust manager for connections
1282   */
1283  public ApplicationTrustManager getTrustManager()
1284  {
1285    return state.trustManager;
1286  }
1287
1288  /**
1289   * Gets the key store that should be used for connections based on this
1290   * interaction.
1291   *
1292   * @return key store for connections
1293   */
1294  public KeyStore getKeyStore()
1295  {
1296    return state.truststore;
1297  }
1298
1299  /**
1300   * Gets the key manager that should be used for connections based on this
1301   * interaction.
1302   *
1303   * @return key manager for connections
1304   */
1305  public KeyManager getKeyManager()
1306  {
1307    return state.keyManager;
1308  }
1309
1310  /**
1311   * Indicate if the trust store is in memory.
1312   *
1313   * @return true if the trust store is in memory.
1314   */
1315  public boolean isTrustStoreInMemory()
1316  {
1317    return state.trustStoreInMemory;
1318  }
1319
1320  /**
1321   * Indicate if all certificates must be accepted.
1322   *
1323   * @return true all certificates must be accepted.
1324   */
1325  public boolean isTrustAll()
1326  {
1327    return state.trustAll;
1328  }
1329
1330  /**
1331   * Returns the timeout to be used to connect with the server.
1332   *
1333   * @return the timeout to be used to connect with the server.
1334   */
1335  public int getConnectTimeout()
1336  {
1337    return state.connectTimeout;
1338  }
1339
1340  /**
1341   * Indicate if the certificate chain can be trusted.
1342   *
1343   * @param chain
1344   *          The certificate chain to validate
1345   * @param authType
1346   *          the authentication type.
1347   * @param host
1348   *          the host we tried to connect and that presented the certificate.
1349   * @return true if the server certificate is trusted.
1350   */
1351  public boolean checkServerCertificate(final X509Certificate[] chain, final String authType, final String host)
1352  {
1353    if (state.trustManager == null)
1354    {
1355      try
1356      {
1357        initializeTrustAndKeyManagers();
1358      }
1359      catch (ArgumentException ae)
1360      {
1361        // Should not append because this.run() should has been called at this stage.
1362        throw new RuntimeException(ae);
1363      }
1364    }
1365    printCertificateChain(chain);
1366    MenuBuilder<Integer> builder = new MenuBuilder<>(app);
1367    builder.setPrompt(INFO_LDAP_CONN_PROMPT_SECURITY_TRUST_OPTION.get());
1368
1369    for (TrustOption t : TrustOption.values())
1370    {
1371      final MenuResult<Integer> result = MenuResult.success(t.getChoice());
1372      int i = builder.addNumberedOption(t.message, result);
1373      if (DEFAULT_PROMPT_TRUST_OPTION.equals(t))
1374      {
1375        builder.setDefault(INFO_LDAP_CONN_PROMPT_SECURITY_PROTOCOL_DEFAULT_CHOICE.get(i), result);
1376      }
1377    }
1378
1379    app.println();
1380    app.println();
1381
1382    final Menu<Integer> menu = builder.toMenu();
1383    try
1384    {
1385      boolean promptAgain;
1386      int userChoice;
1387      do
1388      {
1389        promptAgain = false;
1390        final MenuResult<Integer> result = menu.run();
1391        throwIfMenuResultNotSucceeded(result);
1392        userChoice = result.getValue();
1393        if (TrustOption.CERTIFICATE_DETAILS.getChoice() == userChoice)
1394        {
1395          promptAgain = true;
1396          printCertificateDetails(chain);
1397        }
1398      }
1399      while (promptAgain);
1400
1401      return trustCertificate(TrustOption.getTrustOptionForIndex(userChoice), chain, authType, host);
1402    }
1403    catch (ClientException e)
1404    {
1405      throw new RuntimeException(e);
1406    }
1407  }
1408
1409  private void printCertificateChain(X509Certificate[] chain)
1410  {
1411    app.println();
1412    app.println(INFO_LDAP_CONN_PROMPT_SECURITY_SERVER_CERTIFICATE.get());
1413    app.println();
1414    boolean printSeparatorLines = false;
1415    for (final X509Certificate cert : chain)
1416    {
1417      if (!printSeparatorLines)
1418      {
1419        app.println();
1420        app.println();
1421        printSeparatorLines = true;
1422      }
1423
1424      // Certificate DN
1425      app.println(INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE_USER_DN.get(cert.getSubjectDN()));
1426      // certificate validity
1427      app.println(INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE_VALIDITY.get(
1428          cert.getNotBefore(), cert.getNotAfter()));
1429      // certificate Issuer
1430      app.println(INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE_ISSUER.get(cert.getIssuerDN()));
1431    }
1432  }
1433
1434  private void printCertificateDetails(X509Certificate[] chain)
1435  {
1436    for (X509Certificate cert : chain)
1437    {
1438      app.println();
1439      app.println(INFO_LDAP_CONN_SECURITY_SERVER_CERTIFICATE.get(cert));
1440    }
1441  }
1442
1443  private boolean trustCertificate(final TrustOption trustOption, final X509Certificate[] chain,
1444      final String authType, final String host) throws ClientException
1445  {
1446    try
1447    {
1448      switch (trustOption)
1449      {
1450      case SESSION:
1451        updateTrustManager(chain, authType, host);
1452        return true;
1453
1454      case PERMAMENT:
1455        updateTrustManager(chain, authType, host);
1456        try
1457        {
1458          trustCertificatePermanently(chain);
1459        }
1460        catch (Exception e)
1461        {
1462          app.println(ERR_TRUSTING_CERTIFICATE_PERMANENTLY.get(e.getMessage()));
1463        }
1464        return true;
1465
1466      case UNTRUSTED:
1467      default:
1468        return false;
1469      }
1470    }
1471    catch (KeyStoreException e)
1472    {
1473      app.println(ERR_TRUSTING_CERTIFICATE.get(e.getMessage()));
1474      return false;
1475    }
1476  }
1477
1478  private void updateTrustManager(X509Certificate[] chain, String authType, String host) throws KeyStoreException
1479  {
1480    // User choice if to add the certificate to the trust store for the current session or permanently.
1481    for (final X509Certificate cert : chain)
1482    {
1483      state.truststore.setCertificateEntry(cert.getSubjectDN().getName(), cert);
1484    }
1485
1486    // Update the trust manager
1487    if (state.trustManager == null)
1488    {
1489      state.trustManager = new ApplicationTrustManager(state.truststore);
1490    }
1491
1492    if (authType != null && host != null)
1493    {
1494      // Update the trust manager with the new certificate
1495      state.trustManager.acceptCertificate(chain, authType, host);
1496    }
1497    else
1498    {
1499      // Do a full reset of the contents of the keystore.
1500      state.trustManager = new ApplicationTrustManager(state.truststore);
1501    }
1502  }
1503
1504  private void trustCertificatePermanently(final X509Certificate[] chain) throws Exception
1505  {
1506    app.println();
1507    final String trustStorePath = app.readValidatedInput(
1508        INFO_LDAP_CONN_PROMPT_SECURITY_TRUSTSTORE_PATH.get(),
1509        filePathValidationCallback(!ALLOW_EMPTY_PATH, !FILE_MUST_EXISTS));
1510
1511    // Read the password from the stdin.
1512    final String trustStorePasswordStr = readPassword(
1513        INFO_LDAP_CONN_PROMPT_SECURITY_KEYSTORE_PASSWORD.get(trustStorePath));
1514    final KeyStore keyStore = KeyStore.getInstance("JKS");
1515    final char[] trustStorePassword = trustStorePasswordStr.toCharArray();
1516    loadKeyStoreFromFile(keyStore, trustStorePath, trustStorePassword);
1517
1518    for (final X509Certificate cert : chain)
1519    {
1520      keyStore.setCertificateEntry(cert.getSubjectDN().getName(), cert);
1521    }
1522
1523    try (final FileOutputStream trustStoreOutputFile = new FileOutputStream(trustStorePath))
1524    {
1525      keyStore.store(trustStoreOutputFile, trustStorePassword);
1526    }
1527  }
1528
1529  private void loadKeyStoreFromFile(
1530      final KeyStore keyStore, final String trustStorePath, final char[] trustStorePassword) throws Exception
1531  {
1532      try (FileInputStream inputStream = new FileInputStream(trustStorePath))
1533      {
1534        keyStore.load(inputStream, trustStorePassword);
1535      }
1536      catch (FileNotFoundException ignored)
1537      {
1538        // create empty keystore
1539        keyStore.load(null, trustStorePassword);
1540      }
1541  }
1542
1543  /**
1544   * Populates a set of LDAP options with state from this interaction.
1545   *
1546   * @param options
1547   *          existing set of options; may be null in which case this method
1548   *          will create a new set of <code>LDAPConnectionOptions</code> to be
1549   *          returned
1550   * @return used during this interaction
1551   * @throws SSLConnectionException
1552   *           if this interaction has specified the use of SSL and there is a
1553   *           problem initializing the SSL connection factory
1554   */
1555  public LDAPConnectionOptions populateLDAPOptions(LDAPConnectionOptions options) throws SSLConnectionException
1556  {
1557    if (options == null)
1558    {
1559      options = new LDAPConnectionOptions();
1560    }
1561    options.setUseSSL(state.useSSL);
1562    options.setStartTLS(state.useStartTLS);
1563    if (state.useSSL)
1564    {
1565      SSLConnectionFactory sslConnectionFactory = new SSLConnectionFactory();
1566      sslConnectionFactory.init(getTrustManager() == null, state.keyStorePath,
1567          state.keystorePassword, state.certifNickname, state.truststorePath, state.truststorePassword);
1568      options.setSSLConnectionFactory(sslConnectionFactory);
1569    }
1570
1571    return options;
1572  }
1573
1574  /**
1575   * Prompts the user to accept the certificate.
1576   *
1577   * @param errorRaised
1578   *          the error raised because the certificate was not trusted.
1579   * @param usedTrustManager
1580   *          the trustManager used when trying to establish the connection.
1581   * @param usedUrl
1582   *          the LDAP URL used to connect to the server.
1583   * @param logger
1584   *          the Logger used to log messages.
1585   * @return {@code true} if the user accepted the certificate and
1586   *         {@code false} otherwise.
1587   */
1588  public boolean promptForCertificateConfirmation(Throwable errorRaised,
1589      ApplicationTrustManager usedTrustManager, String usedUrl, LocalizedLogger logger)
1590  {
1591    final ApplicationTrustManager.Cause cause = usedTrustManager != null ? usedTrustManager.getLastRefusedCause()
1592                                                                         : null;
1593    logger.debug(INFO_CERTIFICATE_EXCEPTION_CAUSE.get(cause));
1594
1595    if (cause == null)
1596    {
1597      app.println(getThrowableMsg(INFO_ERROR_CONNECTING_TO_LOCAL.get(), errorRaised));
1598      return false;
1599    }
1600
1601    String host;
1602    int port;
1603    try
1604    {
1605      URI uri = new URI(usedUrl);
1606      host = uri.getHost();
1607      port = uri.getPort();
1608    }
1609    catch (URISyntaxException e)
1610    {
1611      logger.warn(ERROR_CERTIFICATE_PARSING_URL.get(usedUrl, e));
1612      host = INFO_NOT_AVAILABLE_LABEL.get().toString();
1613      port = -1;
1614    }
1615
1616    final String authType = usedTrustManager.getLastRefusedAuthType();
1617    if (authType == null)
1618    {
1619      logger.warn(ERROR_CERTIFICATE_NULL_AUTH_TYPE.get());
1620    }
1621    else
1622    {
1623      app.println(ApplicationTrustManager.Cause.NOT_TRUSTED.equals(authType)
1624          ? INFO_CERTIFICATE_NOT_TRUSTED_TEXT_CLI.get(host, port)
1625          : INFO_CERTIFICATE_NAME_MISMATCH_TEXT_CLI.get(host, port, host, host, port));
1626    }
1627
1628    final X509Certificate[] chain = usedTrustManager.getLastRefusedChain();
1629    if (chain == null)
1630    {
1631      logger.warn(ERROR_CERTIFICATE_NULL_CHAIN.get());
1632      return false;
1633    }
1634    if (host == null)
1635    {
1636      logger.warn(ERROR_CERTIFICATE_NULL_HOST_NAME.get());
1637    }
1638
1639    return checkServerCertificate(chain, authType, host);
1640  }
1641
1642  /**
1643   * Sets the heading that is displayed in interactive mode.
1644   *
1645   * @param heading
1646   *          the heading that is displayed in interactive mode.
1647   */
1648  public void setHeadingMessage(LocalizableMessage heading)
1649  {
1650    this.heading = heading;
1651  }
1652
1653  /**
1654   * Returns the command builder with the equivalent arguments on the
1655   * non-interactive mode.
1656   *
1657   * @return the command builder with the equivalent arguments on the
1658   *         non-interactive mode.
1659   */
1660  public CommandBuilder getCommandBuilder()
1661  {
1662    return commandBuilder;
1663  }
1664
1665  /**
1666   * Displays the heading if it was not displayed before.
1667   */
1668  private void checkHeadingDisplayed()
1669  {
1670    if (!state.isHeadingDisplayed)
1671    {
1672      app.println();
1673      app.println();
1674      app.println(heading);
1675      state.isHeadingDisplayed = true;
1676    }
1677  }
1678
1679  /**
1680   * Tells whether we can ask during interaction for both the DN and the admin
1681   * UID or not.
1682   * Default value is {@code false}.
1683   *
1684   * @param useAdminOrBindDn
1685   *          whether we can ask for both the DN and the admin UID during
1686   *          interaction or not.
1687   */
1688  public void setUseAdminOrBindDn(boolean useAdminOrBindDn)
1689  {
1690    this.useAdminOrBindDn = useAdminOrBindDn;
1691  }
1692
1693  /**
1694   * Tells whether we propose LDAP as protocol even if the user provided
1695   * security parameters. This is required in command-lines that access multiple
1696   * servers (like dsreplication).
1697   *
1698   * @param displayLdapIfSecureParameters
1699   *          whether propose LDAP as protocol even if the user provided
1700   *          security parameters or not.
1701   */
1702  public void setDisplayLdapIfSecureParameters(boolean displayLdapIfSecureParameters)
1703  {
1704    this.displayLdapIfSecureParameters = displayLdapIfSecureParameters;
1705  }
1706
1707  /**
1708   * Resets the heading displayed flag, so that next time we call run the
1709   * heading is displayed.
1710   */
1711  public void resetHeadingDisplayed()
1712  {
1713    state.isHeadingDisplayed = false;
1714  }
1715
1716  /**
1717   * Forces the initialization of the trust manager with the arguments provided
1718   * by the user.
1719   *
1720   * @throws ArgumentException
1721   *           if there is an error with the arguments provided by the user.
1722   */
1723  public void initializeTrustManagerIfRequired() throws ArgumentException
1724  {
1725    if (!state.trustManagerInitialized)
1726    {
1727      initializeTrustAndKeyManagers();
1728    }
1729  }
1730
1731  /**
1732   * Initializes the global arguments in the parser with the provided values.
1733   * This is useful when we want to call LDAPConnectionConsoleInteraction.run()
1734   * with some default values.
1735   *
1736   * @param hostName
1737   *          the host name.
1738   * @param port
1739   *          the port to connect to the server.
1740   * @param adminUid
1741   *          the administrator UID.
1742   * @param bindDn
1743   *          the bind DN to bind to the server.
1744   * @param bindPwd
1745   *          the password to bind.
1746   * @param pwdFile
1747   *          the Map containing the file and the password to bind.
1748   */
1749  public void initializeGlobalArguments(String hostName, int port,
1750      String adminUid, String bindDn, String bindPwd,
1751      LinkedHashMap<String, String> pwdFile)
1752  {
1753    resetConnectionArguments();
1754    if (hostName != null)
1755    {
1756      secureArgsList.getHostNameArg().addValue(hostName);
1757      secureArgsList.getHostNameArg().setPresent(true);
1758    }
1759    // resetConnectionArguments does not clear the values for the port
1760    secureArgsList.getPortArg().clearValues();
1761    if (port != -1)
1762    {
1763      secureArgsList.getPortArg().addValue(String.valueOf(port));
1764      secureArgsList.getPortArg().setPresent(true);
1765    }
1766    else
1767    {
1768      // This is done to be able to call IntegerArgument.getIntValue()
1769      secureArgsList.getPortArg().addValue(secureArgsList.getPortArg().getDefaultValue());
1770    }
1771    secureArgsList.getUseSSLArg().setPresent(state.useSSL);
1772    secureArgsList.getUseStartTLSArg().setPresent(state.useStartTLS);
1773    if (adminUid != null)
1774    {
1775      secureArgsList.getAdminUidArg().addValue(adminUid);
1776      secureArgsList.getAdminUidArg().setPresent(true);
1777    }
1778    if (bindDn != null)
1779    {
1780      secureArgsList.getBindDnArg().addValue(bindDn);
1781      secureArgsList.getBindDnArg().setPresent(true);
1782    }
1783    if (pwdFile != null)
1784    {
1785      secureArgsList.getBindPasswordFileArg().getNameToValueMap().putAll(pwdFile);
1786      for (String value : pwdFile.keySet())
1787      {
1788        secureArgsList.getBindPasswordFileArg().addValue(value);
1789      }
1790      secureArgsList.getBindPasswordFileArg().setPresent(true);
1791    }
1792    else if (bindPwd != null)
1793    {
1794      secureArgsList.getBindPasswordArg().addValue(bindPwd);
1795      secureArgsList.getBindPasswordArg().setPresent(true);
1796    }
1797    state = new State(secureArgsList);
1798  }
1799
1800  /**
1801   * Resets the connection parameters for the LDAPConsoleInteraction object. The
1802   * reset does not apply to the certificate parameters. This is called in order
1803   * the LDAPConnectionConsoleInteraction object to ask for all this connection
1804   * parameters next time we call LDAPConnectionConsoleInteraction.run().
1805   */
1806  public void resetConnectionArguments()
1807  {
1808    secureArgsList.getHostNameArg().clearValues();
1809    secureArgsList.getHostNameArg().setPresent(false);
1810    secureArgsList.getPortArg().clearValues();
1811    secureArgsList.getPortArg().setPresent(false);
1812    //  This is done to be able to call IntegerArgument.getIntValue()
1813    secureArgsList.getPortArg().addValue(secureArgsList.getPortArg().getDefaultValue());
1814    secureArgsList.getBindDnArg().clearValues();
1815    secureArgsList.getBindDnArg().setPresent(false);
1816    secureArgsList.getBindPasswordArg().clearValues();
1817    secureArgsList.getBindPasswordArg().setPresent(false);
1818    secureArgsList.getBindPasswordFileArg().clearValues();
1819    secureArgsList.getBindPasswordFileArg().getNameToValueMap().clear();
1820    secureArgsList.getBindPasswordFileArg().setPresent(false);
1821    state.bindPassword = null;
1822    secureArgsList.getAdminUidArg().clearValues();
1823    secureArgsList.getAdminUidArg().setPresent(false);
1824  }
1825
1826  private void initializeTrustAndKeyManagers() throws ArgumentException
1827  {
1828    // Get trust store info
1829    state.trustManager = getTrustManagerInternal();
1830    // Check if we need client side authentication
1831    state.keyManager = getKeyManagerInternal();
1832    state.trustManagerInitialized = true;
1833  }
1834
1835  /**
1836   * Returns the explicitly provided Admin UID from the user (interactively or
1837   * through the argument).
1838   *
1839   * @return the explicitly provided Admin UID from the user (interactively or
1840   *         through the argument).
1841   */
1842  public String getProvidedAdminUID()
1843  {
1844    return state.providedAdminUID;
1845  }
1846
1847  /**
1848   * Returns the explicitly provided bind DN from the user (interactively or
1849   * through the argument).
1850   *
1851   * @return the explicitly provided bind DN from the user (interactively or
1852   *         through the argument).
1853   */
1854  public String getProvidedBindDN()
1855  {
1856    return state.providedBindDN;
1857  }
1858
1859  /**
1860   * Add the TrustStore of the administration connector of the local instance.
1861   *
1862   * @return true if the local trust store has been added.
1863   */
1864  private boolean useLocalTrustStoreIfPossible()
1865  {
1866    try
1867    {
1868      if (InetAddress.getLocalHost().getHostName().equals(state.hostName)
1869          && secureArgsList.getAdminPortFromConfig() == portNumber)
1870      {
1871        final String trustStoreFileAbsolute = secureArgsList.getTruststoreFileFromConfig();
1872        if (trustStoreFileAbsolute != null)
1873        {
1874          secureArgsList.getTrustStorePathArg().addValue(trustStoreFileAbsolute);
1875          return true;
1876        }
1877      }
1878    }
1879    catch (Exception ex)
1880    {
1881      // do nothing
1882    }
1883    return false;
1884  }
1885
1886  private void throwIfMenuResultNotSucceeded(final MenuResult<?> result)
1887  {
1888    if (!result.isSuccess())
1889    {
1890      throw new RuntimeException("Expected successful menu result, but got " + result);
1891    }
1892  }
1893}