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