001/*
002 * Copyright 2012-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2012-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2012-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.util;
037
038
039
040import java.io.OutputStream;
041import java.util.concurrent.atomic.AtomicReference;
042import javax.net.SocketFactory;
043import javax.net.ssl.KeyManager;
044import javax.net.ssl.SSLSocketFactory;
045import javax.net.ssl.TrustManager;
046
047import com.unboundid.ldap.sdk.BindRequest;
048import com.unboundid.ldap.sdk.ExtendedResult;
049import com.unboundid.ldap.sdk.InternalSDKHelper;
050import com.unboundid.ldap.sdk.LDAPConnection;
051import com.unboundid.ldap.sdk.LDAPConnectionOptions;
052import com.unboundid.ldap.sdk.LDAPConnectionPool;
053import com.unboundid.ldap.sdk.LDAPException;
054import com.unboundid.ldap.sdk.PostConnectProcessor;
055import com.unboundid.ldap.sdk.ResultCode;
056import com.unboundid.ldap.sdk.ServerSet;
057import com.unboundid.ldap.sdk.SimpleBindRequest;
058import com.unboundid.ldap.sdk.SingleServerSet;
059import com.unboundid.ldap.sdk.StartTLSPostConnectProcessor;
060import com.unboundid.ldap.sdk.extensions.StartTLSExtendedRequest;
061import com.unboundid.util.args.ArgumentException;
062import com.unboundid.util.args.ArgumentParser;
063import com.unboundid.util.args.BooleanArgument;
064import com.unboundid.util.args.DNArgument;
065import com.unboundid.util.args.FileArgument;
066import com.unboundid.util.args.IntegerArgument;
067import com.unboundid.util.args.StringArgument;
068import com.unboundid.util.ssl.AggregateTrustManager;
069import com.unboundid.util.ssl.KeyStoreKeyManager;
070import com.unboundid.util.ssl.SSLUtil;
071import com.unboundid.util.ssl.TrustAllTrustManager;
072import com.unboundid.util.ssl.TrustStoreTrustManager;
073
074import static com.unboundid.util.UtilityMessages.*;
075
076
077
078/**
079 * This class provides a basis for developing command-line tools that have the
080 * ability to communicate with multiple directory servers, potentially with
081 * very different settings for each.  For example, it may be used to help create
082 * tools that move or compare data from one server to another.
083 * <BR><BR>
084 * Each server will be identified by a prefix and/or suffix that will be added
085 * to the argument name (e.g., if the first server has a prefix of "source",
086 * then the "hostname" argument will actually be "sourceHostname").  The
087 * base names for the arguments this class supports include:
088 * <UL>
089 *   <LI>hostname -- Specifies the address of the directory server.  If this
090 *       isn't specified, then a default of "localhost" will be used.</LI>
091 *   <LI>port -- specifies the port number of the directory server.  If this
092 *       isn't specified, then a default port of 389 will be used.</LI>
093 *   <LI>bindDN -- Specifies the DN to use to bind to the directory server using
094 *       simple authentication.  If this isn't specified, then simple
095 *       authentication will not be performed.</LI>
096 *   <LI>bindPassword -- Specifies the password to use when binding with simple
097 *       authentication or a password-based SASL mechanism.</LI>
098 *   <LI>bindPasswordFile -- Specifies the path to a file containing the
099 *       password to use when binding with simple authentication or a
100 *       password-based SASL mechanism.</LI>
101 *   <LI>useSSL -- Indicates that communication with the server should be
102 *       secured using SSL.</LI>
103 *   <LI>useStartTLS -- Indicates that communication with the server should be
104 *       secured using StartTLS.</LI>
105 *   <LI>"--defaultTrust" -- Indicates that the client should use a default,
106 *       non-interactive mechanism for determining whether to trust any
107 *       presented server certificate.</LI>
108 *   <LI>trustAll -- Indicates that the client should trust any certificate
109 *       that the server presents to it.</LI>
110 *   <LI>keyStorePath -- Specifies the path to the key store to use to obtain
111 *       client certificates.</LI>
112 *   <LI>keyStorePassword -- Specifies the password to use to access the
113 *       contents of the key store.</LI>
114 *   <LI>keyStorePasswordFile -- Specifies the path ot a file containing the
115 *       password to use to access the contents of the key store.</LI>
116 *   <LI>keyStoreFormat -- Specifies the format to use for the key store
117 *       file.</LI>
118 *   <LI>trustStorePath -- Specifies the path to the trust store to use to
119 *       obtain client certificates.</LI>
120 *   <LI>trustStorePassword -- Specifies the password to use to access the
121 *       contents of the trust store.</LI>
122 *   <LI>trustStorePasswordFile -- Specifies the path ot a file containing the
123 *       password to use to access the contents of the trust store.</LI>
124 *   <LI>trustStoreFormat -- Specifies the format to use for the trust store
125 *       file.</LI>
126 *   <LI>certNickname -- Specifies the nickname of the client certificate to
127 *       use when performing SSL client authentication.</LI>
128 *   <LI>saslOption -- Specifies a SASL option to use when performing SASL
129 *       authentication.</LI>
130 * </UL>
131 * If SASL authentication is to be used, then a "mech" SASL option must be
132 * provided to specify the name of the SASL mechanism to use.  Depending on the
133 * SASL mechanism, additional SASL options may be required or optional.
134 */
135@Extensible()
136@ThreadSafety(level=ThreadSafetyLevel.INTERFACE_NOT_THREADSAFE)
137public abstract class MultiServerLDAPCommandLineTool
138       extends CommandLineTool
139{
140  // The set of prefixes and suffixes that will be used for server names.
141  private final int numServers;
142  @Nullable private final String[] serverNamePrefixes;
143  @Nullable private final String[] serverNameSuffixes;
144
145  // The set of arguments used to hold information about connection properties.
146  @NotNull private final BooleanArgument[] defaultTrust;
147  @NotNull private final BooleanArgument[] trustAll;
148  @NotNull private final BooleanArgument[] useSSL;
149  @NotNull private final BooleanArgument[] useStartTLS;
150  @NotNull private final DNArgument[]      bindDN;
151  @NotNull private final FileArgument[]    bindPasswordFile;
152  @NotNull private final FileArgument[]    keyStorePasswordFile;
153  @NotNull private final FileArgument[]    trustStorePasswordFile;
154  @NotNull private final IntegerArgument[] port;
155  @NotNull private final StringArgument[]  bindPassword;
156  @NotNull private final StringArgument[]  certificateNickname;
157  @NotNull private final StringArgument[]  host;
158  @NotNull private final StringArgument[]  keyStoreFormat;
159  @NotNull private final StringArgument[]  keyStorePath;
160  @NotNull private final StringArgument[]  keyStorePassword;
161  @NotNull private final StringArgument[]  saslOption;
162  @NotNull private final StringArgument[]  trustStoreFormat;
163  @NotNull private final StringArgument[]  trustStorePath;
164  @NotNull private final StringArgument[]  trustStorePassword;
165
166  // Variables used when creating and authenticating connections.
167  @NotNull private final BindRequest[]      bindRequest;
168  @NotNull private final ServerSet[]        serverSet;
169  @NotNull private final SSLSocketFactory[] startTLSSocketFactory;
170
171  // An atomic reference to an aggregate trust manager that will check a
172  // JVM-default set of trusted issuers, and then its own cache, before
173  // prompting the user about whether to trust the presented certificate chain.
174  // Re-using this trust manager will allow the tool to benefit from a common
175  // cache if multiple connections are needed.
176  @NotNull private final AtomicReference<AggregateTrustManager>
177       promptTrustManager;
178
179
180
181  /**
182   * Creates a new instance of this multi-server LDAP command-line tool.  At
183   * least one of the set of server name prefixes and suffixes must be
184   * non-{@code null}.  If both are non-{@code null}, then they must have the
185   * same number of elements.
186   *
187   * @param  outStream           The output stream to use for standard output.
188   *                             It may be {@code System.out} for the JVM's
189   *                             default standard output stream, {@code null} if
190   *                             no output should be generated, or a custom
191   *                             output stream if the output should be sent to
192   *                             an alternate location.
193   * @param  errStream           The output stream to use for standard error.
194   *                             It may be {@code System.err} for the JVM's
195   *                             default standard error stream, {@code null} if
196   *                             no output should be generated, or a custom
197   *                             output stream if the output should be sent to
198   *                             an alternate location.
199   * @param  serverNamePrefixes  The prefixes to include before the names of
200   *                             each of the parameters to identify each server.
201   *                             It may be {@code null} if only suffixes should
202   *                             be used.
203   * @param  serverNameSuffixes  The suffixes to include after the names of each
204   *                             of the parameters to identify each server.  It
205   *                             may be {@code null} if only prefixes should be
206   *                             used.
207   *
208   * @throws  LDAPSDKUsageException  If both the sets of server name prefixes
209   *                                 and suffixes are {@code null} or empty, or
210   *                                 if both sets are non-{@code null} but have
211   *                                 different numbers of elements.
212   */
213  public MultiServerLDAPCommandLineTool(@Nullable final OutputStream outStream,
214              @Nullable final OutputStream errStream,
215              @Nullable final String[] serverNamePrefixes,
216              @Nullable final String[] serverNameSuffixes)
217         throws LDAPSDKUsageException
218  {
219    super(outStream, errStream);
220
221    promptTrustManager = new AtomicReference<>();
222
223    this.serverNamePrefixes = serverNamePrefixes;
224    this.serverNameSuffixes = serverNameSuffixes;
225
226    if (serverNamePrefixes == null)
227    {
228      if (serverNameSuffixes == null)
229      {
230        throw new LDAPSDKUsageException(
231             ERR_MULTI_LDAP_TOOL_PREFIXES_AND_SUFFIXES_NULL.get());
232      }
233      else
234      {
235        numServers = serverNameSuffixes.length;
236      }
237    }
238    else
239    {
240      numServers = serverNamePrefixes.length;
241
242      if ((serverNameSuffixes != null) &&
243          (serverNamePrefixes.length != serverNameSuffixes.length))
244      {
245        throw new LDAPSDKUsageException(
246             ERR_MULTI_LDAP_TOOL_PREFIXES_AND_SUFFIXES_MISMATCH.get());
247      }
248    }
249
250    if (numServers == 0)
251    {
252      throw new LDAPSDKUsageException(
253           ERR_MULTI_LDAP_TOOL_PREFIXES_AND_SUFFIXES_EMPTY.get());
254    }
255
256    defaultTrust           = new BooleanArgument[numServers];
257    trustAll               = new BooleanArgument[numServers];
258    useSSL                 = new BooleanArgument[numServers];
259    useStartTLS            = new BooleanArgument[numServers];
260    bindDN                 = new DNArgument[numServers];
261    bindPasswordFile       = new FileArgument[numServers];
262    keyStorePasswordFile   = new FileArgument[numServers];
263    trustStorePasswordFile = new FileArgument[numServers];
264    port                   = new IntegerArgument[numServers];
265    bindPassword           = new StringArgument[numServers];
266    certificateNickname    = new StringArgument[numServers];
267    host                   = new StringArgument[numServers];
268    keyStoreFormat         = new StringArgument[numServers];
269    keyStorePath           = new StringArgument[numServers];
270    keyStorePassword       = new StringArgument[numServers];
271    saslOption             = new StringArgument[numServers];
272    trustStoreFormat       = new StringArgument[numServers];
273    trustStorePath         = new StringArgument[numServers];
274    trustStorePassword     = new StringArgument[numServers];
275
276    bindRequest           = new BindRequest[numServers];
277    serverSet             = new ServerSet[numServers];
278    startTLSSocketFactory = new SSLSocketFactory[numServers];
279  }
280
281
282
283  /**
284   * {@inheritDoc}
285   */
286  @Override()
287  public final void addToolArguments(@NotNull final ArgumentParser parser)
288         throws ArgumentException
289  {
290    for (int i=0; i < numServers; i++)
291    {
292      final StringBuilder groupNameBuffer = new StringBuilder();
293      if (serverNamePrefixes != null)
294      {
295        final String prefix = serverNamePrefixes[i].replace('-', ' ').trim();
296        groupNameBuffer.append(StaticUtils.capitalize(prefix, true));
297      }
298
299      if (serverNameSuffixes != null)
300      {
301        if (groupNameBuffer.length() > 0)
302        {
303          groupNameBuffer.append(' ');
304        }
305
306        final String suffix = serverNameSuffixes[i].replace('-', ' ').trim();
307        groupNameBuffer.append(StaticUtils.capitalize(suffix, true));
308      }
309
310      groupNameBuffer.append(' ');
311      groupNameBuffer.append(INFO_MULTI_LDAP_TOOL_GROUP_CONN_AND_AUTH.get());
312      final String groupName = groupNameBuffer.toString();
313
314
315      host[i] = new StringArgument(null, genArgName(i, "hostname"), true, 1,
316           INFO_LDAP_TOOL_PLACEHOLDER_HOST.get(),
317           INFO_LDAP_TOOL_DESCRIPTION_HOST.get(), "localhost");
318      if (includeAlternateLongIdentifiers())
319      {
320        host[i].addLongIdentifier(genDashedArgName(i, "hostname"), true);
321        host[i].addLongIdentifier(genArgName(i, "host"), true);
322        host[i].addLongIdentifier(genDashedArgName(i, "host"), true);
323        host[i].addLongIdentifier(genArgName(i, "address"), true);
324        host[i].addLongIdentifier(genDashedArgName(i, "address"), true);
325      }
326      host[i].setArgumentGroupName(groupName);
327      parser.addArgument(host[i]);
328
329      port[i] = new IntegerArgument(null, genArgName(i, "port"), true, 1,
330           INFO_LDAP_TOOL_PLACEHOLDER_PORT.get(),
331           INFO_LDAP_TOOL_DESCRIPTION_PORT.get(), 1, 65_535, 389);
332      port[i].setArgumentGroupName(groupName);
333      if (includeAlternateLongIdentifiers())
334      {
335        port[i].addLongIdentifier(genDashedArgName(i, "port"), true);
336      }
337      parser.addArgument(port[i]);
338
339      bindDN[i] = new DNArgument(null, genArgName(i, "bindDN"), false, 1,
340           INFO_LDAP_TOOL_PLACEHOLDER_DN.get(),
341           INFO_LDAP_TOOL_DESCRIPTION_BIND_DN.get());
342      if (includeAlternateLongIdentifiers())
343      {
344        bindDN[i].addLongIdentifier(genDashedArgName(i, "bind-dn"), true);
345      }
346      bindDN[i].setArgumentGroupName(groupName);
347      parser.addArgument(bindDN[i]);
348
349      bindPassword[i] = new StringArgument(null, genArgName(i, "bindPassword"),
350           false, 1, INFO_LDAP_TOOL_PLACEHOLDER_PASSWORD.get(),
351           INFO_LDAP_TOOL_DESCRIPTION_BIND_PW.get());
352      if (includeAlternateLongIdentifiers())
353      {
354        bindPassword[i].addLongIdentifier(genDashedArgName(i, "bind-password"),
355             true);
356      }
357      bindPassword[i].setSensitive(true);
358      bindPassword[i].setArgumentGroupName(groupName);
359      parser.addArgument(bindPassword[i]);
360
361      bindPasswordFile[i] = new FileArgument(null,
362           genArgName(i, "bindPasswordFile"), false, 1,
363           INFO_LDAP_TOOL_PLACEHOLDER_PATH.get(),
364           INFO_LDAP_TOOL_DESCRIPTION_BIND_PW_FILE.get(), true, true, true,
365           false);
366      if (includeAlternateLongIdentifiers())
367      {
368        bindPasswordFile[i].addLongIdentifier(genDashedArgName(i,
369             "bind-password-file"), true);
370      }
371      bindPasswordFile[i].setArgumentGroupName(groupName);
372      parser.addArgument(bindPasswordFile[i]);
373
374      useSSL[i] = new BooleanArgument(null, genArgName(i, "useSSL"), 1,
375           INFO_LDAP_TOOL_DESCRIPTION_USE_SSL.get());
376      if (includeAlternateLongIdentifiers())
377      {
378        useSSL[i].addLongIdentifier(genDashedArgName(i, "use-ssl"), true);
379      }
380      useSSL[i].setArgumentGroupName(groupName);
381      parser.addArgument(useSSL[i]);
382
383      useStartTLS[i] = new BooleanArgument(null, genArgName(i, "useStartTLS"),
384           1, INFO_LDAP_TOOL_DESCRIPTION_USE_START_TLS.get());
385      if (includeAlternateLongIdentifiers())
386      {
387        useStartTLS[i].addLongIdentifier(genDashedArgName(i, "use-start-tls"),
388             true);
389      }
390      useStartTLS[i].setArgumentGroupName(groupName);
391      parser.addArgument(useStartTLS[i]);
392
393      final String defaultTrustArgDesc;
394      if (InternalSDKHelper.getPingIdentityServerRoot() != null)
395      {
396        defaultTrustArgDesc =
397             INFO_LDAP_TOOL_DESCRIPTION_DEFAULT_TRUST_WITH_PING_DS.get();
398      }
399      else
400      {
401        defaultTrustArgDesc =
402             INFO_LDAP_TOOL_DESCRIPTION_DEFAULT_TRUST_WITHOUT_PING_DS.get();
403      }
404      defaultTrust[i] = new BooleanArgument(null,
405           genArgName(i, "defaultTrust"), 1, defaultTrustArgDesc);
406      defaultTrust[i].setArgumentGroupName(groupName);
407      if (includeAlternateLongIdentifiers())
408      {
409        defaultTrust[i].addLongIdentifier(
410             genDashedArgName(i, "default-trust"), true);
411        defaultTrust[i].addLongIdentifier(
412             genArgName(i, "useDefaultTrust"), true);
413        defaultTrust[i].addLongIdentifier(
414             genDashedArgName(i, "use-default-trust"), true);
415      }
416      parser.addArgument(defaultTrust[i]);
417
418      trustAll[i] = new BooleanArgument(null, genArgName(i, "trustAll"), 1,
419           INFO_LDAP_TOOL_DESCRIPTION_TRUST_ALL.get());
420      if (includeAlternateLongIdentifiers())
421      {
422        trustAll[i].addLongIdentifier(genDashedArgName(i, "trust-all"), true);
423      }
424      trustAll[i].setArgumentGroupName(groupName);
425      parser.addArgument(trustAll[i]);
426
427      keyStorePath[i] = new StringArgument(null, genArgName(i, "keyStorePath"),
428           false, 1, INFO_LDAP_TOOL_PLACEHOLDER_PATH.get(),
429           INFO_LDAP_TOOL_DESCRIPTION_KEY_STORE_PATH.get());
430      if (includeAlternateLongIdentifiers())
431      {
432        keyStorePath[i].addLongIdentifier(genDashedArgName(i, "key-store-path"),
433             true);
434      }
435      keyStorePath[i].setArgumentGroupName(groupName);
436      parser.addArgument(keyStorePath[i]);
437
438      keyStorePassword[i] = new StringArgument(null,
439           genArgName(i, "keyStorePassword"), false, 1,
440           INFO_LDAP_TOOL_PLACEHOLDER_PASSWORD.get(),
441           INFO_LDAP_TOOL_DESCRIPTION_KEY_STORE_PASSWORD.get());
442      if (includeAlternateLongIdentifiers())
443      {
444        keyStorePassword[i].addLongIdentifier(genDashedArgName(i,
445             "key-store-password"), true);
446      }
447      keyStorePassword[i].setSensitive(true);
448      keyStorePassword[i].setArgumentGroupName(groupName);
449      parser.addArgument(keyStorePassword[i]);
450
451      keyStorePasswordFile[i] = new FileArgument(null,
452           genArgName(i, "keyStorePasswordFile"), false, 1,
453           INFO_LDAP_TOOL_PLACEHOLDER_PATH.get(),
454           INFO_LDAP_TOOL_DESCRIPTION_KEY_STORE_PASSWORD_FILE.get(), true,
455           true, true, false);
456      if (includeAlternateLongIdentifiers())
457      {
458        keyStorePasswordFile[i].addLongIdentifier(genDashedArgName(i,
459             "key-store-password-file"), true);
460      }
461      keyStorePasswordFile[i].setArgumentGroupName(groupName);
462      parser.addArgument(keyStorePasswordFile[i]);
463
464      keyStoreFormat[i] = new StringArgument(null,
465           genArgName(i, "keyStoreFormat"), false, 1,
466           INFO_LDAP_TOOL_PLACEHOLDER_FORMAT.get(),
467           INFO_LDAP_TOOL_DESCRIPTION_KEY_STORE_FORMAT.get());
468      if (includeAlternateLongIdentifiers())
469      {
470        keyStoreFormat[i].addLongIdentifier(genDashedArgName(i,
471             "key-store-format"), true);
472        keyStoreFormat[i].addLongIdentifier(genArgName(i, "keyStoreType"),
473             true);
474        keyStoreFormat[i].addLongIdentifier(genDashedArgName(i,
475             "key-store-type"), true);
476      }
477      keyStoreFormat[i].setArgumentGroupName(groupName);
478      parser.addArgument(keyStoreFormat[i]);
479
480      trustStorePath[i] = new StringArgument(null,
481           genArgName(i, "trustStorePath"), false, 1,
482           INFO_LDAP_TOOL_PLACEHOLDER_PATH.get(),
483           INFO_LDAP_TOOL_DESCRIPTION_TRUST_STORE_PATH.get());
484      if (includeAlternateLongIdentifiers())
485      {
486        trustStorePath[i].addLongIdentifier(genDashedArgName(i,
487             "trust-store-path"), true);
488      }
489      trustStorePath[i].setArgumentGroupName(groupName);
490      parser.addArgument(trustStorePath[i]);
491
492      trustStorePassword[i] = new StringArgument(null,
493           genArgName(i, "trustStorePassword"), false, 1,
494           INFO_LDAP_TOOL_PLACEHOLDER_PASSWORD.get(),
495           INFO_LDAP_TOOL_DESCRIPTION_TRUST_STORE_PASSWORD.get());
496      if (includeAlternateLongIdentifiers())
497      {
498        trustStorePassword[i].addLongIdentifier(genDashedArgName(i,
499             "trust-store-password"), true);
500      }
501      trustStorePassword[i].setSensitive(true);
502      trustStorePassword[i].setArgumentGroupName(groupName);
503      parser.addArgument(trustStorePassword[i]);
504
505      trustStorePasswordFile[i] = new FileArgument(null,
506           genArgName(i, "trustStorePasswordFile"), false, 1,
507           INFO_LDAP_TOOL_PLACEHOLDER_PATH.get(),
508           INFO_LDAP_TOOL_DESCRIPTION_TRUST_STORE_PASSWORD_FILE.get(), true,
509           true, true, false);
510      if (includeAlternateLongIdentifiers())
511      {
512        trustStorePasswordFile[i].addLongIdentifier(genDashedArgName(i,
513             "trust-store-password-file"), true);
514      }
515      trustStorePasswordFile[i].setArgumentGroupName(groupName);
516      parser.addArgument(trustStorePasswordFile[i]);
517
518      trustStoreFormat[i] = new StringArgument(null,
519           genArgName(i, "trustStoreFormat"), false, 1,
520           INFO_LDAP_TOOL_PLACEHOLDER_FORMAT.get(),
521           INFO_LDAP_TOOL_DESCRIPTION_TRUST_STORE_FORMAT.get());
522      if (includeAlternateLongIdentifiers())
523      {
524        trustStoreFormat[i].addLongIdentifier(genDashedArgName(i,
525             "trust-store-format"), true);
526        trustStoreFormat[i].addLongIdentifier(genArgName(i, "trustStoreType"),
527             true);
528        trustStoreFormat[i].addLongIdentifier(genDashedArgName(i,
529             "trust-store-type"), true);
530      }
531      trustStoreFormat[i].setArgumentGroupName(groupName);
532      parser.addArgument(trustStoreFormat[i]);
533
534      certificateNickname[i] = new StringArgument(null,
535           genArgName(i, "certNickname"), false, 1,
536           INFO_LDAP_TOOL_PLACEHOLDER_CERT_NICKNAME.get(),
537           INFO_LDAP_TOOL_DESCRIPTION_CERT_NICKNAME.get());
538      if (includeAlternateLongIdentifiers())
539      {
540        certificateNickname[i].addLongIdentifier(genDashedArgName(i,
541             "cert-nickname"), true);
542        certificateNickname[i].addLongIdentifier(genArgName(i,
543             "certificateNickname"), true);
544        certificateNickname[i].addLongIdentifier(genDashedArgName(i,
545             "certificate-nickname"), true);
546      }
547      certificateNickname[i].setArgumentGroupName(groupName);
548      parser.addArgument(certificateNickname[i]);
549
550      saslOption[i] = new StringArgument(null, genArgName(i, "saslOption"),
551           false, 0, INFO_LDAP_TOOL_PLACEHOLDER_SASL_OPTION.get(),
552           INFO_LDAP_TOOL_DESCRIPTION_SASL_OPTION.get());
553      if (includeAlternateLongIdentifiers())
554      {
555        saslOption[i].addLongIdentifier(genDashedArgName(i, "sasl-option"),
556             true);
557      }
558      saslOption[i].setArgumentGroupName(groupName);
559      parser.addArgument(saslOption[i]);
560
561      parser.addDependentArgumentSet(bindDN[i], bindPassword[i],
562           bindPasswordFile[i]);
563
564      parser.addExclusiveArgumentSet(useSSL[i], useStartTLS[i]);
565      parser.addExclusiveArgumentSet(bindPassword[i], bindPasswordFile[i]);
566      parser.addExclusiveArgumentSet(keyStorePassword[i],
567           keyStorePasswordFile[i]);
568      parser.addExclusiveArgumentSet(trustStorePassword[i],
569           trustStorePasswordFile[i]);
570      parser.addExclusiveArgumentSet(trustAll[i], trustStorePath[i]);
571      parser.addExclusiveArgumentSet(trustAll[i], defaultTrust[i]);
572    }
573
574    addNonLDAPArguments(parser);
575  }
576
577
578
579  /**
580   * Constructs the name to use for an argument from the given base and the
581   * appropriate prefix and suffix.
582   *
583   * @param  index  The index into the set of prefixes and suffixes.
584   * @param  base   The base name for the argument.
585   *
586   * @return  The constructed argument name.
587   */
588  @NotNull()
589  private String genArgName(final int index, @NotNull final String base)
590  {
591    final StringBuilder buffer = new StringBuilder();
592
593    if (serverNamePrefixes != null)
594    {
595      buffer.append(serverNamePrefixes[index]);
596
597      if (base.equals("saslOption"))
598      {
599        buffer.append("SASLOption");
600      }
601      else
602      {
603        buffer.append(StaticUtils.capitalize(base));
604      }
605    }
606    else
607    {
608      buffer.append(base);
609    }
610
611    if (serverNameSuffixes != null)
612    {
613      buffer.append(serverNameSuffixes[index]);
614    }
615
616    return buffer.toString();
617  }
618
619
620
621  /**
622   * Constructs the name to use for an argument from the given base and the
623   * appropriate prefix and suffix.  In this case, dashes will be used to
624   * separate words rather than capitalization.
625   *
626   * @param  index  The index into the set of prefixes and suffixes.
627   * @param  base   The base name for the argument.
628   *
629   * @return  The constructed argument name.
630   */
631  @NotNull()
632  private String genDashedArgName(final int index, @NotNull final String base)
633  {
634    final StringBuilder buffer = new StringBuilder();
635
636    if (serverNamePrefixes != null)
637    {
638      buffer.append(serverNamePrefixes[index]);
639      buffer.append('-');
640    }
641
642    buffer.append(base);
643
644    if (serverNameSuffixes != null)
645    {
646      buffer.append('-');
647      buffer.append(serverNameSuffixes[index]);
648    }
649
650    return buffer.toString();
651  }
652
653
654
655  /**
656   * Indicates whether the LDAP-specific arguments should include alternate
657   * versions of all long identifiers that consist of multiple words so that
658   * they are available in both camelCase and dash-separated versions.
659   *
660   * @return  {@code true} if this tool should provide multiple versions of
661   *          long identifiers for LDAP-specific arguments, or {@code false} if
662   *          not.
663   */
664  protected boolean includeAlternateLongIdentifiers()
665  {
666    return false;
667  }
668
669
670
671  /**
672   * Adds the arguments needed by this command-line tool to the provided
673   * argument parser which are not related to connecting or authenticating to
674   * the directory server.
675   *
676   * @param  parser  The argument parser to which the arguments should be added.
677   *
678   * @throws  ArgumentException  If a problem occurs while adding the arguments.
679   */
680  public abstract void addNonLDAPArguments(@NotNull ArgumentParser parser)
681         throws ArgumentException;
682
683
684
685  /**
686   * {@inheritDoc}
687   */
688  @Override()
689  public final void doExtendedArgumentValidation()
690         throws ArgumentException
691  {
692    doExtendedNonLDAPArgumentValidation();
693  }
694
695
696
697  /**
698   * Performs any necessary processing that should be done to ensure that the
699   * provided set of command-line arguments were valid.  This method will be
700   * called after the basic argument parsing has been performed and after all
701   * LDAP-specific argument validation has been processed, and immediately
702   * before the {@link CommandLineTool#doToolProcessing} method is invoked.
703   *
704   * @throws  ArgumentException  If there was a problem with the command-line
705   *                             arguments provided to this program.
706   */
707  public void doExtendedNonLDAPArgumentValidation()
708         throws ArgumentException
709  {
710    // No processing will be performed by default.
711  }
712
713
714
715  /**
716   * Retrieves the connection options that should be used for connections that
717   * are created with this command line tool.  Subclasses may override this
718   * method to use a custom set of connection options.
719   *
720   * @return  The connection options that should be used for connections that
721   *          are created with this command line tool.
722   */
723  @NotNull()
724  public LDAPConnectionOptions getConnectionOptions()
725  {
726    return new LDAPConnectionOptions();
727  }
728
729
730
731  /**
732   * Retrieves a connection that may be used to communicate with the indicated
733   * directory server.
734   * <BR><BR>
735   * Note that this method is threadsafe and may be invoked by multiple threads
736   * accessing the same instance only while that instance is in the process of
737   * invoking the {@link #doToolProcessing} method.
738   *
739   * @param  serverIndex  The zero-based index of the server to which the
740   *                      connection should be established.
741   *
742   * @return  A connection that may be used to communicate with the indicated
743   *          directory server.
744   *
745   * @throws  LDAPException  If a problem occurs while creating the connection.
746   */
747  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
748  @NotNull()
749  public final LDAPConnection getConnection(final int serverIndex)
750         throws LDAPException
751  {
752    final LDAPConnection connection = getUnauthenticatedConnection(serverIndex);
753
754    try
755    {
756      if (bindRequest[serverIndex] != null)
757      {
758        connection.bind(bindRequest[serverIndex]);
759      }
760    }
761    catch (final LDAPException le)
762    {
763      Debug.debugException(le);
764      connection.close();
765      throw le;
766    }
767
768    return connection;
769  }
770
771
772
773  /**
774   * Retrieves an unauthenticated connection that may be used to communicate
775   * with the indicated directory server.
776   * <BR><BR>
777   * Note that this method is threadsafe and may be invoked by multiple threads
778   * accessing the same instance only while that instance is in the process of
779   * invoking the {@link #doToolProcessing} method.
780   *
781   * @param  serverIndex  The zero-based index of the server to which the
782   *                      connection should be established.
783   *
784   * @return  An unauthenticated connection that may be used to communicate with
785   *          the indicated directory server.
786   *
787   * @throws  LDAPException  If a problem occurs while creating the connection.
788   */
789  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
790  @NotNull()
791  public final LDAPConnection getUnauthenticatedConnection(
792                                   final int serverIndex)
793         throws LDAPException
794  {
795    if (serverSet[serverIndex] == null)
796    {
797      serverSet[serverIndex]   = createServerSet(serverIndex);
798      bindRequest[serverIndex] = createBindRequest(serverIndex);
799    }
800
801    final LDAPConnection connection = serverSet[serverIndex].getConnection();
802
803    if (useStartTLS[serverIndex].isPresent())
804    {
805      try
806      {
807        final ExtendedResult extendedResult =
808             connection.processExtendedOperation(new StartTLSExtendedRequest(
809                  startTLSSocketFactory[serverIndex]));
810        if (! extendedResult.getResultCode().equals(ResultCode.SUCCESS))
811        {
812          throw new LDAPException(extendedResult.getResultCode(),
813               ERR_LDAP_TOOL_START_TLS_FAILED.get(
814                    extendedResult.getDiagnosticMessage()));
815        }
816      }
817      catch (final LDAPException le)
818      {
819        Debug.debugException(le);
820        connection.close();
821        throw le;
822      }
823    }
824
825    return connection;
826  }
827
828
829
830  /**
831   * Retrieves a connection pool that may be used to communicate with the
832   * indicated directory server.
833   * <BR><BR>
834   * Note that this method is threadsafe and may be invoked by multiple threads
835   * accessing the same instance only while that instance is in the process of
836   * invoking the {@link #doToolProcessing} method.
837   *
838   * @param  serverIndex         The zero-based index of the server to which the
839   *                             connection should be established.
840   * @param  initialConnections  The number of connections that should be
841   *                             initially established in the pool.
842   * @param  maxConnections      The maximum number of connections to maintain
843   *                             in the pool.
844   *
845   * @return  A connection that may be used to communicate with the indicated
846   *          directory server.
847   *
848   * @throws  LDAPException  If a problem occurs while creating the connection
849   *                         pool.
850   */
851  @ThreadSafety(level=ThreadSafetyLevel.METHOD_THREADSAFE)
852  @NotNull()
853  public final LDAPConnectionPool getConnectionPool(
854                                       final int serverIndex,
855                                       final int initialConnections,
856                                       final int maxConnections)
857            throws LDAPException
858  {
859    if (serverSet[serverIndex] == null)
860    {
861      serverSet[serverIndex]   = createServerSet(serverIndex);
862      bindRequest[serverIndex] = createBindRequest(serverIndex);
863    }
864
865    PostConnectProcessor postConnectProcessor = null;
866    if (useStartTLS[serverIndex].isPresent())
867    {
868      postConnectProcessor = new StartTLSPostConnectProcessor(
869           startTLSSocketFactory[serverIndex]);
870    }
871
872    return new LDAPConnectionPool(serverSet[serverIndex],
873         bindRequest[serverIndex], initialConnections, maxConnections,
874         postConnectProcessor);
875  }
876
877
878
879  /**
880   * Creates the server set to use when creating connections or connection
881   * pools.
882   *
883   * @param  serverIndex  The zero-based index of the server to which the
884   *                      connection should be established.
885   *
886   * @return  The server set to use when creating connections or connection
887   *          pools.
888   *
889   * @throws  LDAPException  If a problem occurs while creating the server set.
890   */
891  @NotNull()
892  public final ServerSet createServerSet(final int serverIndex)
893         throws LDAPException
894  {
895    final SSLUtil sslUtil = createSSLUtil(serverIndex);
896
897    SocketFactory socketFactory = null;
898    if (useSSL[serverIndex].isPresent())
899    {
900      try
901      {
902        socketFactory = sslUtil.createSSLSocketFactory();
903      }
904      catch (final Exception e)
905      {
906        Debug.debugException(e);
907        throw new LDAPException(ResultCode.LOCAL_ERROR,
908             ERR_LDAP_TOOL_CANNOT_CREATE_SSL_SOCKET_FACTORY.get(
909                  StaticUtils.getExceptionMessage(e)), e);
910      }
911    }
912    else if (useStartTLS[serverIndex].isPresent())
913    {
914      try
915      {
916        startTLSSocketFactory[serverIndex] = sslUtil.createSSLSocketFactory();
917      }
918      catch (final Exception e)
919      {
920        Debug.debugException(e);
921        throw new LDAPException(ResultCode.LOCAL_ERROR,
922             ERR_LDAP_TOOL_CANNOT_CREATE_SSL_SOCKET_FACTORY.get(
923                  StaticUtils.getExceptionMessage(e)), e);
924      }
925    }
926
927    return new SingleServerSet(host[serverIndex].getValue(),
928         port[serverIndex].getValue(), socketFactory, getConnectionOptions());
929  }
930
931
932
933  /**
934   * Creates the SSLUtil instance to use for secure communication.
935   *
936   * @param  serverIndex  The zero-based index of the server to which the
937   *                      connection should be established.
938   *
939   * @return  The SSLUtil instance to use for secure communication, or
940   *          {@code null} if secure communication is not needed.
941   *
942   * @throws  LDAPException  If a problem occurs while creating the SSLUtil
943   *                         instance.
944   */
945  @Nullable()
946  public final SSLUtil createSSLUtil(final int serverIndex)
947         throws LDAPException
948  {
949    if (useSSL[serverIndex].isPresent() || useStartTLS[serverIndex].isPresent())
950    {
951      KeyManager keyManager = null;
952      if (keyStorePath[serverIndex].isPresent())
953      {
954        char[] pw = null;
955        if (keyStorePassword[serverIndex].isPresent())
956        {
957          pw = keyStorePassword[serverIndex].getValue().toCharArray();
958        }
959        else if (keyStorePasswordFile[serverIndex].isPresent())
960        {
961          try
962          {
963            pw = getPasswordFileReader().readPassword(
964                 keyStorePasswordFile[serverIndex].getValue());
965          }
966          catch (final Exception e)
967          {
968            Debug.debugException(e);
969            throw new LDAPException(ResultCode.LOCAL_ERROR,
970                 ERR_LDAP_TOOL_CANNOT_READ_KEY_STORE_PASSWORD.get(
971                      StaticUtils.getExceptionMessage(e)), e);
972          }
973        }
974
975        try
976        {
977          keyManager = new KeyStoreKeyManager(
978               keyStorePath[serverIndex].getValue(), pw,
979               keyStoreFormat[serverIndex].getValue(),
980               certificateNickname[serverIndex].getValue(), true);
981        }
982        catch (final Exception e)
983        {
984          Debug.debugException(e);
985          throw new LDAPException(ResultCode.LOCAL_ERROR,
986               ERR_LDAP_TOOL_CANNOT_CREATE_KEY_MANAGER.get(
987                    StaticUtils.getExceptionMessage(e)), e);
988        }
989      }
990
991      TrustManager tm;
992      if (trustAll[serverIndex].isPresent())
993      {
994        tm = new TrustAllTrustManager(false);
995      }
996      else if (trustStorePath[serverIndex].isPresent())
997      {
998        char[] pw = null;
999        if (trustStorePassword[serverIndex].isPresent())
1000        {
1001          pw = trustStorePassword[serverIndex].getValue().toCharArray();
1002        }
1003        else if (trustStorePasswordFile[serverIndex].isPresent())
1004        {
1005          try
1006          {
1007            pw = getPasswordFileReader().readPassword(
1008                 trustStorePasswordFile[serverIndex].getValue());
1009          }
1010          catch (final Exception e)
1011          {
1012            Debug.debugException(e);
1013            throw new LDAPException(ResultCode.LOCAL_ERROR,
1014                 ERR_LDAP_TOOL_CANNOT_READ_TRUST_STORE_PASSWORD.get(
1015                      StaticUtils.getExceptionMessage(e)), e);
1016          }
1017        }
1018
1019        final TrustStoreTrustManager trustStoreTrustManager =
1020             new TrustStoreTrustManager(trustStorePath[serverIndex].getValue(),
1021                  pw, trustStoreFormat[serverIndex].getValue(), true);
1022        if (defaultTrust[serverIndex].isPresent())
1023        {
1024          tm = InternalSDKHelper.getPreferredNonInteractiveTrustManagerChain(
1025               trustStoreTrustManager);
1026        }
1027        else
1028        {
1029          tm = trustStoreTrustManager;
1030        }
1031      }
1032      else if (defaultTrust[serverIndex].isPresent())
1033      {
1034        tm = InternalSDKHelper.getPreferredNonInteractiveTrustManagerChain();
1035      }
1036      else
1037      {
1038        tm = promptTrustManager.get();
1039        if (tm == null)
1040        {
1041          final AggregateTrustManager atm =
1042               InternalSDKHelper.getPreferredPromptTrustManagerChain(null);
1043          if (promptTrustManager.compareAndSet(null, atm))
1044          {
1045            tm = atm;
1046          }
1047          else
1048          {
1049            tm = promptTrustManager.get();
1050          }
1051        }
1052      }
1053
1054      return new SSLUtil(keyManager, tm);
1055    }
1056    else
1057    {
1058      return null;
1059    }
1060  }
1061
1062
1063
1064  /**
1065   * Creates the bind request to use to authenticate to the indicated server.
1066   *
1067   * @param  serverIndex  The zero-based index of the server to which the
1068   *                      connection should be established.
1069   *
1070   * @return  The bind request to use to authenticate to the indicated server,
1071   *          or {@code null} if no bind should be performed.
1072   *
1073   * @throws  LDAPException  If a problem occurs while creating the bind
1074   *                         request.
1075   */
1076  @Nullable()
1077  public final BindRequest createBindRequest(final int serverIndex)
1078         throws LDAPException
1079  {
1080    final String pw;
1081    if (bindPassword[serverIndex].isPresent())
1082    {
1083      pw = bindPassword[serverIndex].getValue();
1084    }
1085    else if (bindPasswordFile[serverIndex].isPresent())
1086    {
1087      try
1088      {
1089        pw = new String(getPasswordFileReader().readPassword(
1090             bindPasswordFile[serverIndex].getValue()));
1091      }
1092      catch (final Exception e)
1093      {
1094        Debug.debugException(e);
1095        throw new LDAPException(ResultCode.LOCAL_ERROR,
1096             ERR_LDAP_TOOL_CANNOT_READ_BIND_PASSWORD.get(
1097                  StaticUtils.getExceptionMessage(e)), e);
1098      }
1099    }
1100    else
1101    {
1102      pw = null;
1103    }
1104
1105    if (saslOption[serverIndex].isPresent())
1106    {
1107      final String dnStr;
1108      if (bindDN[serverIndex].isPresent())
1109      {
1110        dnStr = bindDN[serverIndex].getValue().toString();
1111      }
1112      else
1113      {
1114        dnStr = null;
1115      }
1116
1117      return SASLUtils.createBindRequest(dnStr, pw, null,
1118           saslOption[serverIndex].getValues());
1119    }
1120    else if (bindDN[serverIndex].isPresent())
1121    {
1122      return new SimpleBindRequest(bindDN[serverIndex].getValue(), pw);
1123    }
1124    else
1125    {
1126      return null;
1127    }
1128  }
1129}