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 */
021 package com.unboundid.ldap.sdk.examples;
022
023
024
025 import java.io.IOException;
026 import java.io.OutputStream;
027 import java.io.Serializable;
028 import java.net.InetAddress;
029 import java.util.LinkedHashMap;
030 import java.util.logging.ConsoleHandler;
031 import java.util.logging.FileHandler;
032 import java.util.logging.Handler;
033 import java.util.logging.Level;
034
035 import com.unboundid.ldap.listener.LDAPDebuggerRequestHandler;
036 import com.unboundid.ldap.listener.LDAPListenerRequestHandler;
037 import com.unboundid.ldap.listener.LDAPListener;
038 import com.unboundid.ldap.listener.LDAPListenerConfig;
039 import com.unboundid.ldap.listener.ProxyRequestHandler;
040 import com.unboundid.ldap.listener.ToCodeRequestHandler;
041 import com.unboundid.ldap.sdk.LDAPException;
042 import com.unboundid.ldap.sdk.ResultCode;
043 import com.unboundid.ldap.sdk.Version;
044 import com.unboundid.util.Debug;
045 import com.unboundid.util.LDAPCommandLineTool;
046 import com.unboundid.util.MinimalLogFormatter;
047 import com.unboundid.util.StaticUtils;
048 import com.unboundid.util.ThreadSafety;
049 import com.unboundid.util.ThreadSafetyLevel;
050 import com.unboundid.util.args.ArgumentException;
051 import com.unboundid.util.args.ArgumentParser;
052 import com.unboundid.util.args.BooleanArgument;
053 import com.unboundid.util.args.FileArgument;
054 import com.unboundid.util.args.IntegerArgument;
055 import 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)
096 public 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 }