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