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