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 supports the use of a properties file for 270 * specifying default values for arguments that aren't specified on the 271 * command line. 272 * 273 * @return {@code true} if this tool supports the use of a properties file 274 * for specifying default values for arguments that aren't specified 275 * on the command line, or {@code false} if not. 276 */ 277 @Override() 278 public boolean supportsPropertiesFile() 279 { 280 return true; 281 } 282 283 284 285 /** 286 * Indicates whether the LDAP-specific arguments should include alternate 287 * versions of all long identifiers that consist of multiple words so that 288 * they are available in both camelCase and dash-separated versions. 289 * 290 * @return {@code true} if this tool should provide multiple versions of 291 * long identifiers for LDAP-specific arguments, or {@code false} if 292 * not. 293 */ 294 @Override() 295 protected boolean includeAlternateLongIdentifiers() 296 { 297 return true; 298 } 299 300 301 302 /** 303 * Adds the arguments used by this program that aren't already provided by the 304 * generic {@code LDAPCommandLineTool} framework. 305 * 306 * @param parser The argument parser to which the arguments should be added. 307 * 308 * @throws ArgumentException If a problem occurs while adding the arguments. 309 */ 310 @Override() 311 public void addNonLDAPArguments(final ArgumentParser parser) 312 throws ArgumentException 313 { 314 String description = "The address on which to listen for client " + 315 "connections. If this is not provided, then it will listen on " + 316 "all interfaces."; 317 listenAddress = new StringArgument('a', "listenAddress", false, 1, 318 "{address}", description); 319 listenAddress.addLongIdentifier("listen-address"); 320 parser.addArgument(listenAddress); 321 322 323 description = "The port on which to listen for client connections. If " + 324 "no value is provided, then a free port will be automatically " + 325 "selected."; 326 listenPort = new IntegerArgument('L', "listenPort", true, 1, "{port}", 327 description, 0, 65535, 0); 328 listenPort.addLongIdentifier("listen-port"); 329 parser.addArgument(listenPort); 330 331 332 description = "Use SSL when accepting client connections. This is " + 333 "independent of the '--useSSL' option, which applies only to " + 334 "communication between the LDAP debugger and the backend server."; 335 listenUsingSSL = new BooleanArgument('S', "listenUsingSSL", 1, 336 description); 337 listenUsingSSL.addLongIdentifier("listen-using-ssl"); 338 parser.addArgument(listenUsingSSL); 339 340 341 description = "The path to the output file to be written. If no value " + 342 "is provided, then the output will be written to standard output."; 343 outputFile = new FileArgument('f', "outputFile", false, 1, "{path}", 344 description, false, true, true, false); 345 outputFile.addLongIdentifier("output-file"); 346 parser.addArgument(outputFile); 347 348 349 description = "The path to the a code log file to be written. If a " + 350 "value is provided, then the tool will generate sample code that " + 351 "corresponds to the requests received from clients. If no value is " + 352 "provided, then no code log will be generated."; 353 codeLogFile = new FileArgument('c', "codeLogFile", false, 1, "{path}", 354 description, false, true, true, false); 355 codeLogFile.addLongIdentifier("code-log-file"); 356 parser.addArgument(codeLogFile); 357 } 358 359 360 361 /** 362 * Performs the actual processing for this tool. In this case, it gets a 363 * connection to the directory server and uses it to perform the requested 364 * search. 365 * 366 * @return The result code for the processing that was performed. 367 */ 368 @Override() 369 public ResultCode doToolProcessing() 370 { 371 // Create the proxy request handler that will be used to forward requests to 372 // a remote directory. 373 final ProxyRequestHandler proxyHandler; 374 try 375 { 376 proxyHandler = new ProxyRequestHandler(createServerSet()); 377 } 378 catch (final LDAPException le) 379 { 380 err("Unable to prepare to connect to the target server: ", 381 le.getMessage()); 382 return le.getResultCode(); 383 } 384 385 386 // Create the log handler to use for the output. 387 final Handler logHandler; 388 if (outputFile.isPresent()) 389 { 390 try 391 { 392 logHandler = new FileHandler(outputFile.getValue().getAbsolutePath()); 393 } 394 catch (final IOException ioe) 395 { 396 err("Unable to open the output file for writing: ", 397 StaticUtils.getExceptionMessage(ioe)); 398 return ResultCode.LOCAL_ERROR; 399 } 400 } 401 else 402 { 403 logHandler = new ConsoleHandler(); 404 } 405 logHandler.setLevel(Level.INFO); 406 logHandler.setFormatter(new MinimalLogFormatter( 407 MinimalLogFormatter.DEFAULT_TIMESTAMP_FORMAT, false, false, true)); 408 409 410 // Create the debugger request handler that will be used to write the 411 // debug output. 412 LDAPListenerRequestHandler requestHandler = 413 new LDAPDebuggerRequestHandler(logHandler, proxyHandler); 414 415 416 // If a code log file was specified, then create the appropriate request 417 // handler to accomplish that. 418 if (codeLogFile.isPresent()) 419 { 420 try 421 { 422 requestHandler = new ToCodeRequestHandler(codeLogFile.getValue(), true, 423 requestHandler); 424 } 425 catch (final Exception e) 426 { 427 err("Unable to open code log file '", 428 codeLogFile.getValue().getAbsolutePath(), "' for writing: ", 429 StaticUtils.getExceptionMessage(e)); 430 return ResultCode.LOCAL_ERROR; 431 } 432 } 433 434 435 // Create and start the LDAP listener. 436 final LDAPListenerConfig config = 437 new LDAPListenerConfig(listenPort.getValue(), requestHandler); 438 if (listenAddress.isPresent()) 439 { 440 try 441 { 442 config.setListenAddress( 443 InetAddress.getByName(listenAddress.getValue())); 444 } 445 catch (final Exception e) 446 { 447 err("Unable to resolve '", listenAddress.getValue(), 448 "' as a valid address: ", StaticUtils.getExceptionMessage(e)); 449 return ResultCode.PARAM_ERROR; 450 } 451 } 452 453 if (listenUsingSSL.isPresent()) 454 { 455 try 456 { 457 config.setServerSocketFactory( 458 createSSLUtil(true).createSSLServerSocketFactory()); 459 } 460 catch (final Exception e) 461 { 462 err("Unable to create a server socket factory to accept SSL-based " + 463 "client connections: ", StaticUtils.getExceptionMessage(e)); 464 return ResultCode.LOCAL_ERROR; 465 } 466 } 467 468 listener = new LDAPListener(config); 469 470 try 471 { 472 listener.startListening(); 473 } 474 catch (final Exception e) 475 { 476 err("Unable to start listening for client connections: ", 477 StaticUtils.getExceptionMessage(e)); 478 return ResultCode.LOCAL_ERROR; 479 } 480 481 482 // Display a message with information about the port on which it is 483 // listening for connections. 484 int port = listener.getListenPort(); 485 while (port <= 0) 486 { 487 try 488 { 489 Thread.sleep(1L); 490 } catch (final Exception e) {} 491 492 port = listener.getListenPort(); 493 } 494 495 if (listenUsingSSL.isPresent()) 496 { 497 out("Listening for SSL-based LDAP client connections on port ", port); 498 } 499 else 500 { 501 out("Listening for LDAP client connections on port ", port); 502 } 503 504 // Note that at this point, the listener will continue running in a 505 // separate thread, so we can return from this thread without exiting the 506 // program. However, we'll want to register a shutdown hook so that we can 507 // close the logger. 508 shutdownListener = new LDAPDebuggerShutdownListener(listener, logHandler); 509 Runtime.getRuntime().addShutdownHook(shutdownListener); 510 511 return ResultCode.SUCCESS; 512 } 513 514 515 516 /** 517 * {@inheritDoc} 518 */ 519 @Override() 520 public LinkedHashMap<String[],String> getExampleUsages() 521 { 522 final LinkedHashMap<String[],String> examples = 523 new LinkedHashMap<String[],String>(); 524 525 final String[] args = 526 { 527 "--hostname", "server.example.com", 528 "--port", "389", 529 "--listenPort", "1389", 530 "--outputFile", "/tmp/ldap-debugger.log" 531 }; 532 final String description = 533 "Listen for client connections on port 1389 on all interfaces and " + 534 "forward any traffic received to server.example.com:389. The " + 535 "decoded LDAP communication will be written to the " + 536 "/tmp/ldap-debugger.log log file."; 537 examples.put(args, description); 538 539 return examples; 540 } 541 542 543 544 /** 545 * Retrieves the LDAP listener used to decode the communication. 546 * 547 * @return The LDAP listener used to decode the communication, or 548 * {@code null} if the tool is not running. 549 */ 550 public LDAPListener getListener() 551 { 552 return listener; 553 } 554 555 556 557 /** 558 * Indicates that the associated listener should shut down. 559 */ 560 public void shutDown() 561 { 562 Runtime.getRuntime().removeShutdownHook(shutdownListener); 563 shutdownListener.run(); 564 } 565}