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