001/*
002 * Copyright 2009-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2009-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) 2009-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.ldap.sdk.examples;
037
038
039
040import java.io.IOException;
041import java.io.OutputStream;
042import java.io.Serializable;
043import java.text.ParseException;
044import java.util.ArrayList;
045import java.util.LinkedHashMap;
046import java.util.List;
047import java.util.Set;
048import java.util.concurrent.CyclicBarrier;
049import java.util.concurrent.atomic.AtomicBoolean;
050import java.util.concurrent.atomic.AtomicInteger;
051import java.util.concurrent.atomic.AtomicLong;
052
053import com.unboundid.ldap.sdk.Control;
054import com.unboundid.ldap.sdk.LDAPConnection;
055import com.unboundid.ldap.sdk.LDAPConnectionOptions;
056import com.unboundid.ldap.sdk.LDAPException;
057import com.unboundid.ldap.sdk.ResultCode;
058import com.unboundid.ldap.sdk.SearchScope;
059import com.unboundid.ldap.sdk.Version;
060import com.unboundid.ldap.sdk.controls.AuthorizationIdentityRequestControl;
061import com.unboundid.ldap.sdk.experimental.
062            DraftBeheraLDAPPasswordPolicy10RequestControl;
063import com.unboundid.util.ColumnFormatter;
064import com.unboundid.util.Debug;
065import com.unboundid.util.FixedRateBarrier;
066import com.unboundid.util.FormattableColumn;
067import com.unboundid.util.HorizontalAlignment;
068import com.unboundid.util.LDAPCommandLineTool;
069import com.unboundid.util.NotNull;
070import com.unboundid.util.Nullable;
071import com.unboundid.util.ObjectPair;
072import com.unboundid.util.OutputFormat;
073import com.unboundid.util.RateAdjustor;
074import com.unboundid.util.ResultCodeCounter;
075import com.unboundid.util.StaticUtils;
076import com.unboundid.util.ThreadSafety;
077import com.unboundid.util.ThreadSafetyLevel;
078import com.unboundid.util.ValuePattern;
079import com.unboundid.util.WakeableSleeper;
080import com.unboundid.util.args.ArgumentException;
081import com.unboundid.util.args.ArgumentParser;
082import com.unboundid.util.args.BooleanArgument;
083import com.unboundid.util.args.ControlArgument;
084import com.unboundid.util.args.FileArgument;
085import com.unboundid.util.args.IntegerArgument;
086import com.unboundid.util.args.ScopeArgument;
087import com.unboundid.util.args.StringArgument;
088
089
090
091/**
092 * This class provides a tool that can be used to test authentication processing
093 * in an LDAP directory server using multiple threads.  Each authentication will
094 * consist of two operations:  a search to find the target entry followed by a
095 * bind to verify the credentials for that user.  The search will use the given
096 * base DN and filter, either or both of which may be a value pattern as
097 * described in the {@link ValuePattern} class.  This makes it possible to
098 * search over a range of entries rather than repeatedly performing searches
099 * with the same base DN and filter.
100 * <BR><BR>
101 * Some of the APIs demonstrated by this example include:
102 * <UL>
103 *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
104 *       package)</LI>
105 *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
106 *       package)</LI>
107 *   <LI>LDAP Communication (from the {@code com.unboundid.ldap.sdk}
108 *       package)</LI>
109 *   <LI>Value Patterns (from the {@code com.unboundid.util} package)</LI>
110 * </UL>
111 * Each search must match exactly one entry, and this tool will then attempt to
112 * authenticate as the user associated with that entry.  It supports simple
113 * authentication, as well as the CRAM-MD5, DIGEST-MD5, and PLAIN SASL
114 * mechanisms.
115 * <BR><BR>
116 * All of the necessary information is provided using command line arguments.
117 * Supported arguments include those allowed by the {@link LDAPCommandLineTool}
118 * class, as well as the following additional arguments:
119 * <UL>
120 *   <LI>"-b {baseDN}" or "--baseDN {baseDN}" -- specifies the base DN to use
121 *       for the searches.  This must be provided.  It may be a simple DN, or it
122 *       may be a value pattern to express a range of base DNs.</LI>
123 *   <LI>"-s {scope}" or "--scope {scope}" -- specifies the scope to use for the
124 *       search.  The scope value should be one of "base", "one", "sub", or
125 *       "subord".  If this isn't specified, then a scope of "sub" will be
126 *       used.</LI>
127 *   <LI>"-f {filter}" or "--filter {filter}" -- specifies the filter to use for
128 *       the searches.  This must be provided.  It may be a simple filter, or it
129 *       may be a value pattern to express a range of filters.</LI>
130 *   <LI>"-A {name}" or "--attribute {name}" -- specifies the name of an
131 *       attribute that should be included in entries returned from the server.
132 *       If this is not provided, then all user attributes will be requested.
133 *       This may include special tokens that the server may interpret, like
134 *       "1.1" to indicate that no attributes should be returned, "*", for all
135 *       user attributes, or "+" for all operational attributes.  Multiple
136 *       attributes may be requested with multiple instances of this
137 *       argument.</LI>
138 *   <LI>"-C {password}" or "--credentials {password}" -- specifies the password
139 *       to use when authenticating users identified by the searches.</LI>
140 *   <LI>"-a {authType}" or "--authType {authType}" -- specifies the type of
141 *       authentication to attempt.  Supported values include "SIMPLE",
142 *       "CRAM-MD5", "DIGEST-MD5", and "PLAIN".
143 *   <LI>"-t {num}" or "--numThreads {num}" -- specifies the number of
144 *       concurrent threads to use when performing the authentication
145 *       processing.  If this is not provided, then a default of one thread will
146 *       be used.</LI>
147 *   <LI>"-i {sec}" or "--intervalDuration {sec}" -- specifies the length of
148 *       time in seconds between lines out output.  If this is not provided,
149 *       then a default interval duration of five seconds will be used.</LI>
150 *   <LI>"-I {num}" or "--numIntervals {num}" -- specifies the maximum number of
151 *       intervals for which to run.  If this is not provided, then it will
152 *       run forever.</LI>
153 *   <LI>"-r {auths-per-second}" or "--ratePerSecond {auths-per-second}" --
154 *       specifies the target number of authorizations to perform per second.
155 *       It is still necessary to specify a sufficient number of threads for
156 *       achieving this rate.  If this option is not provided, then the tool
157 *       will run at the maximum rate for the specified number of threads.</LI>
158 *   <LI>"--variableRateData {path}" -- specifies the path to a file containing
159 *       information needed to allow the tool to vary the target rate over time.
160 *       If this option is not provided, then the tool will either use a fixed
161 *       target rate as specified by the "--ratePerSecond" argument, or it will
162 *       run at the maximum rate.</LI>
163 *   <LI>"--generateSampleRateFile {path}" -- specifies the path to a file to
164 *       which sample data will be written illustrating and describing the
165 *       format of the file expected to be used in conjunction with the
166 *       "--variableRateData" argument.</LI>
167 *   <LI>"--warmUpIntervals {num}" -- specifies the number of intervals to
168 *       complete before beginning overall statistics collection.</LI>
169 *   <LI>"--timestampFormat {format}" -- specifies the format to use for
170 *       timestamps included before each output line.  The format may be one of
171 *       "none" (for no timestamps), "with-date" (to include both the date and
172 *       the time), or "without-date" (to include only time time).</LI>
173 *   <LI>"--suppressErrorResultCodes" -- Indicates that information about the
174 *       result codes for failed operations should not be displayed.</LI>
175 *   <LI>"-c" or "--csv" -- Generate output in CSV format rather than a
176 *       display-friendly format.</LI>
177 * </UL>
178 */
179@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
180public final class AuthRate
181       extends LDAPCommandLineTool
182       implements Serializable
183{
184  /**
185   * The serial version UID for this serializable class.
186   */
187  private static final long serialVersionUID = 6918029871717330547L;
188
189
190
191  // Indicates whether a request has been made to stop running.
192  @NotNull private final AtomicBoolean stopRequested;
193
194  // The number of authrate threads that are currently running.
195  @NotNull private final AtomicInteger runningThreads;
196
197  // The argument used to indicate that bind requests should include the
198  // authorization identity request control.
199  @Nullable private BooleanArgument authorizationIdentityRequestControl;
200
201  // The argument used to indicate whether the tool should only perform a bind
202  // without a search.
203  @Nullable private BooleanArgument bindOnly;
204
205  // The argument used to indicate whether to generate output in CSV format.
206  @Nullable private BooleanArgument csvFormat;
207
208  // The argument used to indicate that bind requests should include the
209  // password policy request control.
210  @Nullable private BooleanArgument passwordPolicyRequestControl;
211
212  // The argument used to indicate whether to suppress information about error
213  // result codes.
214  @Nullable private BooleanArgument suppressErrorsArgument;
215
216  // The argument used to specify arbitrary controls to include in bind
217  // requests.
218  @Nullable private ControlArgument bindControl;
219
220  // The argument used to specify arbitrary controls to include in search
221  // requests.
222  @Nullable private ControlArgument searchControl;
223
224  // The argument used to specify a variable rate file.
225  @Nullable private FileArgument sampleRateFile;
226
227  // The argument used to specify a variable rate file.
228  @Nullable private FileArgument variableRateData;
229
230  // The argument used to specify the collection interval.
231  @Nullable private IntegerArgument collectionInterval;
232
233  // The argument used to specify the number of intervals.
234  @Nullable private IntegerArgument numIntervals;
235
236  // The argument used to specify the number of threads.
237  @Nullable private IntegerArgument numThreads;
238
239  // The argument used to specify the seed to use for the random number
240  // generator.
241  @Nullable private IntegerArgument randomSeed;
242
243  // The target rate of authentications per second.
244  @Nullable private IntegerArgument ratePerSecond;
245
246  // The number of warm-up intervals to perform.
247  @Nullable private IntegerArgument warmUpIntervals;
248
249  // The argument used to specify the attributes to return.
250  @Nullable private StringArgument attributes;
251
252  // The argument used to specify the type of authentication to perform.
253  @Nullable private StringArgument authType;
254
255  // The argument used to specify the base DNs for the searches.
256  @Nullable private StringArgument baseDN;
257
258  // The argument used to specify the filters for the searches.
259  @Nullable private StringArgument filter;
260
261  // The argument used to specify the scope for the searches.
262  @Nullable private ScopeArgument scopeArg;
263
264  // The argument used to specify the timestamp format.
265  @Nullable private StringArgument timestampFormat;
266
267  // The argument used to specify the password to use to authenticate.
268  @Nullable private StringArgument userPassword;
269
270  // A wakeable sleeper that will be used to sleep between reporting intervals.
271  @NotNull private final WakeableSleeper sleeper;
272
273
274
275  /**
276   * Parse the provided command line arguments and make the appropriate set of
277   * changes.
278   *
279   * @param  args  The command line arguments provided to this program.
280   */
281  public static void main(@NotNull final String[] args)
282  {
283    final ResultCode resultCode = main(args, System.out, System.err);
284    if (resultCode != ResultCode.SUCCESS)
285    {
286      System.exit(resultCode.intValue());
287    }
288  }
289
290
291
292  /**
293   * Parse the provided command line arguments and make the appropriate set of
294   * changes.
295   *
296   * @param  args       The command line arguments provided to this program.
297   * @param  outStream  The output stream to which standard out should be
298   *                    written.  It may be {@code null} if output should be
299   *                    suppressed.
300   * @param  errStream  The output stream to which standard error should be
301   *                    written.  It may be {@code null} if error messages
302   *                    should be suppressed.
303   *
304   * @return  A result code indicating whether the processing was successful.
305   */
306  @NotNull()
307  public static ResultCode main(@NotNull final String[] args,
308                                @Nullable final OutputStream outStream,
309                                @Nullable final OutputStream errStream)
310  {
311    final AuthRate authRate = new AuthRate(outStream, errStream);
312    return authRate.runTool(args);
313  }
314
315
316
317  /**
318   * Creates a new instance of this tool.
319   *
320   * @param  outStream  The output stream to which standard out should be
321   *                    written.  It may be {@code null} if output should be
322   *                    suppressed.
323   * @param  errStream  The output stream to which standard error should be
324   *                    written.  It may be {@code null} if error messages
325   *                    should be suppressed.
326   */
327  public AuthRate(@Nullable final OutputStream outStream,
328                  @Nullable final OutputStream errStream)
329  {
330    super(outStream, errStream);
331
332    stopRequested = new AtomicBoolean(false);
333    runningThreads = new AtomicInteger(0);
334    sleeper = new WakeableSleeper();
335  }
336
337
338
339  /**
340   * Retrieves the name for this tool.
341   *
342   * @return  The name for this tool.
343   */
344  @Override()
345  @NotNull()
346  public String getToolName()
347  {
348    return "authrate";
349  }
350
351
352
353  /**
354   * Retrieves the description for this tool.
355   *
356   * @return  The description for this tool.
357   */
358  @Override()
359  @NotNull()
360  public String getToolDescription()
361  {
362    return "Perform repeated authentications against an LDAP directory " +
363           "server, where each authentication consists of a search to " +
364           "find a user followed by a bind to verify the credentials " +
365           "for that user.";
366  }
367
368
369
370  /**
371   * Retrieves the version string for this tool.
372   *
373   * @return  The version string for this tool.
374   */
375  @Override()
376  @NotNull()
377  public String getToolVersion()
378  {
379    return Version.NUMERIC_VERSION_STRING;
380  }
381
382
383
384  /**
385   * Indicates whether this tool should provide support for an interactive mode,
386   * in which the tool offers a mode in which the arguments can be provided in
387   * a text-driven menu rather than requiring them to be given on the command
388   * line.  If interactive mode is supported, it may be invoked using the
389   * "--interactive" argument.  Alternately, if interactive mode is supported
390   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
391   * interactive mode may be invoked by simply launching the tool without any
392   * arguments.
393   *
394   * @return  {@code true} if this tool supports interactive mode, or
395   *          {@code false} if not.
396   */
397  @Override()
398  public boolean supportsInteractiveMode()
399  {
400    return true;
401  }
402
403
404
405  /**
406   * Indicates whether this tool defaults to launching in interactive mode if
407   * the tool is invoked without any command-line arguments.  This will only be
408   * used if {@link #supportsInteractiveMode()} returns {@code true}.
409   *
410   * @return  {@code true} if this tool defaults to using interactive mode if
411   *          launched without any command-line arguments, or {@code false} if
412   *          not.
413   */
414  @Override()
415  public boolean defaultsToInteractiveMode()
416  {
417    return true;
418  }
419
420
421
422  /**
423   * Indicates whether this tool should provide arguments for redirecting output
424   * to a file.  If this method returns {@code true}, then the tool will offer
425   * an "--outputFile" argument that will specify the path to a file to which
426   * all standard output and standard error content will be written, and it will
427   * also offer a "--teeToStandardOut" argument that can only be used if the
428   * "--outputFile" argument is present and will cause all output to be written
429   * to both the specified output file and to standard output.
430   *
431   * @return  {@code true} if this tool should provide arguments for redirecting
432   *          output to a file, or {@code false} if not.
433   */
434  @Override()
435  protected boolean supportsOutputFile()
436  {
437    return true;
438  }
439
440
441
442  /**
443   * Indicates whether this tool should default to interactively prompting for
444   * the bind password if a password is required but no argument was provided
445   * to indicate how to get the password.
446   *
447   * @return  {@code true} if this tool should default to interactively
448   *          prompting for the bind password, or {@code false} if not.
449   */
450  @Override()
451  protected boolean defaultToPromptForBindPassword()
452  {
453    return true;
454  }
455
456
457
458  /**
459   * Indicates whether this tool supports the use of a properties file for
460   * specifying default values for arguments that aren't specified on the
461   * command line.
462   *
463   * @return  {@code true} if this tool supports the use of a properties file
464   *          for specifying default values for arguments that aren't specified
465   *          on the command line, or {@code false} if not.
466   */
467  @Override()
468  public boolean supportsPropertiesFile()
469  {
470    return true;
471  }
472
473
474
475  /**
476   * Indicates whether this tool supports the ability to generate a debug log
477   * file.  If this method returns {@code true}, then the tool will expose
478   * additional arguments that can control debug logging.
479   *
480   * @return  {@code true} if this tool supports the ability to generate a debug
481   *          log file, or {@code false} if not.
482   */
483  @Override()
484  protected boolean supportsDebugLogging()
485  {
486    return true;
487  }
488
489
490
491  /**
492   * Indicates whether the LDAP-specific arguments should include alternate
493   * versions of all long identifiers that consist of multiple words so that
494   * they are available in both camelCase and dash-separated versions.
495   *
496   * @return  {@code true} if this tool should provide multiple versions of
497   *          long identifiers for LDAP-specific arguments, or {@code false} if
498   *          not.
499   */
500  @Override()
501  protected boolean includeAlternateLongIdentifiers()
502  {
503    return true;
504  }
505
506
507
508  /**
509   * Adds the arguments used by this program that aren't already provided by the
510   * generic {@code LDAPCommandLineTool} framework.
511   *
512   * @param  parser  The argument parser to which the arguments should be added.
513   *
514   * @throws  ArgumentException  If a problem occurs while adding the arguments.
515   */
516  @Override()
517  public void addNonLDAPArguments(@NotNull final ArgumentParser parser)
518         throws ArgumentException
519  {
520    String description = "The base DN to use for the searches.  It may be a " +
521         "simple DN or a value pattern to specify a range of DNs (e.g., " +
522         "\"uid=user.[1-1000],ou=People,dc=example,dc=com\").  See " +
523         ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " +
524         "value pattern syntax.  This must be provided.";
525    baseDN = new StringArgument('b', "baseDN", true, 1, "{dn}", description);
526    baseDN.setArgumentGroupName("Search and Authentication Arguments");
527    baseDN.addLongIdentifier("base-dn", true);
528    parser.addArgument(baseDN);
529
530
531    description = "The scope to use for the searches.  It should be 'base', " +
532                  "'one', 'sub', or 'subord'.  If this is not provided, a " +
533                  "default scope of 'sub' will be used.";
534    scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description,
535                                 SearchScope.SUB);
536    scopeArg.setArgumentGroupName("Search and Authentication Arguments");
537    parser.addArgument(scopeArg);
538
539
540    description = "The filter to use for the searches.  It may be a simple " +
541                  "filter or a value pattern to specify a range of filters " +
542                  "(e.g., \"(uid=user.[1-1000])\").  See " +
543                  ValuePattern.PUBLIC_JAVADOC_URL + " for complete details " +
544                  "about the value pattern syntax.  This must be provided.";
545    filter = new StringArgument('f', "filter", false, 1, "{filter}",
546                                description);
547    filter.setArgumentGroupName("Search and Authentication Arguments");
548    parser.addArgument(filter);
549
550
551    description = "The name of an attribute to include in entries returned " +
552                  "from the searches.  Multiple attributes may be requested " +
553                  "by providing this argument multiple times.  If no return " +
554                  "attributes are specified, then entries will be returned " +
555                  "with all user attributes.";
556    attributes = new StringArgument('A', "attribute", false, 0, "{name}",
557                                    description);
558    attributes.setArgumentGroupName("Search and Authentication Arguments");
559    parser.addArgument(attributes);
560
561
562    description = "The password to use when binding as the users returned " +
563                  "from the searches.  This must be provided.";
564    userPassword = new StringArgument('C', "credentials", true, 1, "{password}",
565                                      description);
566    userPassword.setSensitive(true);
567    userPassword.setArgumentGroupName("Search and Authentication Arguments");
568    parser.addArgument(userPassword);
569
570
571    description = "Indicates that the tool should only perform bind " +
572                  "operations without the initial search.  If this argument " +
573                  "is provided, then the base DN pattern will be used to " +
574                  "obtain the bind DNs.";
575    bindOnly = new BooleanArgument('B', "bindOnly", 1, description);
576    bindOnly.setArgumentGroupName("Search and Authentication Arguments");
577    bindOnly.addLongIdentifier("bind-only", true);
578    parser.addArgument(bindOnly);
579    parser.addRequiredArgumentSet(filter, bindOnly);
580
581
582    description = "The type of authentication to perform.  Allowed values " +
583                  "are:  SIMPLE, CRAM-MD5, DIGEST-MD5, and PLAIN.  If no "+
584                  "value is provided, then SIMPLE authentication will be " +
585                  "performed.";
586    final Set<String> allowedAuthTypes =
587         StaticUtils.setOf("simple", "cram-md5", "digest-md5", "plain");
588    authType = new StringArgument('a', "authType", true, 1, "{authType}",
589                                  description, allowedAuthTypes, "simple");
590    authType.setArgumentGroupName("Search and Authentication Arguments");
591    authType.addLongIdentifier("auth-type", true);
592    parser.addArgument(authType);
593
594
595    description = "Indicates that bind requests should include the " +
596                  "authorization identity request control as described in " +
597                  "RFC 3829.";
598    authorizationIdentityRequestControl = new BooleanArgument(null,
599         "authorizationIdentityRequestControl", 1, description);
600    authorizationIdentityRequestControl.setArgumentGroupName(
601         "Request Control Arguments");
602    authorizationIdentityRequestControl.addLongIdentifier(
603         "authorization-identity-request-control", true);
604    parser.addArgument(authorizationIdentityRequestControl);
605
606
607    description = "Indicates that bind requests should include the " +
608                  "password policy request control as described in " +
609                  "draft-behera-ldap-password-policy-10.";
610    passwordPolicyRequestControl = new BooleanArgument(null,
611         "passwordPolicyRequestControl", 1, description);
612    passwordPolicyRequestControl.setArgumentGroupName(
613         "Request Control Arguments");
614    passwordPolicyRequestControl.addLongIdentifier(
615         "password-policy-request-control", true);
616    parser.addArgument(passwordPolicyRequestControl);
617
618
619    description = "Indicates that search requests should include the " +
620                  "specified request control.  This may be provided multiple " +
621                  "times to include multiple search request controls.";
622    searchControl = new ControlArgument(null, "searchControl", false, 0, null,
623                                        description);
624    searchControl.setArgumentGroupName("Request Control Arguments");
625    searchControl.addLongIdentifier("search-control", true);
626    parser.addArgument(searchControl);
627
628
629    description = "Indicates that bind requests should include the " +
630                  "specified request control.  This may be provided multiple " +
631                  "times to include multiple modify request controls.";
632    bindControl = new ControlArgument(null, "bindControl", false, 0, null,
633                                      description);
634    bindControl.setArgumentGroupName("Request Control Arguments");
635    bindControl.addLongIdentifier("bind-control", true);
636    parser.addArgument(bindControl);
637
638
639    description = "The number of threads to use to perform the " +
640                  "authentication processing.  If this is not provided, then " +
641                  "a default of one thread will be used.";
642    numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}",
643                                     description, 1, Integer.MAX_VALUE, 1);
644    numThreads.setArgumentGroupName("Rate Management Arguments");
645    numThreads.addLongIdentifier("num-threads", true);
646    parser.addArgument(numThreads);
647
648
649    description = "The length of time in seconds between output lines.  If " +
650                  "this is not provided, then a default interval of five " +
651                  "seconds will be used.";
652    collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1,
653                                             "{num}", description, 1,
654                                             Integer.MAX_VALUE, 5);
655    collectionInterval.setArgumentGroupName("Rate Management Arguments");
656    collectionInterval.addLongIdentifier("interval-duration", true);
657    parser.addArgument(collectionInterval);
658
659
660    description = "The maximum number of intervals for which to run.  If " +
661                  "this is not provided, then the tool will run until it is " +
662                  "interrupted.";
663    numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}",
664                                       description, 1, Integer.MAX_VALUE,
665                                       Integer.MAX_VALUE);
666    numIntervals.setArgumentGroupName("Rate Management Arguments");
667    numIntervals.addLongIdentifier("num-intervals", true);
668    parser.addArgument(numIntervals);
669
670    description = "The target number of authorizations to perform per " +
671                  "second.  It is still necessary to specify a sufficient " +
672                  "number of threads for achieving this rate.  If neither " +
673                  "this option nor --variableRateData is provided, then the " +
674                  "tool will run at the maximum rate for the specified " +
675                  "number of threads.";
676    ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1,
677                                        "{auths-per-second}", description,
678                                        1, Integer.MAX_VALUE);
679    ratePerSecond.setArgumentGroupName("Rate Management Arguments");
680    ratePerSecond.addLongIdentifier("rate-per-second", true);
681    parser.addArgument(ratePerSecond);
682
683    final String variableRateDataArgName = "variableRateData";
684    final String generateSampleRateFileArgName = "generateSampleRateFile";
685    description = RateAdjustor.getVariableRateDataArgumentDescription(
686         generateSampleRateFileArgName);
687    variableRateData = new FileArgument(null, variableRateDataArgName, false, 1,
688                                        "{path}", description, true, true, true,
689                                        false);
690    variableRateData.setArgumentGroupName("Rate Management Arguments");
691    variableRateData.addLongIdentifier("variable-rate-data", true);
692    parser.addArgument(variableRateData);
693
694    description = RateAdjustor.getGenerateSampleVariableRateFileDescription(
695         variableRateDataArgName);
696    sampleRateFile = new FileArgument(null, generateSampleRateFileArgName,
697                                      false, 1, "{path}", description, false,
698                                      true, true, false);
699    sampleRateFile.setArgumentGroupName("Rate Management Arguments");
700    sampleRateFile.addLongIdentifier("generate-sample-rate-file", true);
701    sampleRateFile.setUsageArgument(true);
702    parser.addArgument(sampleRateFile);
703    parser.addExclusiveArgumentSet(variableRateData, sampleRateFile);
704
705    description = "The number of intervals to complete before beginning " +
706                  "overall statistics collection.  Specifying a nonzero " +
707                  "number of warm-up intervals gives the client and server " +
708                  "a chance to warm up without skewing performance results.";
709    warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1,
710         "{num}", description, 0, Integer.MAX_VALUE, 0);
711    warmUpIntervals.setArgumentGroupName("Rate Management Arguments");
712    warmUpIntervals.addLongIdentifier("warm-up-intervals", true);
713    parser.addArgument(warmUpIntervals);
714
715    description = "Indicates the format to use for timestamps included in " +
716                  "the output.  A value of 'none' indicates that no " +
717                  "timestamps should be included.  A value of 'with-date' " +
718                  "indicates that both the date and the time should be " +
719                  "included.  A value of 'without-date' indicates that only " +
720                  "the time should be included.";
721    final Set<String> allowedFormats =
722         StaticUtils.setOf("none", "with-date", "without-date");
723    timestampFormat = new StringArgument(null, "timestampFormat", true, 1,
724         "{format}", description, allowedFormats, "none");
725    timestampFormat.addLongIdentifier("timestamp-format", true);
726    parser.addArgument(timestampFormat);
727
728    description = "Indicates that information about the result codes for " +
729                  "failed operations should not be displayed.";
730    suppressErrorsArgument = new BooleanArgument(null,
731         "suppressErrorResultCodes", 1, description);
732    suppressErrorsArgument.addLongIdentifier("suppress-error-result-codes",
733         true);
734    parser.addArgument(suppressErrorsArgument);
735
736    description = "Generate output in CSV format rather than a " +
737                  "display-friendly format";
738    csvFormat = new BooleanArgument('c', "csv", 1, description);
739    parser.addArgument(csvFormat);
740
741    description = "Specifies the seed to use for the random number generator.";
742    randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}",
743         description);
744    randomSeed.addLongIdentifier("random-seed", true);
745    parser.addArgument(randomSeed);
746  }
747
748
749
750  /**
751   * Indicates whether this tool supports creating connections to multiple
752   * servers.  If it is to support multiple servers, then the "--hostname" and
753   * "--port" arguments will be allowed to be provided multiple times, and
754   * will be required to be provided the same number of times.  The same type of
755   * communication security and bind credentials will be used for all servers.
756   *
757   * @return  {@code true} if this tool supports creating connections to
758   *          multiple servers, or {@code false} if not.
759   */
760  @Override()
761  protected boolean supportsMultipleServers()
762  {
763    return true;
764  }
765
766
767
768  /**
769   * Retrieves the connection options that should be used for connections
770   * created for use with this tool.
771   *
772   * @return  The connection options that should be used for connections created
773   *          for use with this tool.
774   */
775  @Override()
776  @NotNull()
777  public LDAPConnectionOptions getConnectionOptions()
778  {
779    final LDAPConnectionOptions options = new LDAPConnectionOptions();
780    options.setUseSynchronousMode(true);
781    return options;
782  }
783
784
785
786  /**
787   * Performs the actual processing for this tool.  In this case, it gets a
788   * connection to the directory server and uses it to perform the requested
789   * searches.
790   *
791   * @return  The result code for the processing that was performed.
792   */
793  @Override()
794  @NotNull()
795  public ResultCode doToolProcessing()
796  {
797    // If the sample rate file argument was specified, then generate the sample
798    // variable rate data file and return.
799    if (sampleRateFile.isPresent())
800    {
801      try
802      {
803        RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue());
804        return ResultCode.SUCCESS;
805      }
806      catch (final Exception e)
807      {
808        Debug.debugException(e);
809        err("An error occurred while trying to write sample variable data " +
810             "rate file '", sampleRateFile.getValue().getAbsolutePath(),
811             "':  ", StaticUtils.getExceptionMessage(e));
812        return ResultCode.LOCAL_ERROR;
813      }
814    }
815
816
817    // Determine the random seed to use.
818    final Long seed;
819    if (randomSeed.isPresent())
820    {
821      seed = Long.valueOf(randomSeed.getValue());
822    }
823    else
824    {
825      seed = null;
826    }
827
828    // Create value patterns for the base DN and filter.
829    final ValuePattern dnPattern;
830    try
831    {
832      dnPattern = new ValuePattern(baseDN.getValue(), seed);
833    }
834    catch (final ParseException pe)
835    {
836      Debug.debugException(pe);
837      err("Unable to parse the base DN value pattern:  ", pe.getMessage());
838      return ResultCode.PARAM_ERROR;
839    }
840
841    final ValuePattern filterPattern;
842    if (filter.isPresent())
843    {
844      try
845      {
846        filterPattern = new ValuePattern(filter.getValue(), seed);
847      }
848      catch (final ParseException pe)
849      {
850        Debug.debugException(pe);
851        err("Unable to parse the filter pattern:  ", pe.getMessage());
852        return ResultCode.PARAM_ERROR;
853      }
854    }
855    else
856    {
857      filterPattern = null;
858    }
859
860
861    // Get the attributes to return.
862    final String[] attrs;
863    if (attributes.isPresent())
864    {
865      final List<String> attrList = attributes.getValues();
866      attrs = new String[attrList.size()];
867      attrList.toArray(attrs);
868    }
869    else
870    {
871      attrs = StaticUtils.NO_STRINGS;
872    }
873
874
875    // If the --ratePerSecond option was specified, then limit the rate
876    // accordingly.
877    FixedRateBarrier fixedRateBarrier = null;
878    if (ratePerSecond.isPresent() || variableRateData.isPresent())
879    {
880      // We might not have a rate per second if --variableRateData is specified.
881      // The rate typically doesn't matter except when we have warm-up
882      // intervals.  In this case, we'll run at the max rate.
883      final int intervalSeconds = collectionInterval.getValue();
884      final int ratePerInterval =
885           (ratePerSecond.getValue() == null)
886           ? Integer.MAX_VALUE
887           : ratePerSecond.getValue() * intervalSeconds;
888      fixedRateBarrier =
889           new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval);
890    }
891
892
893    // If --variableRateData was specified, then initialize a RateAdjustor.
894    RateAdjustor rateAdjustor = null;
895    if (variableRateData.isPresent())
896    {
897      try
898      {
899        rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier,
900             ratePerSecond.getValue(), variableRateData.getValue());
901      }
902      catch (final IOException | IllegalArgumentException e)
903      {
904        Debug.debugException(e);
905        err("Initializing the variable rates failed: " + e.getMessage());
906        return ResultCode.PARAM_ERROR;
907      }
908    }
909
910
911    // Determine whether to include timestamps in the output and if so what
912    // format should be used for them.
913    final boolean includeTimestamp;
914    final String timeFormat;
915    if (timestampFormat.getValue().equalsIgnoreCase("with-date"))
916    {
917      includeTimestamp = true;
918      timeFormat       = "dd/MM/yyyy HH:mm:ss";
919    }
920    else if (timestampFormat.getValue().equalsIgnoreCase("without-date"))
921    {
922      includeTimestamp = true;
923      timeFormat       = "HH:mm:ss";
924    }
925    else
926    {
927      includeTimestamp = false;
928      timeFormat       = null;
929    }
930
931
932    // Get the controls to include in bind requests.
933    final ArrayList<Control> bindControls = new ArrayList<>(5);
934    if (authorizationIdentityRequestControl.isPresent())
935    {
936      bindControls.add(new AuthorizationIdentityRequestControl());
937    }
938
939    if (passwordPolicyRequestControl.isPresent())
940    {
941      bindControls.add(new DraftBeheraLDAPPasswordPolicy10RequestControl());
942    }
943
944    bindControls.addAll(bindControl.getValues());
945
946
947    // Determine whether any warm-up intervals should be run.
948    final long totalIntervals;
949    final boolean warmUp;
950    int remainingWarmUpIntervals = warmUpIntervals.getValue();
951    if (remainingWarmUpIntervals > 0)
952    {
953      warmUp = true;
954      totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals;
955    }
956    else
957    {
958      warmUp = true;
959      totalIntervals = 0L + numIntervals.getValue();
960    }
961
962
963    // Create the table that will be used to format the output.
964    final OutputFormat outputFormat;
965    if (csvFormat.isPresent())
966    {
967      outputFormat = OutputFormat.CSV;
968    }
969    else
970    {
971      outputFormat = OutputFormat.COLUMNS;
972    }
973
974    final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp,
975         timeFormat, outputFormat, " ",
976         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
977                  "Auths/Sec"),
978         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
979                  "Avg Dur ms"),
980         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent",
981                  "Errors/Sec"),
982         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
983                  "Auths/Sec"),
984         new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall",
985                  "Avg Dur ms"));
986
987
988    // Create values to use for statistics collection.
989    final AtomicLong        authCounter   = new AtomicLong(0L);
990    final AtomicLong        errorCounter  = new AtomicLong(0L);
991    final AtomicLong        authDurations = new AtomicLong(0L);
992    final ResultCodeCounter rcCounter     = new ResultCodeCounter();
993
994
995    // Determine the length of each interval in milliseconds.
996    final long intervalMillis = 1000L * collectionInterval.getValue();
997
998
999    // Create the threads to use for the searches.
1000    final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1);
1001    final AuthRateThread[] threads = new AuthRateThread[numThreads.getValue()];
1002    for (int i=0; i < threads.length; i++)
1003    {
1004      final LDAPConnection searchConnection;
1005      final LDAPConnection bindConnection;
1006      try
1007      {
1008        searchConnection = getConnection();
1009        bindConnection   = getConnection();
1010      }
1011      catch (final LDAPException le)
1012      {
1013        Debug.debugException(le);
1014        err("Unable to connect to the directory server:  ",
1015            StaticUtils.getExceptionMessage(le));
1016        return le.getResultCode();
1017      }
1018
1019      threads[i] = new AuthRateThread(this, i, searchConnection, bindConnection,
1020           dnPattern, scopeArg.getValue(), filterPattern, attrs,
1021           userPassword.getValue(), bindOnly.isPresent(), authType.getValue(),
1022           searchControl.getValues(), bindControls, runningThreads, barrier,
1023           authCounter, authDurations, errorCounter, rcCounter,
1024           fixedRateBarrier);
1025      threads[i].start();
1026    }
1027
1028
1029    // Display the table header.
1030    for (final String headerLine : formatter.getHeaderLines(true))
1031    {
1032      out(headerLine);
1033    }
1034
1035
1036    // Start the RateAdjustor before the threads so that the initial value is
1037    // in place before any load is generated unless we're doing a warm-up in
1038    // which case, we'll start it after the warm-up is complete.
1039    if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0))
1040    {
1041      rateAdjustor.start();
1042    }
1043
1044
1045    // Indicate that the threads can start running.
1046    try
1047    {
1048      barrier.await();
1049    }
1050    catch (final Exception e)
1051    {
1052      Debug.debugException(e);
1053    }
1054
1055    long overallStartTime = System.nanoTime();
1056    long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis;
1057
1058
1059    boolean setOverallStartTime = false;
1060    long    lastDuration        = 0L;
1061    long    lastNumErrors       = 0L;
1062    long    lastNumAuths        = 0L;
1063    long    lastEndTime         = System.nanoTime();
1064    for (long i=0; i < totalIntervals; i++)
1065    {
1066      if (rateAdjustor != null)
1067      {
1068        if (! rateAdjustor.isAlive())
1069        {
1070          out("All of the rates in " + variableRateData.getValue().getName() +
1071              " have been completed.");
1072          break;
1073        }
1074      }
1075
1076      final long startTimeMillis = System.currentTimeMillis();
1077      final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis;
1078      nextIntervalStartTime += intervalMillis;
1079      if (sleepTimeMillis > 0)
1080      {
1081        sleeper.sleep(sleepTimeMillis);
1082      }
1083
1084      if (stopRequested.get())
1085      {
1086        break;
1087      }
1088
1089      final long endTime          = System.nanoTime();
1090      final long intervalDuration = endTime - lastEndTime;
1091
1092      final long numAuths;
1093      final long numErrors;
1094      final long totalDuration;
1095      if (warmUp && (remainingWarmUpIntervals > 0))
1096      {
1097        numAuths      = authCounter.getAndSet(0L);
1098        numErrors     = errorCounter.getAndSet(0L);
1099        totalDuration = authDurations.getAndSet(0L);
1100      }
1101      else
1102      {
1103        numAuths      = authCounter.get();
1104        numErrors     = errorCounter.get();
1105        totalDuration = authDurations.get();
1106      }
1107
1108      final long recentNumAuths  = numAuths - lastNumAuths;
1109      final long recentNumErrors = numErrors - lastNumErrors;
1110      final long recentDuration = totalDuration - lastDuration;
1111
1112      final double numSeconds = intervalDuration / 1_000_000_000.0d;
1113      final double recentAuthRate = recentNumAuths / numSeconds;
1114      final double recentErrorRate  = recentNumErrors / numSeconds;
1115
1116      final double recentAvgDuration;
1117      if (recentNumAuths > 0L)
1118      {
1119        recentAvgDuration = 1.0d * recentDuration / recentNumAuths / 1_000_000;
1120      }
1121      else
1122      {
1123        recentAvgDuration = 0.0d;
1124      }
1125
1126      if (warmUp && (remainingWarmUpIntervals > 0))
1127      {
1128        out(formatter.formatRow(recentAuthRate, recentAvgDuration,
1129             recentErrorRate, "warming up", "warming up"));
1130
1131        remainingWarmUpIntervals--;
1132        if (remainingWarmUpIntervals == 0)
1133        {
1134          out("Warm-up completed.  Beginning overall statistics collection.");
1135          setOverallStartTime = true;
1136          if (rateAdjustor != null)
1137          {
1138            rateAdjustor.start();
1139          }
1140        }
1141      }
1142      else
1143      {
1144        if (setOverallStartTime)
1145        {
1146          overallStartTime    = lastEndTime;
1147          setOverallStartTime = false;
1148        }
1149
1150        final double numOverallSeconds =
1151             (endTime - overallStartTime) / 1_000_000_000.0d;
1152        final double overallAuthRate = numAuths / numOverallSeconds;
1153
1154        final double overallAvgDuration;
1155        if (numAuths > 0L)
1156        {
1157          overallAvgDuration = 1.0d * totalDuration / numAuths / 1_000_000;
1158        }
1159        else
1160        {
1161          overallAvgDuration = 0.0d;
1162        }
1163
1164        out(formatter.formatRow(recentAuthRate, recentAvgDuration,
1165             recentErrorRate, overallAuthRate, overallAvgDuration));
1166
1167        lastNumAuths    = numAuths;
1168        lastNumErrors   = numErrors;
1169        lastDuration    = totalDuration;
1170      }
1171
1172      final List<ObjectPair<ResultCode,Long>> rcCounts =
1173           rcCounter.getCounts(true);
1174      if ((! suppressErrorsArgument.isPresent()) && (! rcCounts.isEmpty()))
1175      {
1176        err("\tError Results:");
1177        for (final ObjectPair<ResultCode,Long> p : rcCounts)
1178        {
1179          err("\t", p.getFirst().getName(), ":  ", p.getSecond());
1180        }
1181      }
1182
1183      lastEndTime = endTime;
1184    }
1185
1186
1187    // Shut down the RateAdjustor if we have one.
1188    if (rateAdjustor != null)
1189    {
1190      rateAdjustor.shutDown();
1191    }
1192
1193
1194    // Stop all of the threads.
1195    ResultCode resultCode = ResultCode.SUCCESS;
1196    for (final AuthRateThread t : threads)
1197    {
1198      final ResultCode r = t.stopRunning();
1199      if (resultCode == ResultCode.SUCCESS)
1200      {
1201        resultCode = r;
1202      }
1203    }
1204
1205    return resultCode;
1206  }
1207
1208
1209
1210  /**
1211   * Requests that this tool stop running.  This method will attempt to wait
1212   * for all threads to complete before returning control to the caller.
1213   */
1214  public void stopRunning()
1215  {
1216    stopRequested.set(true);
1217    sleeper.wakeup();
1218
1219    while (true)
1220    {
1221      final int stillRunning = runningThreads.get();
1222      if (stillRunning <= 0)
1223      {
1224        break;
1225      }
1226      else
1227      {
1228        try
1229        {
1230          Thread.sleep(1L);
1231        } catch (final Exception e) {}
1232      }
1233    }
1234  }
1235
1236
1237
1238  /**
1239   * {@inheritDoc}
1240   */
1241  @Override()
1242  @NotNull()
1243  public LinkedHashMap<String[],String> getExampleUsages()
1244  {
1245    final LinkedHashMap<String[],String> examples =
1246         new LinkedHashMap<>(StaticUtils.computeMapCapacity(2));
1247
1248    String[] args =
1249    {
1250      "--hostname", "server.example.com",
1251      "--port", "389",
1252      "--bindDN", "uid=admin,dc=example,dc=com",
1253      "--bindPassword", "password",
1254      "--baseDN", "dc=example,dc=com",
1255      "--scope", "sub",
1256      "--filter", "(uid=user.[1-1000000])",
1257      "--credentials", "password",
1258      "--numThreads", "10"
1259    };
1260    String description =
1261         "Test authentication performance by searching randomly across a set " +
1262         "of one million users located below 'dc=example,dc=com' with ten " +
1263         "concurrent threads and performing simple binds with a password of " +
1264         "'password'.  The searches will be performed anonymously.";
1265    examples.put(args, description);
1266
1267    args = new String[]
1268    {
1269      "--generateSampleRateFile", "variable-rate-data.txt"
1270    };
1271    description =
1272         "Generate a sample variable rate definition file that may be used " +
1273         "in conjunction with the --variableRateData argument.  The sample " +
1274         "file will include comments that describe the format for data to be " +
1275         "included in this file.";
1276    examples.put(args, description);
1277
1278    return examples;
1279  }
1280}