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