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