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