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