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