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