001/* 002 * Copyright 2010-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2010-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.File; 026import java.io.IOException; 027import java.io.OutputStream; 028import java.io.Serializable; 029import java.util.LinkedHashMap; 030import java.util.logging.ConsoleHandler; 031import java.util.logging.FileHandler; 032import java.util.logging.Handler; 033import java.util.logging.Level; 034 035import com.unboundid.ldap.listener.LDAPDebuggerRequestHandler; 036import com.unboundid.ldap.listener.LDAPListenerRequestHandler; 037import com.unboundid.ldap.listener.LDAPListener; 038import com.unboundid.ldap.listener.LDAPListenerConfig; 039import com.unboundid.ldap.listener.ProxyRequestHandler; 040import com.unboundid.ldap.listener.SelfSignedCertificateGenerator; 041import com.unboundid.ldap.listener.ToCodeRequestHandler; 042import com.unboundid.ldap.sdk.LDAPConnectionOptions; 043import com.unboundid.ldap.sdk.LDAPException; 044import com.unboundid.ldap.sdk.ResultCode; 045import com.unboundid.ldap.sdk.Version; 046import com.unboundid.util.Debug; 047import com.unboundid.util.LDAPCommandLineTool; 048import com.unboundid.util.MinimalLogFormatter; 049import com.unboundid.util.ObjectPair; 050import com.unboundid.util.StaticUtils; 051import com.unboundid.util.ThreadSafety; 052import com.unboundid.util.ThreadSafetyLevel; 053import com.unboundid.util.args.Argument; 054import com.unboundid.util.args.ArgumentException; 055import com.unboundid.util.args.ArgumentParser; 056import com.unboundid.util.args.BooleanArgument; 057import com.unboundid.util.args.FileArgument; 058import com.unboundid.util.args.IntegerArgument; 059import com.unboundid.util.args.StringArgument; 060import com.unboundid.util.ssl.KeyStoreKeyManager; 061import com.unboundid.util.ssl.SSLUtil; 062import com.unboundid.util.ssl.TrustAllTrustManager; 063 064 065 066/** 067 * This class provides a tool that can be used to create a simple listener that 068 * may be used to intercept and decode LDAP requests before forwarding them to 069 * another directory server, and then intercept and decode responses before 070 * returning them to the client. Some of the APIs demonstrated by this example 071 * include: 072 * <UL> 073 * <LI>Argument Parsing (from the {@code com.unboundid.util.args} 074 * package)</LI> 075 * <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util} 076 * package)</LI> 077 * <LI>LDAP Listener API (from the {@code com.unboundid.ldap.listener} 078 * package)</LI> 079 * </UL> 080 * <BR><BR> 081 * All of the necessary information is provided using 082 * command line arguments. Supported arguments include those allowed by the 083 * {@link LDAPCommandLineTool} class, as well as the following additional 084 * arguments: 085 * <UL> 086 * <LI>"-a {address}" or "--listenAddress {address}" -- Specifies the address 087 * on which to listen for requests from clients.</LI> 088 * <LI>"-L {port}" or "--listenPort {port}" -- Specifies the port on which to 089 * listen for requests from clients.</LI> 090 * <LI>"-S" or "--listenUsingSSL" -- Indicates that the listener should 091 * accept connections from SSL-based clients rather than those using 092 * unencrypted LDAP.</LI> 093 * <LI>"-f {path}" or "--outputFile {path}" -- Specifies the path to the 094 * output file to be written. If this is not provided, then the output 095 * will be written to standard output.</LI> 096 * <LI>"-c {path}" or "--codeLogFile {path}" -- Specifies the path to a file 097 * to be written with generated code that corresponds to requests received 098 * from clients. If this is not provided, then no code log will be 099 * generated.</LI> 100 * </UL> 101 */ 102@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 103public final class LDAPDebugger 104 extends LDAPCommandLineTool 105 implements Serializable 106{ 107 /** 108 * The serial version UID for this serializable class. 109 */ 110 private static final long serialVersionUID = -8942937427428190983L; 111 112 113 114 // The argument parser for this tool. 115 private ArgumentParser parser; 116 117 // The argument used to specify the output file for the decoded content. 118 private BooleanArgument listenUsingSSL; 119 120 // The argument used to indicate that the listener should generate a 121 // self-signed certificate instead of using an existing keystore. 122 private BooleanArgument generateSelfSignedCertificate; 123 124 // The argument used to specify the code log file to use, if any. 125 private FileArgument codeLogFile; 126 127 // The argument used to specify the output file for the decoded content. 128 private FileArgument outputFile; 129 130 // The argument used to specify the port on which to listen for client 131 // connections. 132 private IntegerArgument listenPort; 133 134 // The shutdown hook that will be used to stop the listener when the JVM 135 // exits. 136 private LDAPDebuggerShutdownListener shutdownListener; 137 138 // The listener used to intercept and decode the client communication. 139 private LDAPListener listener; 140 141 // The argument used to specify the address on which to listen for client 142 // connections. 143 private StringArgument listenAddress; 144 145 146 147 /** 148 * Parse the provided command line arguments and make the appropriate set of 149 * changes. 150 * 151 * @param args The command line arguments provided to this program. 152 */ 153 public static void main(final String[] args) 154 { 155 final ResultCode resultCode = main(args, System.out, System.err); 156 if (resultCode != ResultCode.SUCCESS) 157 { 158 System.exit(resultCode.intValue()); 159 } 160 } 161 162 163 164 /** 165 * Parse the provided command line arguments and make the appropriate set of 166 * changes. 167 * 168 * @param args The command line arguments provided to this program. 169 * @param outStream The output stream to which standard out should be 170 * written. It may be {@code null} if output should be 171 * suppressed. 172 * @param errStream The output stream to which standard error should be 173 * written. It may be {@code null} if error messages 174 * should be suppressed. 175 * 176 * @return A result code indicating whether the processing was successful. 177 */ 178 public static ResultCode main(final String[] args, 179 final OutputStream outStream, 180 final OutputStream errStream) 181 { 182 final LDAPDebugger ldapDebugger = new LDAPDebugger(outStream, errStream); 183 return ldapDebugger.runTool(args); 184 } 185 186 187 188 /** 189 * Creates a new instance of this tool. 190 * 191 * @param outStream The output stream to which standard out should be 192 * written. It may be {@code null} if output should be 193 * suppressed. 194 * @param errStream The output stream to which standard error should be 195 * written. It may be {@code null} if error messages 196 * should be suppressed. 197 */ 198 public LDAPDebugger(final OutputStream outStream, 199 final OutputStream errStream) 200 { 201 super(outStream, errStream); 202 } 203 204 205 206 /** 207 * Retrieves the name for this tool. 208 * 209 * @return The name for this tool. 210 */ 211 @Override() 212 public String getToolName() 213 { 214 return "ldap-debugger"; 215 } 216 217 218 219 /** 220 * Retrieves the description for this tool. 221 * 222 * @return The description for this tool. 223 */ 224 @Override() 225 public String getToolDescription() 226 { 227 return "Intercept and decode LDAP communication."; 228 } 229 230 231 232 /** 233 * Retrieves the version string for this tool. 234 * 235 * @return The version string for this tool. 236 */ 237 @Override() 238 public String getToolVersion() 239 { 240 return Version.NUMERIC_VERSION_STRING; 241 } 242 243 244 245 /** 246 * Indicates whether this tool should provide support for an interactive mode, 247 * in which the tool offers a mode in which the arguments can be provided in 248 * a text-driven menu rather than requiring them to be given on the command 249 * line. If interactive mode is supported, it may be invoked using the 250 * "--interactive" argument. Alternately, if interactive mode is supported 251 * and {@link #defaultsToInteractiveMode()} returns {@code true}, then 252 * interactive mode may be invoked by simply launching the tool without any 253 * arguments. 254 * 255 * @return {@code true} if this tool supports interactive mode, or 256 * {@code false} if not. 257 */ 258 @Override() 259 public boolean supportsInteractiveMode() 260 { 261 return true; 262 } 263 264 265 266 /** 267 * Indicates whether this tool defaults to launching in interactive mode if 268 * the tool is invoked without any command-line arguments. This will only be 269 * used if {@link #supportsInteractiveMode()} returns {@code true}. 270 * 271 * @return {@code true} if this tool defaults to using interactive mode if 272 * launched without any command-line arguments, or {@code false} if 273 * not. 274 */ 275 @Override() 276 public boolean defaultsToInteractiveMode() 277 { 278 return true; 279 } 280 281 282 283 /** 284 * Indicates whether this tool should default to interactively prompting for 285 * the bind password if a password is required but no argument was provided 286 * to indicate how to get the password. 287 * 288 * @return {@code true} if this tool should default to interactively 289 * prompting for the bind password, or {@code false} if not. 290 */ 291 @Override() 292 protected boolean defaultToPromptForBindPassword() 293 { 294 return true; 295 } 296 297 298 299 /** 300 * Indicates whether this tool supports the use of a properties file for 301 * specifying default values for arguments that aren't specified on the 302 * command line. 303 * 304 * @return {@code true} if this tool supports the use of a properties file 305 * for specifying default values for arguments that aren't specified 306 * on the command line, or {@code false} if not. 307 */ 308 @Override() 309 public boolean supportsPropertiesFile() 310 { 311 return true; 312 } 313 314 315 316 /** 317 * Indicates whether the LDAP-specific arguments should include alternate 318 * versions of all long identifiers that consist of multiple words so that 319 * they are available in both camelCase and dash-separated versions. 320 * 321 * @return {@code true} if this tool should provide multiple versions of 322 * long identifiers for LDAP-specific arguments, or {@code false} if 323 * not. 324 */ 325 @Override() 326 protected boolean includeAlternateLongIdentifiers() 327 { 328 return true; 329 } 330 331 332 333 /** 334 * Indicates whether this tool should provide a command-line argument that 335 * allows for low-level SSL debugging. If this returns {@code true}, then an 336 * "--enableSSLDebugging}" argument will be added that sets the 337 * "javax.net.debug" system property to "all" before attempting any 338 * communication. 339 * 340 * @return {@code true} if this tool should offer an "--enableSSLDebugging" 341 * argument, or {@code false} if not. 342 */ 343 @Override() 344 protected boolean supportsSSLDebugging() 345 { 346 return true; 347 } 348 349 350 351 /** 352 * Adds the arguments used by this program that aren't already provided by the 353 * generic {@code LDAPCommandLineTool} framework. 354 * 355 * @param parser The argument parser to which the arguments should be added. 356 * 357 * @throws ArgumentException If a problem occurs while adding the arguments. 358 */ 359 @Override() 360 public void addNonLDAPArguments(final ArgumentParser parser) 361 throws ArgumentException 362 { 363 this.parser = parser; 364 365 String description = "The address on which to listen for client " + 366 "connections. If this is not provided, then it will listen on " + 367 "all interfaces."; 368 listenAddress = new StringArgument('a', "listenAddress", false, 1, 369 "{address}", description); 370 listenAddress.addLongIdentifier("listen-address", true); 371 parser.addArgument(listenAddress); 372 373 374 description = "The port on which to listen for client connections. If " + 375 "no value is provided, then a free port will be automatically " + 376 "selected."; 377 listenPort = new IntegerArgument('L', "listenPort", true, 1, "{port}", 378 description, 0, 65_535, 0); 379 listenPort.addLongIdentifier("listen-port", true); 380 parser.addArgument(listenPort); 381 382 383 description = "Use SSL when accepting client connections. This is " + 384 "independent of the '--useSSL' option, which applies only to " + 385 "communication between the LDAP debugger and the backend server. " + 386 "If this argument is provided, then either the --keyStorePath or " + 387 "the --generateSelfSignedCertificate argument must also be provided."; 388 listenUsingSSL = new BooleanArgument('S', "listenUsingSSL", 1, 389 description); 390 listenUsingSSL.addLongIdentifier("listen-using-ssl", true); 391 parser.addArgument(listenUsingSSL); 392 393 394 description = "Generate a self-signed certificate to present to clients " + 395 "when the --listenUsingSSL argument is provided. This argument " + 396 "cannot be used in conjunction with the --keyStorePath argument."; 397 generateSelfSignedCertificate = new BooleanArgument(null, 398 "generateSelfSignedCertificate", 1, description); 399 generateSelfSignedCertificate.addLongIdentifier( 400 "generate-self-signed-certificate", true); 401 parser.addArgument(generateSelfSignedCertificate); 402 403 404 description = "The path to the output file to be written. If no value " + 405 "is provided, then the output will be written to standard output."; 406 outputFile = new FileArgument('f', "outputFile", false, 1, "{path}", 407 description, false, true, true, false); 408 outputFile.addLongIdentifier("output-file", true); 409 parser.addArgument(outputFile); 410 411 412 description = "The path to the a code log file to be written. If a " + 413 "value is provided, then the tool will generate sample code that " + 414 "corresponds to the requests received from clients. If no value is " + 415 "provided, then no code log will be generated."; 416 codeLogFile = new FileArgument('c', "codeLogFile", false, 1, "{path}", 417 description, false, true, true, false); 418 codeLogFile.addLongIdentifier("code-log-file", true); 419 parser.addArgument(codeLogFile); 420 421 422 // If --listenUsingSSL is provided, then either the --keyStorePath argument 423 // or the --generateSelfSignedCertificate argument must also be provided. 424 final Argument keyStorePathArgument = 425 parser.getNamedArgument("keyStorePath"); 426 parser.addDependentArgumentSet(listenUsingSSL, keyStorePathArgument, 427 generateSelfSignedCertificate); 428 429 430 // The --generateSelfSignedCertificate argument cannot be used with any of 431 // the arguments pertaining to a key store path. 432 final Argument keyStorePasswordArgument = 433 parser.getNamedArgument("keyStorePassword"); 434 final Argument keyStorePasswordFileArgument = 435 parser.getNamedArgument("keyStorePasswordFile"); 436 final Argument promptForKeyStorePasswordArgument = 437 parser.getNamedArgument("promptForKeyStorePassword"); 438 parser.addExclusiveArgumentSet(generateSelfSignedCertificate, 439 keyStorePathArgument); 440 parser.addExclusiveArgumentSet(generateSelfSignedCertificate, 441 keyStorePasswordArgument); 442 parser.addExclusiveArgumentSet(generateSelfSignedCertificate, 443 keyStorePasswordFileArgument); 444 parser.addExclusiveArgumentSet(generateSelfSignedCertificate, 445 promptForKeyStorePasswordArgument); 446 } 447 448 449 450 /** 451 * Performs the actual processing for this tool. In this case, it gets a 452 * connection to the directory server and uses it to perform the requested 453 * search. 454 * 455 * @return The result code for the processing that was performed. 456 */ 457 @Override() 458 public ResultCode doToolProcessing() 459 { 460 // Create the proxy request handler that will be used to forward requests to 461 // a remote directory. 462 final ProxyRequestHandler proxyHandler; 463 try 464 { 465 proxyHandler = new ProxyRequestHandler(createServerSet()); 466 } 467 catch (final LDAPException le) 468 { 469 err("Unable to prepare to connect to the target server: ", 470 le.getMessage()); 471 return le.getResultCode(); 472 } 473 474 475 // Create the log handler to use for the output. 476 final Handler logHandler; 477 if (outputFile.isPresent()) 478 { 479 try 480 { 481 logHandler = new FileHandler(outputFile.getValue().getAbsolutePath()); 482 } 483 catch (final IOException ioe) 484 { 485 err("Unable to open the output file for writing: ", 486 StaticUtils.getExceptionMessage(ioe)); 487 return ResultCode.LOCAL_ERROR; 488 } 489 } 490 else 491 { 492 logHandler = new ConsoleHandler(); 493 } 494 StaticUtils.setLogHandlerLevel(logHandler, Level.INFO); 495 logHandler.setFormatter(new MinimalLogFormatter( 496 MinimalLogFormatter.DEFAULT_TIMESTAMP_FORMAT, false, false, true)); 497 498 499 // Create the debugger request handler that will be used to write the 500 // debug output. 501 LDAPListenerRequestHandler requestHandler = 502 new LDAPDebuggerRequestHandler(logHandler, proxyHandler); 503 504 505 // If a code log file was specified, then create the appropriate request 506 // handler to accomplish that. 507 if (codeLogFile.isPresent()) 508 { 509 try 510 { 511 requestHandler = new ToCodeRequestHandler(codeLogFile.getValue(), true, 512 requestHandler); 513 } 514 catch (final Exception e) 515 { 516 err("Unable to open code log file '", 517 codeLogFile.getValue().getAbsolutePath(), "' for writing: ", 518 StaticUtils.getExceptionMessage(e)); 519 return ResultCode.LOCAL_ERROR; 520 } 521 } 522 523 524 // Create and start the LDAP listener. 525 final LDAPListenerConfig config = 526 new LDAPListenerConfig(listenPort.getValue(), requestHandler); 527 if (listenAddress.isPresent()) 528 { 529 try 530 { 531 config.setListenAddress(LDAPConnectionOptions.DEFAULT_NAME_RESOLVER. 532 getByName(listenAddress.getValue())); 533 } 534 catch (final Exception e) 535 { 536 err("Unable to resolve '", listenAddress.getValue(), 537 "' as a valid address: ", StaticUtils.getExceptionMessage(e)); 538 return ResultCode.PARAM_ERROR; 539 } 540 } 541 542 if (listenUsingSSL.isPresent()) 543 { 544 try 545 { 546 final SSLUtil sslUtil; 547 if (generateSelfSignedCertificate.isPresent()) 548 { 549 final ObjectPair<File,char[]> keyStoreInfo = 550 SelfSignedCertificateGenerator. 551 generateTemporarySelfSignedCertificate(getToolName(), 552 "JKS"); 553 554 sslUtil = new SSLUtil( 555 new KeyStoreKeyManager(keyStoreInfo.getFirst(), 556 keyStoreInfo.getSecond(), "JKS", null), 557 new TrustAllTrustManager(false)); 558 } 559 else 560 { 561 sslUtil = createSSLUtil(true); 562 } 563 564 config.setServerSocketFactory(sslUtil.createSSLServerSocketFactory()); 565 } 566 catch (final Exception e) 567 { 568 err("Unable to create a server socket factory to accept SSL-based " + 569 "client connections: ", StaticUtils.getExceptionMessage(e)); 570 return ResultCode.LOCAL_ERROR; 571 } 572 } 573 574 listener = new LDAPListener(config); 575 576 try 577 { 578 listener.startListening(); 579 } 580 catch (final Exception e) 581 { 582 err("Unable to start listening for client connections: ", 583 StaticUtils.getExceptionMessage(e)); 584 return ResultCode.LOCAL_ERROR; 585 } 586 587 588 // Display a message with information about the port on which it is 589 // listening for connections. 590 int port = listener.getListenPort(); 591 while (port <= 0) 592 { 593 try 594 { 595 Thread.sleep(1L); 596 } 597 catch (final Exception e) 598 { 599 Debug.debugException(e); 600 601 if (e instanceof InterruptedException) 602 { 603 Thread.currentThread().interrupt(); 604 } 605 } 606 607 port = listener.getListenPort(); 608 } 609 610 if (listenUsingSSL.isPresent()) 611 { 612 out("Listening for SSL-based LDAP client connections on port ", port); 613 } 614 else 615 { 616 out("Listening for LDAP client connections on port ", port); 617 } 618 619 // Note that at this point, the listener will continue running in a 620 // separate thread, so we can return from this thread without exiting the 621 // program. However, we'll want to register a shutdown hook so that we can 622 // close the logger. 623 shutdownListener = new LDAPDebuggerShutdownListener(listener, logHandler); 624 Runtime.getRuntime().addShutdownHook(shutdownListener); 625 626 return ResultCode.SUCCESS; 627 } 628 629 630 631 /** 632 * {@inheritDoc} 633 */ 634 @Override() 635 public LinkedHashMap<String[],String> getExampleUsages() 636 { 637 final LinkedHashMap<String[],String> examples = 638 new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); 639 640 final String[] args = 641 { 642 "--hostname", "server.example.com", 643 "--port", "389", 644 "--listenPort", "1389", 645 "--outputFile", "/tmp/ldap-debugger.log" 646 }; 647 final String description = 648 "Listen for client connections on port 1389 on all interfaces and " + 649 "forward any traffic received to server.example.com:389. The " + 650 "decoded LDAP communication will be written to the " + 651 "/tmp/ldap-debugger.log log file."; 652 examples.put(args, description); 653 654 return examples; 655 } 656 657 658 659 /** 660 * Retrieves the LDAP listener used to decode the communication. 661 * 662 * @return The LDAP listener used to decode the communication, or 663 * {@code null} if the tool is not running. 664 */ 665 public LDAPListener getListener() 666 { 667 return listener; 668 } 669 670 671 672 /** 673 * Indicates that the associated listener should shut down. 674 */ 675 public void shutDown() 676 { 677 Runtime.getRuntime().removeShutdownHook(shutdownListener); 678 shutdownListener.run(); 679 } 680}