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