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