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