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