001/*
002 * Copyright 2010-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2010-2018 Ping Identity Corporation
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.Argument;
051import com.unboundid.util.args.ArgumentException;
052import com.unboundid.util.args.ArgumentParser;
053import com.unboundid.util.args.BooleanArgument;
054import com.unboundid.util.args.FileArgument;
055import com.unboundid.util.args.IntegerArgument;
056import com.unboundid.util.args.StringArgument;
057
058
059
060/**
061 * This class provides a tool that can be used to create a simple listener that
062 * may be used to intercept and decode LDAP requests before forwarding them to
063 * another directory server, and then intercept and decode responses before
064 * returning them to the client.  Some of the APIs demonstrated by this example
065 * include:
066 * <UL>
067 *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
068 *       package)</LI>
069 *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
070 *       package)</LI>
071 *   <LI>LDAP Listener API (from the {@code com.unboundid.ldap.listener}
072 *       package)</LI>
073 * </UL>
074 * <BR><BR>
075 * All of the necessary information is provided using
076 * command line arguments.  Supported arguments include those allowed by the
077 * {@link LDAPCommandLineTool} class, as well as the following additional
078 * arguments:
079 * <UL>
080 *   <LI>"-a {address}" or "--listenAddress {address}" -- Specifies the address
081 *       on which to listen for requests from clients.</LI>
082 *   <LI>"-L {port}" or "--listenPort {port}" -- Specifies the port on which to
083 *       listen for requests from clients.</LI>
084 *   <LI>"-S" or "--listenUsingSSL" -- Indicates that the listener should
085 *       accept connections from SSL-based clients rather than those using
086 *       unencrypted LDAP.</LI>
087 *   <LI>"-f {path}" or "--outputFile {path}" -- Specifies the path to the
088 *       output file to be written.  If this is not provided, then the output
089 *       will be written to standard output.</LI>
090 *   <LI>"-c {path}" or "--codeLogFile {path}" -- Specifies the path to a file
091 *       to be written with generated code that corresponds to requests received
092 *       from clients.  If this is not provided, then no code log will be
093 *       generated.</LI>
094 * </UL>
095 */
096@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
097public final class LDAPDebugger
098       extends LDAPCommandLineTool
099       implements Serializable
100{
101  /**
102   * The serial version UID for this serializable class.
103   */
104  private static final long serialVersionUID = -8942937427428190983L;
105
106
107
108  // The argument used to specify the output file for the decoded content.
109  private BooleanArgument listenUsingSSL;
110
111  // The argument used to specify the code log file to use, if any.
112  private FileArgument codeLogFile;
113
114  // The argument used to specify the output file for the decoded content.
115  private FileArgument outputFile;
116
117  // The argument used to specify the port on which to listen for client
118  // connections.
119  private IntegerArgument listenPort;
120
121  // The shutdown hook that will be used to stop the listener when the JVM
122  // exits.
123  private LDAPDebuggerShutdownListener shutdownListener;
124
125  // The listener used to intercept and decode the client communication.
126  private LDAPListener listener;
127
128  // The argument used to specify the address on which to listen for client
129  // connections.
130  private StringArgument listenAddress;
131
132
133
134  /**
135   * Parse the provided command line arguments and make the appropriate set of
136   * changes.
137   *
138   * @param  args  The command line arguments provided to this program.
139   */
140  public static void main(final String[] args)
141  {
142    final ResultCode resultCode = main(args, System.out, System.err);
143    if (resultCode != ResultCode.SUCCESS)
144    {
145      System.exit(resultCode.intValue());
146    }
147  }
148
149
150
151  /**
152   * Parse the provided command line arguments and make the appropriate set of
153   * changes.
154   *
155   * @param  args       The command line arguments provided to this program.
156   * @param  outStream  The output stream to which standard out should be
157   *                    written.  It may be {@code null} if output should be
158   *                    suppressed.
159   * @param  errStream  The output stream to which standard error should be
160   *                    written.  It may be {@code null} if error messages
161   *                    should be suppressed.
162   *
163   * @return  A result code indicating whether the processing was successful.
164   */
165  public static ResultCode main(final String[] args,
166                                final OutputStream outStream,
167                                final OutputStream errStream)
168  {
169    final LDAPDebugger ldapDebugger = new LDAPDebugger(outStream, errStream);
170    return ldapDebugger.runTool(args);
171  }
172
173
174
175  /**
176   * Creates a new instance of this tool.
177   *
178   * @param  outStream  The output stream to which standard out should be
179   *                    written.  It may be {@code null} if output should be
180   *                    suppressed.
181   * @param  errStream  The output stream to which standard error should be
182   *                    written.  It may be {@code null} if error messages
183   *                    should be suppressed.
184   */
185  public LDAPDebugger(final OutputStream outStream,
186                      final OutputStream errStream)
187  {
188    super(outStream, errStream);
189  }
190
191
192
193  /**
194   * Retrieves the name for this tool.
195   *
196   * @return  The name for this tool.
197   */
198  @Override()
199  public String getToolName()
200  {
201    return "ldap-debugger";
202  }
203
204
205
206  /**
207   * Retrieves the description for this tool.
208   *
209   * @return  The description for this tool.
210   */
211  @Override()
212  public String getToolDescription()
213  {
214    return "Intercept and decode LDAP communication.";
215  }
216
217
218
219  /**
220   * Retrieves the version string for this tool.
221   *
222   * @return  The version string for this tool.
223   */
224  @Override()
225  public String getToolVersion()
226  {
227    return Version.NUMERIC_VERSION_STRING;
228  }
229
230
231
232  /**
233   * Indicates whether this tool should provide support for an interactive mode,
234   * in which the tool offers a mode in which the arguments can be provided in
235   * a text-driven menu rather than requiring them to be given on the command
236   * line.  If interactive mode is supported, it may be invoked using the
237   * "--interactive" argument.  Alternately, if interactive mode is supported
238   * and {@link #defaultsToInteractiveMode()} returns {@code true}, then
239   * interactive mode may be invoked by simply launching the tool without any
240   * arguments.
241   *
242   * @return  {@code true} if this tool supports interactive mode, or
243   *          {@code false} if not.
244   */
245  @Override()
246  public boolean supportsInteractiveMode()
247  {
248    return true;
249  }
250
251
252
253  /**
254   * Indicates whether this tool defaults to launching in interactive mode if
255   * the tool is invoked without any command-line arguments.  This will only be
256   * used if {@link #supportsInteractiveMode()} returns {@code true}.
257   *
258   * @return  {@code true} if this tool defaults to using interactive mode if
259   *          launched without any command-line arguments, or {@code false} if
260   *          not.
261   */
262  @Override()
263  public boolean defaultsToInteractiveMode()
264  {
265    return true;
266  }
267
268
269
270  /**
271   * Indicates whether this tool should default to interactively prompting for
272   * the bind password if a password is required but no argument was provided
273   * to indicate how to get the password.
274   *
275   * @return  {@code true} if this tool should default to interactively
276   *          prompting for the bind password, or {@code false} if not.
277   */
278  @Override()
279  protected boolean defaultToPromptForBindPassword()
280  {
281    return true;
282  }
283
284
285
286  /**
287   * Indicates whether this tool supports the use of a properties file for
288   * specifying default values for arguments that aren't specified on the
289   * command line.
290   *
291   * @return  {@code true} if this tool supports the use of a properties file
292   *          for specifying default values for arguments that aren't specified
293   *          on the command line, or {@code false} if not.
294   */
295  @Override()
296  public boolean supportsPropertiesFile()
297  {
298    return true;
299  }
300
301
302
303  /**
304   * Indicates whether the LDAP-specific arguments should include alternate
305   * versions of all long identifiers that consist of multiple words so that
306   * they are available in both camelCase and dash-separated versions.
307   *
308   * @return  {@code true} if this tool should provide multiple versions of
309   *          long identifiers for LDAP-specific arguments, or {@code false} if
310   *          not.
311   */
312  @Override()
313  protected boolean includeAlternateLongIdentifiers()
314  {
315    return true;
316  }
317
318
319
320  /**
321   * Adds the arguments used by this program that aren't already provided by the
322   * generic {@code LDAPCommandLineTool} framework.
323   *
324   * @param  parser  The argument parser to which the arguments should be added.
325   *
326   * @throws  ArgumentException  If a problem occurs while adding the arguments.
327   */
328  @Override()
329  public void addNonLDAPArguments(final ArgumentParser parser)
330         throws ArgumentException
331  {
332    String description = "The address on which to listen for client " +
333         "connections.  If this is not provided, then it will listen on " +
334         "all interfaces.";
335    listenAddress = new StringArgument('a', "listenAddress", false, 1,
336         "{address}", description);
337    listenAddress.addLongIdentifier("listen-address", true);
338    parser.addArgument(listenAddress);
339
340
341    description = "The port on which to listen for client connections.  If " +
342         "no value is provided, then a free port will be automatically " +
343         "selected.";
344    listenPort = new IntegerArgument('L', "listenPort", true, 1, "{port}",
345         description, 0, 65_535, 0);
346    listenPort.addLongIdentifier("listen-port", true);
347    parser.addArgument(listenPort);
348
349
350    description = "Use SSL when accepting client connections.  This is " +
351         "independent of the '--useSSL' option, which applies only to " +
352         "communication between the LDAP debugger and the backend server.";
353    listenUsingSSL = new BooleanArgument('S', "listenUsingSSL", 1,
354         description);
355    listenUsingSSL.addLongIdentifier("listen-using-ssl", true);
356    parser.addArgument(listenUsingSSL);
357
358
359    description = "The path to the output file to be written.  If no value " +
360         "is provided, then the output will be written to standard output.";
361    outputFile = new FileArgument('f', "outputFile", false, 1, "{path}",
362         description, false, true, true, false);
363    outputFile.addLongIdentifier("output-file", true);
364    parser.addArgument(outputFile);
365
366
367    description = "The path to the a code log file to be written.  If a " +
368         "value is provided, then the tool will generate sample code that " +
369         "corresponds to the requests received from clients.  If no value is " +
370         "provided, then no code log will be generated.";
371    codeLogFile = new FileArgument('c', "codeLogFile", false, 1, "{path}",
372         description, false, true, true, false);
373    codeLogFile.addLongIdentifier("code-log-file", true);
374    parser.addArgument(codeLogFile);
375
376
377    // If --listenUsingSSL is provided, then the --keyStorePath argument must
378    // also be provided.
379    final Argument keyStorePathArgument =
380         parser.getNamedArgument("keyStorePath");
381    parser.addDependentArgumentSet(listenUsingSSL, keyStorePathArgument);
382
383
384    // If the listenUsingSSL argument is provided, then one of the
385    // --keyStorePassword, --keyStorePasswordFile, or
386    // --promptForKeyStorePassword arguments.
387    final Argument keyStorePasswordArgument =
388         parser.getNamedArgument("keyStorePassword");
389    final Argument keyStorePasswordFileArgument =
390         parser.getNamedArgument("keyStorePasswordFile");
391    final Argument promptForKeyStorePasswordArgument =
392         parser.getNamedArgument("promptForKeyStorePassword");
393    parser.addDependentArgumentSet(listenUsingSSL, keyStorePasswordArgument,
394         keyStorePasswordFileArgument, promptForKeyStorePasswordArgument);
395  }
396
397
398
399  /**
400   * Performs the actual processing for this tool.  In this case, it gets a
401   * connection to the directory server and uses it to perform the requested
402   * search.
403   *
404   * @return  The result code for the processing that was performed.
405   */
406  @Override()
407  public ResultCode doToolProcessing()
408  {
409    // Create the proxy request handler that will be used to forward requests to
410    // a remote directory.
411    final ProxyRequestHandler proxyHandler;
412    try
413    {
414      proxyHandler = new ProxyRequestHandler(createServerSet());
415    }
416    catch (final LDAPException le)
417    {
418      err("Unable to prepare to connect to the target server:  ",
419           le.getMessage());
420      return le.getResultCode();
421    }
422
423
424    // Create the log handler to use for the output.
425    final Handler logHandler;
426    if (outputFile.isPresent())
427    {
428      try
429      {
430        logHandler = new FileHandler(outputFile.getValue().getAbsolutePath());
431      }
432      catch (final IOException ioe)
433      {
434        err("Unable to open the output file for writing:  ",
435             StaticUtils.getExceptionMessage(ioe));
436        return ResultCode.LOCAL_ERROR;
437      }
438    }
439    else
440    {
441      logHandler = new ConsoleHandler();
442    }
443    logHandler.setLevel(Level.INFO);
444    logHandler.setFormatter(new MinimalLogFormatter(
445         MinimalLogFormatter.DEFAULT_TIMESTAMP_FORMAT, false, false, true));
446
447
448    // Create the debugger request handler that will be used to write the
449    // debug output.
450    LDAPListenerRequestHandler requestHandler =
451         new LDAPDebuggerRequestHandler(logHandler, proxyHandler);
452
453
454    // If a code log file was specified, then create the appropriate request
455    // handler to accomplish that.
456    if (codeLogFile.isPresent())
457    {
458      try
459      {
460        requestHandler = new ToCodeRequestHandler(codeLogFile.getValue(), true,
461             requestHandler);
462      }
463      catch (final Exception e)
464      {
465        err("Unable to open code log file '",
466             codeLogFile.getValue().getAbsolutePath(), "' for writing:  ",
467             StaticUtils.getExceptionMessage(e));
468        return ResultCode.LOCAL_ERROR;
469      }
470    }
471
472
473    // Create and start the LDAP listener.
474    final LDAPListenerConfig config =
475         new LDAPListenerConfig(listenPort.getValue(), requestHandler);
476    if (listenAddress.isPresent())
477    {
478      try
479      {
480        config.setListenAddress(
481             InetAddress.getByName(listenAddress.getValue()));
482      }
483      catch (final Exception e)
484      {
485        err("Unable to resolve '", listenAddress.getValue(),
486            "' as a valid address:  ", StaticUtils.getExceptionMessage(e));
487        return ResultCode.PARAM_ERROR;
488      }
489    }
490
491    if (listenUsingSSL.isPresent())
492    {
493      try
494      {
495        config.setServerSocketFactory(
496             createSSLUtil(true).createSSLServerSocketFactory());
497      }
498      catch (final Exception e)
499      {
500        err("Unable to create a server socket factory to accept SSL-based " +
501             "client connections:  ", StaticUtils.getExceptionMessage(e));
502        return ResultCode.LOCAL_ERROR;
503      }
504    }
505
506    listener = new LDAPListener(config);
507
508    try
509    {
510      listener.startListening();
511    }
512    catch (final Exception e)
513    {
514      err("Unable to start listening for client connections:  ",
515          StaticUtils.getExceptionMessage(e));
516      return ResultCode.LOCAL_ERROR;
517    }
518
519
520    // Display a message with information about the port on which it is
521    // listening for connections.
522    int port = listener.getListenPort();
523    while (port <= 0)
524    {
525      try
526      {
527        Thread.sleep(1L);
528      }
529      catch (final Exception e)
530      {
531        Debug.debugException(e);
532
533        if (e instanceof InterruptedException)
534        {
535          Thread.currentThread().interrupt();
536        }
537      }
538
539      port = listener.getListenPort();
540    }
541
542    if (listenUsingSSL.isPresent())
543    {
544      out("Listening for SSL-based LDAP client connections on port ", port);
545    }
546    else
547    {
548      out("Listening for LDAP client connections on port ", port);
549    }
550
551    // Note that at this point, the listener will continue running in a
552    // separate thread, so we can return from this thread without exiting the
553    // program.  However, we'll want to register a shutdown hook so that we can
554    // close the logger.
555    shutdownListener = new LDAPDebuggerShutdownListener(listener, logHandler);
556    Runtime.getRuntime().addShutdownHook(shutdownListener);
557
558    return ResultCode.SUCCESS;
559  }
560
561
562
563  /**
564   * {@inheritDoc}
565   */
566  @Override()
567  public LinkedHashMap<String[],String> getExampleUsages()
568  {
569    final LinkedHashMap<String[],String> examples = new LinkedHashMap<>(1);
570
571    final String[] args =
572    {
573      "--hostname", "server.example.com",
574      "--port", "389",
575      "--listenPort", "1389",
576      "--outputFile", "/tmp/ldap-debugger.log"
577    };
578    final String description =
579         "Listen for client connections on port 1389 on all interfaces and " +
580         "forward any traffic received to server.example.com:389.  The " +
581         "decoded LDAP communication will be written to the " +
582         "/tmp/ldap-debugger.log log file.";
583    examples.put(args, description);
584
585    return examples;
586  }
587
588
589
590  /**
591   * Retrieves the LDAP listener used to decode the communication.
592   *
593   * @return  The LDAP listener used to decode the communication, or
594   *          {@code null} if the tool is not running.
595   */
596  public LDAPListener getListener()
597  {
598    return listener;
599  }
600
601
602
603  /**
604   * Indicates that the associated listener should shut down.
605   */
606  public void shutDown()
607  {
608    Runtime.getRuntime().removeShutdownHook(shutdownListener);
609    shutdownListener.run();
610  }
611}