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