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