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 this tool supports the ability to generate a debug log 477 * file. If this method returns {@code true}, then the tool will expose 478 * additional arguments that can control debug logging. 479 * 480 * @return {@code true} if this tool supports the ability to generate a debug 481 * log file, or {@code false} if not. 482 */ 483 @Override() 484 protected boolean supportsDebugLogging() 485 { 486 return true; 487 } 488 489 490 491 /** 492 * Indicates whether the LDAP-specific arguments should include alternate 493 * versions of all long identifiers that consist of multiple words so that 494 * they are available in both camelCase and dash-separated versions. 495 * 496 * @return {@code true} if this tool should provide multiple versions of 497 * long identifiers for LDAP-specific arguments, or {@code false} if 498 * not. 499 */ 500 @Override() 501 protected boolean includeAlternateLongIdentifiers() 502 { 503 return true; 504 } 505 506 507 508 /** 509 * Adds the arguments used by this program that aren't already provided by the 510 * generic {@code LDAPCommandLineTool} framework. 511 * 512 * @param parser The argument parser to which the arguments should be added. 513 * 514 * @throws ArgumentException If a problem occurs while adding the arguments. 515 */ 516 @Override() 517 public void addNonLDAPArguments(@NotNull final ArgumentParser parser) 518 throws ArgumentException 519 { 520 String description = "The base DN to use for the searches. It may be a " + 521 "simple DN or a value pattern to specify a range of DNs (e.g., " + 522 "\"uid=user.[1-1000],ou=People,dc=example,dc=com\"). See " + 523 ValuePattern.PUBLIC_JAVADOC_URL + " for complete details about the " + 524 "value pattern syntax. This must be provided."; 525 baseDN = new StringArgument('b', "baseDN", true, 1, "{dn}", description); 526 baseDN.setArgumentGroupName("Search and Authentication Arguments"); 527 baseDN.addLongIdentifier("base-dn", true); 528 parser.addArgument(baseDN); 529 530 531 description = "The scope to use for the searches. It should be 'base', " + 532 "'one', 'sub', or 'subord'. If this is not provided, a " + 533 "default scope of 'sub' will be used."; 534 scopeArg = new ScopeArgument('s', "scope", false, "{scope}", description, 535 SearchScope.SUB); 536 scopeArg.setArgumentGroupName("Search and Authentication Arguments"); 537 parser.addArgument(scopeArg); 538 539 540 description = "The filter to use for the searches. It may be a simple " + 541 "filter or a value pattern to specify a range of filters " + 542 "(e.g., \"(uid=user.[1-1000])\"). See " + 543 ValuePattern.PUBLIC_JAVADOC_URL + " for complete details " + 544 "about the value pattern syntax. This must be provided."; 545 filter = new StringArgument('f', "filter", false, 1, "{filter}", 546 description); 547 filter.setArgumentGroupName("Search and Authentication Arguments"); 548 parser.addArgument(filter); 549 550 551 description = "The name of an attribute to include in entries returned " + 552 "from the searches. Multiple attributes may be requested " + 553 "by providing this argument multiple times. If no return " + 554 "attributes are specified, then entries will be returned " + 555 "with all user attributes."; 556 attributes = new StringArgument('A', "attribute", false, 0, "{name}", 557 description); 558 attributes.setArgumentGroupName("Search and Authentication Arguments"); 559 parser.addArgument(attributes); 560 561 562 description = "The password to use when binding as the users returned " + 563 "from the searches. This must be provided."; 564 userPassword = new StringArgument('C', "credentials", true, 1, "{password}", 565 description); 566 userPassword.setSensitive(true); 567 userPassword.setArgumentGroupName("Search and Authentication Arguments"); 568 parser.addArgument(userPassword); 569 570 571 description = "Indicates that the tool should only perform bind " + 572 "operations without the initial search. If this argument " + 573 "is provided, then the base DN pattern will be used to " + 574 "obtain the bind DNs."; 575 bindOnly = new BooleanArgument('B', "bindOnly", 1, description); 576 bindOnly.setArgumentGroupName("Search and Authentication Arguments"); 577 bindOnly.addLongIdentifier("bind-only", true); 578 parser.addArgument(bindOnly); 579 parser.addRequiredArgumentSet(filter, bindOnly); 580 581 582 description = "The type of authentication to perform. Allowed values " + 583 "are: SIMPLE, CRAM-MD5, DIGEST-MD5, and PLAIN. If no "+ 584 "value is provided, then SIMPLE authentication will be " + 585 "performed."; 586 final Set<String> allowedAuthTypes = 587 StaticUtils.setOf("simple", "cram-md5", "digest-md5", "plain"); 588 authType = new StringArgument('a', "authType", true, 1, "{authType}", 589 description, allowedAuthTypes, "simple"); 590 authType.setArgumentGroupName("Search and Authentication Arguments"); 591 authType.addLongIdentifier("auth-type", true); 592 parser.addArgument(authType); 593 594 595 description = "Indicates that bind requests should include the " + 596 "authorization identity request control as described in " + 597 "RFC 3829."; 598 authorizationIdentityRequestControl = new BooleanArgument(null, 599 "authorizationIdentityRequestControl", 1, description); 600 authorizationIdentityRequestControl.setArgumentGroupName( 601 "Request Control Arguments"); 602 authorizationIdentityRequestControl.addLongIdentifier( 603 "authorization-identity-request-control", true); 604 parser.addArgument(authorizationIdentityRequestControl); 605 606 607 description = "Indicates that bind requests should include the " + 608 "password policy request control as described in " + 609 "draft-behera-ldap-password-policy-10."; 610 passwordPolicyRequestControl = new BooleanArgument(null, 611 "passwordPolicyRequestControl", 1, description); 612 passwordPolicyRequestControl.setArgumentGroupName( 613 "Request Control Arguments"); 614 passwordPolicyRequestControl.addLongIdentifier( 615 "password-policy-request-control", true); 616 parser.addArgument(passwordPolicyRequestControl); 617 618 619 description = "Indicates that search requests should include the " + 620 "specified request control. This may be provided multiple " + 621 "times to include multiple search request controls."; 622 searchControl = new ControlArgument(null, "searchControl", false, 0, null, 623 description); 624 searchControl.setArgumentGroupName("Request Control Arguments"); 625 searchControl.addLongIdentifier("search-control", true); 626 parser.addArgument(searchControl); 627 628 629 description = "Indicates that bind requests should include the " + 630 "specified request control. This may be provided multiple " + 631 "times to include multiple modify request controls."; 632 bindControl = new ControlArgument(null, "bindControl", false, 0, null, 633 description); 634 bindControl.setArgumentGroupName("Request Control Arguments"); 635 bindControl.addLongIdentifier("bind-control", true); 636 parser.addArgument(bindControl); 637 638 639 description = "The number of threads to use to perform the " + 640 "authentication processing. If this is not provided, then " + 641 "a default of one thread will be used."; 642 numThreads = new IntegerArgument('t', "numThreads", true, 1, "{num}", 643 description, 1, Integer.MAX_VALUE, 1); 644 numThreads.setArgumentGroupName("Rate Management Arguments"); 645 numThreads.addLongIdentifier("num-threads", true); 646 parser.addArgument(numThreads); 647 648 649 description = "The length of time in seconds between output lines. If " + 650 "this is not provided, then a default interval of five " + 651 "seconds will be used."; 652 collectionInterval = new IntegerArgument('i', "intervalDuration", true, 1, 653 "{num}", description, 1, 654 Integer.MAX_VALUE, 5); 655 collectionInterval.setArgumentGroupName("Rate Management Arguments"); 656 collectionInterval.addLongIdentifier("interval-duration", true); 657 parser.addArgument(collectionInterval); 658 659 660 description = "The maximum number of intervals for which to run. If " + 661 "this is not provided, then the tool will run until it is " + 662 "interrupted."; 663 numIntervals = new IntegerArgument('I', "numIntervals", true, 1, "{num}", 664 description, 1, Integer.MAX_VALUE, 665 Integer.MAX_VALUE); 666 numIntervals.setArgumentGroupName("Rate Management Arguments"); 667 numIntervals.addLongIdentifier("num-intervals", true); 668 parser.addArgument(numIntervals); 669 670 description = "The target number of authorizations to perform per " + 671 "second. It is still necessary to specify a sufficient " + 672 "number of threads for achieving this rate. If neither " + 673 "this option nor --variableRateData is provided, then the " + 674 "tool will run at the maximum rate for the specified " + 675 "number of threads."; 676 ratePerSecond = new IntegerArgument('r', "ratePerSecond", false, 1, 677 "{auths-per-second}", description, 678 1, Integer.MAX_VALUE); 679 ratePerSecond.setArgumentGroupName("Rate Management Arguments"); 680 ratePerSecond.addLongIdentifier("rate-per-second", true); 681 parser.addArgument(ratePerSecond); 682 683 final String variableRateDataArgName = "variableRateData"; 684 final String generateSampleRateFileArgName = "generateSampleRateFile"; 685 description = RateAdjustor.getVariableRateDataArgumentDescription( 686 generateSampleRateFileArgName); 687 variableRateData = new FileArgument(null, variableRateDataArgName, false, 1, 688 "{path}", description, true, true, true, 689 false); 690 variableRateData.setArgumentGroupName("Rate Management Arguments"); 691 variableRateData.addLongIdentifier("variable-rate-data", true); 692 parser.addArgument(variableRateData); 693 694 description = RateAdjustor.getGenerateSampleVariableRateFileDescription( 695 variableRateDataArgName); 696 sampleRateFile = new FileArgument(null, generateSampleRateFileArgName, 697 false, 1, "{path}", description, false, 698 true, true, false); 699 sampleRateFile.setArgumentGroupName("Rate Management Arguments"); 700 sampleRateFile.addLongIdentifier("generate-sample-rate-file", true); 701 sampleRateFile.setUsageArgument(true); 702 parser.addArgument(sampleRateFile); 703 parser.addExclusiveArgumentSet(variableRateData, sampleRateFile); 704 705 description = "The number of intervals to complete before beginning " + 706 "overall statistics collection. Specifying a nonzero " + 707 "number of warm-up intervals gives the client and server " + 708 "a chance to warm up without skewing performance results."; 709 warmUpIntervals = new IntegerArgument(null, "warmUpIntervals", true, 1, 710 "{num}", description, 0, Integer.MAX_VALUE, 0); 711 warmUpIntervals.setArgumentGroupName("Rate Management Arguments"); 712 warmUpIntervals.addLongIdentifier("warm-up-intervals", true); 713 parser.addArgument(warmUpIntervals); 714 715 description = "Indicates the format to use for timestamps included in " + 716 "the output. A value of 'none' indicates that no " + 717 "timestamps should be included. A value of 'with-date' " + 718 "indicates that both the date and the time should be " + 719 "included. A value of 'without-date' indicates that only " + 720 "the time should be included."; 721 final Set<String> allowedFormats = 722 StaticUtils.setOf("none", "with-date", "without-date"); 723 timestampFormat = new StringArgument(null, "timestampFormat", true, 1, 724 "{format}", description, allowedFormats, "none"); 725 timestampFormat.addLongIdentifier("timestamp-format", true); 726 parser.addArgument(timestampFormat); 727 728 description = "Indicates that information about the result codes for " + 729 "failed operations should not be displayed."; 730 suppressErrorsArgument = new BooleanArgument(null, 731 "suppressErrorResultCodes", 1, description); 732 suppressErrorsArgument.addLongIdentifier("suppress-error-result-codes", 733 true); 734 parser.addArgument(suppressErrorsArgument); 735 736 description = "Generate output in CSV format rather than a " + 737 "display-friendly format"; 738 csvFormat = new BooleanArgument('c', "csv", 1, description); 739 parser.addArgument(csvFormat); 740 741 description = "Specifies the seed to use for the random number generator."; 742 randomSeed = new IntegerArgument('R', "randomSeed", false, 1, "{value}", 743 description); 744 randomSeed.addLongIdentifier("random-seed", true); 745 parser.addArgument(randomSeed); 746 } 747 748 749 750 /** 751 * Indicates whether this tool supports creating connections to multiple 752 * servers. If it is to support multiple servers, then the "--hostname" and 753 * "--port" arguments will be allowed to be provided multiple times, and 754 * will be required to be provided the same number of times. The same type of 755 * communication security and bind credentials will be used for all servers. 756 * 757 * @return {@code true} if this tool supports creating connections to 758 * multiple servers, or {@code false} if not. 759 */ 760 @Override() 761 protected boolean supportsMultipleServers() 762 { 763 return true; 764 } 765 766 767 768 /** 769 * Retrieves the connection options that should be used for connections 770 * created for use with this tool. 771 * 772 * @return The connection options that should be used for connections created 773 * for use with this tool. 774 */ 775 @Override() 776 @NotNull() 777 public LDAPConnectionOptions getConnectionOptions() 778 { 779 final LDAPConnectionOptions options = new LDAPConnectionOptions(); 780 options.setUseSynchronousMode(true); 781 return options; 782 } 783 784 785 786 /** 787 * Performs the actual processing for this tool. In this case, it gets a 788 * connection to the directory server and uses it to perform the requested 789 * searches. 790 * 791 * @return The result code for the processing that was performed. 792 */ 793 @Override() 794 @NotNull() 795 public ResultCode doToolProcessing() 796 { 797 // If the sample rate file argument was specified, then generate the sample 798 // variable rate data file and return. 799 if (sampleRateFile.isPresent()) 800 { 801 try 802 { 803 RateAdjustor.writeSampleVariableRateFile(sampleRateFile.getValue()); 804 return ResultCode.SUCCESS; 805 } 806 catch (final Exception e) 807 { 808 Debug.debugException(e); 809 err("An error occurred while trying to write sample variable data " + 810 "rate file '", sampleRateFile.getValue().getAbsolutePath(), 811 "': ", StaticUtils.getExceptionMessage(e)); 812 return ResultCode.LOCAL_ERROR; 813 } 814 } 815 816 817 // Determine the random seed to use. 818 final Long seed; 819 if (randomSeed.isPresent()) 820 { 821 seed = Long.valueOf(randomSeed.getValue()); 822 } 823 else 824 { 825 seed = null; 826 } 827 828 // Create value patterns for the base DN and filter. 829 final ValuePattern dnPattern; 830 try 831 { 832 dnPattern = new ValuePattern(baseDN.getValue(), seed); 833 } 834 catch (final ParseException pe) 835 { 836 Debug.debugException(pe); 837 err("Unable to parse the base DN value pattern: ", pe.getMessage()); 838 return ResultCode.PARAM_ERROR; 839 } 840 841 final ValuePattern filterPattern; 842 if (filter.isPresent()) 843 { 844 try 845 { 846 filterPattern = new ValuePattern(filter.getValue(), seed); 847 } 848 catch (final ParseException pe) 849 { 850 Debug.debugException(pe); 851 err("Unable to parse the filter pattern: ", pe.getMessage()); 852 return ResultCode.PARAM_ERROR; 853 } 854 } 855 else 856 { 857 filterPattern = null; 858 } 859 860 861 // Get the attributes to return. 862 final String[] attrs; 863 if (attributes.isPresent()) 864 { 865 final List<String> attrList = attributes.getValues(); 866 attrs = new String[attrList.size()]; 867 attrList.toArray(attrs); 868 } 869 else 870 { 871 attrs = StaticUtils.NO_STRINGS; 872 } 873 874 875 // If the --ratePerSecond option was specified, then limit the rate 876 // accordingly. 877 FixedRateBarrier fixedRateBarrier = null; 878 if (ratePerSecond.isPresent() || variableRateData.isPresent()) 879 { 880 // We might not have a rate per second if --variableRateData is specified. 881 // The rate typically doesn't matter except when we have warm-up 882 // intervals. In this case, we'll run at the max rate. 883 final int intervalSeconds = collectionInterval.getValue(); 884 final int ratePerInterval = 885 (ratePerSecond.getValue() == null) 886 ? Integer.MAX_VALUE 887 : ratePerSecond.getValue() * intervalSeconds; 888 fixedRateBarrier = 889 new FixedRateBarrier(1000L * intervalSeconds, ratePerInterval); 890 } 891 892 893 // If --variableRateData was specified, then initialize a RateAdjustor. 894 RateAdjustor rateAdjustor = null; 895 if (variableRateData.isPresent()) 896 { 897 try 898 { 899 rateAdjustor = RateAdjustor.newInstance(fixedRateBarrier, 900 ratePerSecond.getValue(), variableRateData.getValue()); 901 } 902 catch (final IOException | IllegalArgumentException e) 903 { 904 Debug.debugException(e); 905 err("Initializing the variable rates failed: " + e.getMessage()); 906 return ResultCode.PARAM_ERROR; 907 } 908 } 909 910 911 // Determine whether to include timestamps in the output and if so what 912 // format should be used for them. 913 final boolean includeTimestamp; 914 final String timeFormat; 915 if (timestampFormat.getValue().equalsIgnoreCase("with-date")) 916 { 917 includeTimestamp = true; 918 timeFormat = "dd/MM/yyyy HH:mm:ss"; 919 } 920 else if (timestampFormat.getValue().equalsIgnoreCase("without-date")) 921 { 922 includeTimestamp = true; 923 timeFormat = "HH:mm:ss"; 924 } 925 else 926 { 927 includeTimestamp = false; 928 timeFormat = null; 929 } 930 931 932 // Get the controls to include in bind requests. 933 final ArrayList<Control> bindControls = new ArrayList<>(5); 934 if (authorizationIdentityRequestControl.isPresent()) 935 { 936 bindControls.add(new AuthorizationIdentityRequestControl()); 937 } 938 939 if (passwordPolicyRequestControl.isPresent()) 940 { 941 bindControls.add(new DraftBeheraLDAPPasswordPolicy10RequestControl()); 942 } 943 944 bindControls.addAll(bindControl.getValues()); 945 946 947 // Determine whether any warm-up intervals should be run. 948 final long totalIntervals; 949 final boolean warmUp; 950 int remainingWarmUpIntervals = warmUpIntervals.getValue(); 951 if (remainingWarmUpIntervals > 0) 952 { 953 warmUp = true; 954 totalIntervals = 0L + numIntervals.getValue() + remainingWarmUpIntervals; 955 } 956 else 957 { 958 warmUp = true; 959 totalIntervals = 0L + numIntervals.getValue(); 960 } 961 962 963 // Create the table that will be used to format the output. 964 final OutputFormat outputFormat; 965 if (csvFormat.isPresent()) 966 { 967 outputFormat = OutputFormat.CSV; 968 } 969 else 970 { 971 outputFormat = OutputFormat.COLUMNS; 972 } 973 974 final ColumnFormatter formatter = new ColumnFormatter(includeTimestamp, 975 timeFormat, outputFormat, " ", 976 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 977 "Auths/Sec"), 978 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 979 "Avg Dur ms"), 980 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Recent", 981 "Errors/Sec"), 982 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 983 "Auths/Sec"), 984 new FormattableColumn(12, HorizontalAlignment.RIGHT, "Overall", 985 "Avg Dur ms")); 986 987 988 // Create values to use for statistics collection. 989 final AtomicLong authCounter = new AtomicLong(0L); 990 final AtomicLong errorCounter = new AtomicLong(0L); 991 final AtomicLong authDurations = new AtomicLong(0L); 992 final ResultCodeCounter rcCounter = new ResultCodeCounter(); 993 994 995 // Determine the length of each interval in milliseconds. 996 final long intervalMillis = 1000L * collectionInterval.getValue(); 997 998 999 // Create the threads to use for the searches. 1000 final CyclicBarrier barrier = new CyclicBarrier(numThreads.getValue() + 1); 1001 final AuthRateThread[] threads = new AuthRateThread[numThreads.getValue()]; 1002 for (int i=0; i < threads.length; i++) 1003 { 1004 final LDAPConnection searchConnection; 1005 final LDAPConnection bindConnection; 1006 try 1007 { 1008 searchConnection = getConnection(); 1009 bindConnection = getConnection(); 1010 } 1011 catch (final LDAPException le) 1012 { 1013 Debug.debugException(le); 1014 err("Unable to connect to the directory server: ", 1015 StaticUtils.getExceptionMessage(le)); 1016 return le.getResultCode(); 1017 } 1018 1019 threads[i] = new AuthRateThread(this, i, searchConnection, bindConnection, 1020 dnPattern, scopeArg.getValue(), filterPattern, attrs, 1021 userPassword.getValue(), bindOnly.isPresent(), authType.getValue(), 1022 searchControl.getValues(), bindControls, runningThreads, barrier, 1023 authCounter, authDurations, errorCounter, rcCounter, 1024 fixedRateBarrier); 1025 threads[i].start(); 1026 } 1027 1028 1029 // Display the table header. 1030 for (final String headerLine : formatter.getHeaderLines(true)) 1031 { 1032 out(headerLine); 1033 } 1034 1035 1036 // Start the RateAdjustor before the threads so that the initial value is 1037 // in place before any load is generated unless we're doing a warm-up in 1038 // which case, we'll start it after the warm-up is complete. 1039 if ((rateAdjustor != null) && (remainingWarmUpIntervals <= 0)) 1040 { 1041 rateAdjustor.start(); 1042 } 1043 1044 1045 // Indicate that the threads can start running. 1046 try 1047 { 1048 barrier.await(); 1049 } 1050 catch (final Exception e) 1051 { 1052 Debug.debugException(e); 1053 } 1054 1055 long overallStartTime = System.nanoTime(); 1056 long nextIntervalStartTime = System.currentTimeMillis() + intervalMillis; 1057 1058 1059 boolean setOverallStartTime = false; 1060 long lastDuration = 0L; 1061 long lastNumErrors = 0L; 1062 long lastNumAuths = 0L; 1063 long lastEndTime = System.nanoTime(); 1064 for (long i=0; i < totalIntervals; i++) 1065 { 1066 if (rateAdjustor != null) 1067 { 1068 if (! rateAdjustor.isAlive()) 1069 { 1070 out("All of the rates in " + variableRateData.getValue().getName() + 1071 " have been completed."); 1072 break; 1073 } 1074 } 1075 1076 final long startTimeMillis = System.currentTimeMillis(); 1077 final long sleepTimeMillis = nextIntervalStartTime - startTimeMillis; 1078 nextIntervalStartTime += intervalMillis; 1079 if (sleepTimeMillis > 0) 1080 { 1081 sleeper.sleep(sleepTimeMillis); 1082 } 1083 1084 if (stopRequested.get()) 1085 { 1086 break; 1087 } 1088 1089 final long endTime = System.nanoTime(); 1090 final long intervalDuration = endTime - lastEndTime; 1091 1092 final long numAuths; 1093 final long numErrors; 1094 final long totalDuration; 1095 if (warmUp && (remainingWarmUpIntervals > 0)) 1096 { 1097 numAuths = authCounter.getAndSet(0L); 1098 numErrors = errorCounter.getAndSet(0L); 1099 totalDuration = authDurations.getAndSet(0L); 1100 } 1101 else 1102 { 1103 numAuths = authCounter.get(); 1104 numErrors = errorCounter.get(); 1105 totalDuration = authDurations.get(); 1106 } 1107 1108 final long recentNumAuths = numAuths - lastNumAuths; 1109 final long recentNumErrors = numErrors - lastNumErrors; 1110 final long recentDuration = totalDuration - lastDuration; 1111 1112 final double numSeconds = intervalDuration / 1_000_000_000.0d; 1113 final double recentAuthRate = recentNumAuths / numSeconds; 1114 final double recentErrorRate = recentNumErrors / numSeconds; 1115 1116 final double recentAvgDuration; 1117 if (recentNumAuths > 0L) 1118 { 1119 recentAvgDuration = 1.0d * recentDuration / recentNumAuths / 1_000_000; 1120 } 1121 else 1122 { 1123 recentAvgDuration = 0.0d; 1124 } 1125 1126 if (warmUp && (remainingWarmUpIntervals > 0)) 1127 { 1128 out(formatter.formatRow(recentAuthRate, recentAvgDuration, 1129 recentErrorRate, "warming up", "warming up")); 1130 1131 remainingWarmUpIntervals--; 1132 if (remainingWarmUpIntervals == 0) 1133 { 1134 out("Warm-up completed. Beginning overall statistics collection."); 1135 setOverallStartTime = true; 1136 if (rateAdjustor != null) 1137 { 1138 rateAdjustor.start(); 1139 } 1140 } 1141 } 1142 else 1143 { 1144 if (setOverallStartTime) 1145 { 1146 overallStartTime = lastEndTime; 1147 setOverallStartTime = false; 1148 } 1149 1150 final double numOverallSeconds = 1151 (endTime - overallStartTime) / 1_000_000_000.0d; 1152 final double overallAuthRate = numAuths / numOverallSeconds; 1153 1154 final double overallAvgDuration; 1155 if (numAuths > 0L) 1156 { 1157 overallAvgDuration = 1.0d * totalDuration / numAuths / 1_000_000; 1158 } 1159 else 1160 { 1161 overallAvgDuration = 0.0d; 1162 } 1163 1164 out(formatter.formatRow(recentAuthRate, recentAvgDuration, 1165 recentErrorRate, overallAuthRate, overallAvgDuration)); 1166 1167 lastNumAuths = numAuths; 1168 lastNumErrors = numErrors; 1169 lastDuration = totalDuration; 1170 } 1171 1172 final List<ObjectPair<ResultCode,Long>> rcCounts = 1173 rcCounter.getCounts(true); 1174 if ((! suppressErrorsArgument.isPresent()) && (! rcCounts.isEmpty())) 1175 { 1176 err("\tError Results:"); 1177 for (final ObjectPair<ResultCode,Long> p : rcCounts) 1178 { 1179 err("\t", p.getFirst().getName(), ": ", p.getSecond()); 1180 } 1181 } 1182 1183 lastEndTime = endTime; 1184 } 1185 1186 1187 // Shut down the RateAdjustor if we have one. 1188 if (rateAdjustor != null) 1189 { 1190 rateAdjustor.shutDown(); 1191 } 1192 1193 1194 // Stop all of the threads. 1195 ResultCode resultCode = ResultCode.SUCCESS; 1196 for (final AuthRateThread t : threads) 1197 { 1198 final ResultCode r = t.stopRunning(); 1199 if (resultCode == ResultCode.SUCCESS) 1200 { 1201 resultCode = r; 1202 } 1203 } 1204 1205 return resultCode; 1206 } 1207 1208 1209 1210 /** 1211 * Requests that this tool stop running. This method will attempt to wait 1212 * for all threads to complete before returning control to the caller. 1213 */ 1214 public void stopRunning() 1215 { 1216 stopRequested.set(true); 1217 sleeper.wakeup(); 1218 1219 while (true) 1220 { 1221 final int stillRunning = runningThreads.get(); 1222 if (stillRunning <= 0) 1223 { 1224 break; 1225 } 1226 else 1227 { 1228 try 1229 { 1230 Thread.sleep(1L); 1231 } catch (final Exception e) {} 1232 } 1233 } 1234 } 1235 1236 1237 1238 /** 1239 * {@inheritDoc} 1240 */ 1241 @Override() 1242 @NotNull() 1243 public LinkedHashMap<String[],String> getExampleUsages() 1244 { 1245 final LinkedHashMap<String[],String> examples = 1246 new LinkedHashMap<>(StaticUtils.computeMapCapacity(2)); 1247 1248 String[] args = 1249 { 1250 "--hostname", "server.example.com", 1251 "--port", "389", 1252 "--bindDN", "uid=admin,dc=example,dc=com", 1253 "--bindPassword", "password", 1254 "--baseDN", "dc=example,dc=com", 1255 "--scope", "sub", 1256 "--filter", "(uid=user.[1-1000000])", 1257 "--credentials", "password", 1258 "--numThreads", "10" 1259 }; 1260 String description = 1261 "Test authentication performance by searching randomly across a set " + 1262 "of one million users located below 'dc=example,dc=com' with ten " + 1263 "concurrent threads and performing simple binds with a password of " + 1264 "'password'. The searches will be performed anonymously."; 1265 examples.put(args, description); 1266 1267 args = new String[] 1268 { 1269 "--generateSampleRateFile", "variable-rate-data.txt" 1270 }; 1271 description = 1272 "Generate a sample variable rate definition file that may be used " + 1273 "in conjunction with the --variableRateData argument. The sample " + 1274 "file will include comments that describe the format for data to be " + 1275 "included in this file."; 1276 examples.put(args, description); 1277 1278 return examples; 1279 } 1280}