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