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