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