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