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