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 */
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.LDAPCommandLineTool;
045 import com.unboundid.util.MinimalLogFormatter;
046 import com.unboundid.util.StaticUtils;
047 import com.unboundid.util.ThreadSafety;
048 import com.unboundid.util.ThreadSafetyLevel;
049 import com.unboundid.util.args.ArgumentException;
050 import com.unboundid.util.args.ArgumentParser;
051 import com.unboundid.util.args.BooleanArgument;
052 import com.unboundid.util.args.FileArgument;
053 import com.unboundid.util.args.IntegerArgument;
054 import 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)
095 public 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 }