001/* 002 * Copyright 2010-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2010-2023 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-2023 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 the LDAP-specific arguments should include alternate 340 * versions of all long identifiers that consist of multiple words so that 341 * they are available in both camelCase and dash-separated versions. 342 * 343 * @return {@code true} if this tool should provide multiple versions of 344 * long identifiers for LDAP-specific arguments, or {@code false} if 345 * not. 346 */ 347 @Override() 348 protected boolean includeAlternateLongIdentifiers() 349 { 350 return true; 351 } 352 353 354 355 /** 356 * Indicates whether this tool should provide a command-line argument that 357 * allows for low-level SSL debugging. If this returns {@code true}, then an 358 * "--enableSSLDebugging}" argument will be added that sets the 359 * "javax.net.debug" system property to "all" before attempting any 360 * communication. 361 * 362 * @return {@code true} if this tool should offer an "--enableSSLDebugging" 363 * argument, or {@code false} if not. 364 */ 365 @Override() 366 protected boolean supportsSSLDebugging() 367 { 368 return true; 369 } 370 371 372 373 /** 374 * Adds the arguments used by this program that aren't already provided by the 375 * generic {@code LDAPCommandLineTool} framework. 376 * 377 * @param parser The argument parser to which the arguments should be added. 378 * 379 * @throws ArgumentException If a problem occurs while adding the arguments. 380 */ 381 @Override() 382 public void addNonLDAPArguments(@NotNull final ArgumentParser parser) 383 throws ArgumentException 384 { 385 this.parser = parser; 386 387 String description = "The address on which to listen for client " + 388 "connections. If this is not provided, then it will listen on " + 389 "all interfaces."; 390 listenAddress = new StringArgument('a', "listenAddress", false, 1, 391 "{address}", description); 392 listenAddress.addLongIdentifier("listen-address", true); 393 parser.addArgument(listenAddress); 394 395 396 description = "The port on which to listen for client connections. If " + 397 "no value is provided, then a free port will be automatically " + 398 "selected."; 399 listenPort = new IntegerArgument('L', "listenPort", true, 1, "{port}", 400 description, 0, 65_535, 0); 401 listenPort.addLongIdentifier("listen-port", true); 402 parser.addArgument(listenPort); 403 404 405 description = "Use SSL when accepting client connections. This is " + 406 "independent of the '--useSSL' option, which applies only to " + 407 "communication between the LDAP debugger and the backend server. " + 408 "If this argument is provided, then either the --keyStorePath or " + 409 "the --generateSelfSignedCertificate argument must also be provided."; 410 listenUsingSSL = new BooleanArgument('S', "listenUsingSSL", 1, 411 description); 412 listenUsingSSL.addLongIdentifier("listen-using-ssl", true); 413 parser.addArgument(listenUsingSSL); 414 415 416 description = "Generate a self-signed certificate to present to clients " + 417 "when the --listenUsingSSL argument is provided. This argument " + 418 "cannot be used in conjunction with the --keyStorePath argument."; 419 generateSelfSignedCertificate = new BooleanArgument(null, 420 "generateSelfSignedCertificate", 1, description); 421 generateSelfSignedCertificate.addLongIdentifier( 422 "generate-self-signed-certificate", true); 423 parser.addArgument(generateSelfSignedCertificate); 424 425 426 description = "The path to the output file to be written. If no value " + 427 "is provided, then the output will be written to standard output."; 428 outputFile = new FileArgument('f', "outputFile", false, 1, "{path}", 429 description, false, true, true, false); 430 outputFile.addLongIdentifier("output-file", true); 431 parser.addArgument(outputFile); 432 433 434 description = "The path to the a code log file to be written. If a " + 435 "value is provided, then the tool will generate sample code that " + 436 "corresponds to the requests received from clients. If no value is " + 437 "provided, then no code log will be generated."; 438 codeLogFile = new FileArgument('c', "codeLogFile", false, 1, "{path}", 439 description, false, true, true, false); 440 codeLogFile.addLongIdentifier("code-log-file", true); 441 parser.addArgument(codeLogFile); 442 443 444 // If --listenUsingSSL is provided, then either the --keyStorePath argument 445 // or the --generateSelfSignedCertificate argument must also be provided. 446 final Argument keyStorePathArgument = 447 parser.getNamedArgument("keyStorePath"); 448 parser.addDependentArgumentSet(listenUsingSSL, keyStorePathArgument, 449 generateSelfSignedCertificate); 450 451 452 // The --generateSelfSignedCertificate argument cannot be used with any of 453 // the arguments pertaining to a key store path. 454 final Argument keyStorePasswordArgument = 455 parser.getNamedArgument("keyStorePassword"); 456 final Argument keyStorePasswordFileArgument = 457 parser.getNamedArgument("keyStorePasswordFile"); 458 final Argument promptForKeyStorePasswordArgument = 459 parser.getNamedArgument("promptForKeyStorePassword"); 460 parser.addExclusiveArgumentSet(generateSelfSignedCertificate, 461 keyStorePathArgument); 462 parser.addExclusiveArgumentSet(generateSelfSignedCertificate, 463 keyStorePasswordArgument); 464 parser.addExclusiveArgumentSet(generateSelfSignedCertificate, 465 keyStorePasswordFileArgument); 466 parser.addExclusiveArgumentSet(generateSelfSignedCertificate, 467 promptForKeyStorePasswordArgument); 468 } 469 470 471 472 /** 473 * Performs the actual processing for this tool. In this case, it gets a 474 * connection to the directory server and uses it to perform the requested 475 * search. 476 * 477 * @return The result code for the processing that was performed. 478 */ 479 @Override() 480 @NotNull() 481 public ResultCode doToolProcessing() 482 { 483 // Create the proxy request handler that will be used to forward requests to 484 // a remote directory. 485 final ProxyRequestHandler proxyHandler; 486 try 487 { 488 proxyHandler = new ProxyRequestHandler(createServerSet()); 489 } 490 catch (final LDAPException le) 491 { 492 err("Unable to prepare to connect to the target server: ", 493 le.getMessage()); 494 return le.getResultCode(); 495 } 496 497 498 // Create the log handler to use for the output. 499 final Handler logHandler; 500 if (outputFile.isPresent()) 501 { 502 try 503 { 504 logHandler = new FileHandler(outputFile.getValue().getAbsolutePath()); 505 } 506 catch (final IOException ioe) 507 { 508 err("Unable to open the output file for writing: ", 509 StaticUtils.getExceptionMessage(ioe)); 510 return ResultCode.LOCAL_ERROR; 511 } 512 } 513 else 514 { 515 logHandler = new ConsoleHandler(); 516 } 517 StaticUtils.setLogHandlerLevel(logHandler, Level.INFO); 518 logHandler.setFormatter(new MinimalLogFormatter( 519 MinimalLogFormatter.DEFAULT_TIMESTAMP_FORMAT, false, false, true)); 520 521 522 // Create the debugger request handler that will be used to write the 523 // debug output. 524 LDAPListenerRequestHandler requestHandler = 525 new LDAPDebuggerRequestHandler(logHandler, proxyHandler); 526 527 528 // If a code log file was specified, then create the appropriate request 529 // handler to accomplish that. 530 if (codeLogFile.isPresent()) 531 { 532 try 533 { 534 requestHandler = new ToCodeRequestHandler(codeLogFile.getValue(), true, 535 requestHandler); 536 } 537 catch (final Exception e) 538 { 539 err("Unable to open code log file '", 540 codeLogFile.getValue().getAbsolutePath(), "' for writing: ", 541 StaticUtils.getExceptionMessage(e)); 542 return ResultCode.LOCAL_ERROR; 543 } 544 } 545 546 547 // Create and start the LDAP listener. 548 final LDAPListenerConfig config = 549 new LDAPListenerConfig(listenPort.getValue(), requestHandler); 550 if (listenAddress.isPresent()) 551 { 552 try 553 { 554 config.setListenAddress(LDAPConnectionOptions.DEFAULT_NAME_RESOLVER. 555 getByName(listenAddress.getValue())); 556 } 557 catch (final Exception e) 558 { 559 err("Unable to resolve '", listenAddress.getValue(), 560 "' as a valid address: ", StaticUtils.getExceptionMessage(e)); 561 return ResultCode.PARAM_ERROR; 562 } 563 } 564 565 if (listenUsingSSL.isPresent()) 566 { 567 try 568 { 569 final SSLUtil sslUtil; 570 if (generateSelfSignedCertificate.isPresent()) 571 { 572 final ObjectPair<File,char[]> keyStoreInfo = 573 SelfSignedCertificateGenerator. 574 generateTemporarySelfSignedCertificate(getToolName(), 575 CryptoHelper.KEY_STORE_TYPE_JKS); 576 577 sslUtil = new SSLUtil( 578 new KeyStoreKeyManager(keyStoreInfo.getFirst(), 579 keyStoreInfo.getSecond(), CryptoHelper.KEY_STORE_TYPE_JKS, 580 null, true), 581 new TrustAllTrustManager(false)); 582 } 583 else 584 { 585 sslUtil = createSSLUtil(true); 586 } 587 588 config.setServerSocketFactory(sslUtil.createSSLServerSocketFactory()); 589 } 590 catch (final Exception e) 591 { 592 err("Unable to create a server socket factory to accept SSL-based " + 593 "client connections: ", StaticUtils.getExceptionMessage(e)); 594 return ResultCode.LOCAL_ERROR; 595 } 596 } 597 598 listener = new LDAPListener(config); 599 600 try 601 { 602 listener.startListening(); 603 } 604 catch (final Exception e) 605 { 606 err("Unable to start listening for client connections: ", 607 StaticUtils.getExceptionMessage(e)); 608 return ResultCode.LOCAL_ERROR; 609 } 610 611 612 // Display a message with information about the port on which it is 613 // listening for connections. 614 int port = listener.getListenPort(); 615 while (port <= 0) 616 { 617 try 618 { 619 Thread.sleep(1L); 620 } 621 catch (final Exception e) 622 { 623 Debug.debugException(e); 624 625 if (e instanceof InterruptedException) 626 { 627 Thread.currentThread().interrupt(); 628 } 629 } 630 631 port = listener.getListenPort(); 632 } 633 634 if (listenUsingSSL.isPresent()) 635 { 636 out("Listening for SSL-based LDAP client connections on port ", port); 637 } 638 else 639 { 640 out("Listening for LDAP client connections on port ", port); 641 } 642 643 // Note that at this point, the listener will continue running in a 644 // separate thread, so we can return from this thread without exiting the 645 // program. However, we'll want to register a shutdown hook so that we can 646 // close the logger. 647 shutdownListener = new LDAPDebuggerShutdownListener(listener, logHandler); 648 Runtime.getRuntime().addShutdownHook(shutdownListener); 649 650 return ResultCode.SUCCESS; 651 } 652 653 654 655 /** 656 * {@inheritDoc} 657 */ 658 @Override() 659 @NotNull() 660 public LinkedHashMap<String[],String> getExampleUsages() 661 { 662 final LinkedHashMap<String[],String> examples = 663 new LinkedHashMap<>(StaticUtils.computeMapCapacity(1)); 664 665 final String[] args = 666 { 667 "--hostname", "server.example.com", 668 "--port", "389", 669 "--listenPort", "1389", 670 "--outputFile", "/tmp/ldap-debugger.log" 671 }; 672 final String description = 673 "Listen for client connections on port 1389 on all interfaces and " + 674 "forward any traffic received to server.example.com:389. The " + 675 "decoded LDAP communication will be written to the " + 676 "/tmp/ldap-debugger.log log file."; 677 examples.put(args, description); 678 679 return examples; 680 } 681 682 683 684 /** 685 * Retrieves the LDAP listener used to decode the communication. 686 * 687 * @return The LDAP listener used to decode the communication, or 688 * {@code null} if the tool is not running. 689 */ 690 @Nullable() 691 public LDAPListener getListener() 692 { 693 return listener; 694 } 695 696 697 698 /** 699 * Indicates that the associated listener should shut down. 700 */ 701 public void shutDown() 702 { 703 Runtime.getRuntime().removeShutdownHook(shutdownListener); 704 shutdownListener.run(); 705 } 706}