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}