001    /*
002     * Copyright 2010-2015 UnboundID Corp.
003     * All Rights Reserved.
004     */
005    /*
006     * Copyright (C) 2010-2015 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.LDAPListener;
037    import com.unboundid.ldap.listener.LDAPListenerConfig;
038    import com.unboundid.ldap.listener.ProxyRequestHandler;
039    import com.unboundid.ldap.sdk.LDAPException;
040    import com.unboundid.ldap.sdk.ResultCode;
041    import com.unboundid.ldap.sdk.Version;
042    import com.unboundid.util.LDAPCommandLineTool;
043    import com.unboundid.util.MinimalLogFormatter;
044    import com.unboundid.util.StaticUtils;
045    import com.unboundid.util.ThreadSafety;
046    import com.unboundid.util.ThreadSafetyLevel;
047    import com.unboundid.util.args.ArgumentException;
048    import com.unboundid.util.args.ArgumentParser;
049    import com.unboundid.util.args.BooleanArgument;
050    import com.unboundid.util.args.FileArgument;
051    import com.unboundid.util.args.IntegerArgument;
052    import com.unboundid.util.args.StringArgument;
053    
054    
055    
056    /**
057     * This class provides a tool that can be used to create a simple listener that
058     * may be used to intercept and decode LDAP requests before forwarding them to
059     * another Directory Server, and then intercept and decode responses before
060     * returning them to the client.  Some of the APIs demonstrated by this example
061     * include:
062     * <UL>
063     *   <LI>Argument Parsing (from the {@code com.unboundid.util.args}
064     *       package)</LI>
065     *   <LI>LDAP Command-Line Tool (from the {@code com.unboundid.util}
066     *       package)</LI>
067     *   <LI>LDAP Listener API (from the {@code com.unboundid.ldap.listener}
068     *       package)</LI>
069     * </UL>
070     * <BR><BR>
071     * All of the necessary information is provided using
072     * command line arguments.  Supported arguments include those allowed by the
073     * {@link LDAPCommandLineTool} class, as well as the following additional
074     * arguments:
075     * <UL>
076     *   <LI>"-a {address}" or "--listenAddress {address}" -- Specifies the address
077     *       on which to listen for requests from clients.</LI>
078     *   <LI>"-L {port}" or "--listenPort {port}" -- Specifies the port on which to
079     *       listen for requests from clients.</LI>
080     *   <LI>"-S" or "--listenUsingSSL" -- Indicates that the listener should
081     *       accept connections from SSL-based clients rather than those using
082     *       unencrypted LDAP.</LI>
083     *   <LI>"-f {path}" or "--outputFile {path}" -- Specifies the path to the
084     *       output file to be written.  If this is not provided, then the output
085     *       will be written to standard output.</LI>
086     * </UL>
087     */
088    @ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
089    public final class LDAPDebugger
090           extends LDAPCommandLineTool
091           implements Serializable
092    {
093      /**
094       * The serial version UID for this serializable class.
095       */
096      private static final long serialVersionUID = -8942937427428190983L;
097    
098    
099    
100      // The argument used to specify the output file for the decoded content.
101      private BooleanArgument listenUsingSSL;
102    
103      // The argument used to specify the output file for the decoded content.
104      private FileArgument outputFile;
105    
106      // The argument used to specify the port on which to listen for client
107      // connections.
108      private IntegerArgument listenPort;
109    
110      // The shutdown hook that will be used to stop the listener when the JVM
111      // exits.
112      private LDAPDebuggerShutdownListener shutdownListener;
113    
114      // The listener used to intercept and decode the client communication.
115      private LDAPListener listener;
116    
117      // The argument used to specify the address on which to listen for client
118      // connections.
119      private StringArgument listenAddress;
120    
121    
122    
123      /**
124       * Parse the provided command line arguments and make the appropriate set of
125       * changes.
126       *
127       * @param  args  The command line arguments provided to this program.
128       */
129      public static void main(final String[] args)
130      {
131        final ResultCode resultCode = main(args, System.out, System.err);
132        if (resultCode != ResultCode.SUCCESS)
133        {
134          System.exit(resultCode.intValue());
135        }
136      }
137    
138    
139    
140      /**
141       * Parse the provided command line arguments and make the appropriate set of
142       * changes.
143       *
144       * @param  args       The command line arguments provided to this program.
145       * @param  outStream  The output stream to which standard out should be
146       *                    written.  It may be {@code null} if output should be
147       *                    suppressed.
148       * @param  errStream  The output stream to which standard error should be
149       *                    written.  It may be {@code null} if error messages
150       *                    should be suppressed.
151       *
152       * @return  A result code indicating whether the processing was successful.
153       */
154      public static ResultCode main(final String[] args,
155                                    final OutputStream outStream,
156                                    final OutputStream errStream)
157      {
158        final LDAPDebugger ldapDebugger = new LDAPDebugger(outStream, errStream);
159        return ldapDebugger.runTool(args);
160      }
161    
162    
163    
164      /**
165       * Creates a new instance of this tool.
166       *
167       * @param  outStream  The output stream to which standard out should be
168       *                    written.  It may be {@code null} if output should be
169       *                    suppressed.
170       * @param  errStream  The output stream to which standard error should be
171       *                    written.  It may be {@code null} if error messages
172       *                    should be suppressed.
173       */
174      public LDAPDebugger(final OutputStream outStream,
175                          final OutputStream errStream)
176      {
177        super(outStream, errStream);
178      }
179    
180    
181    
182      /**
183       * Retrieves the name for this tool.
184       *
185       * @return  The name for this tool.
186       */
187      @Override()
188      public String getToolName()
189      {
190        return "ldap-debugger";
191      }
192    
193    
194    
195      /**
196       * Retrieves the description for this tool.
197       *
198       * @return  The description for this tool.
199       */
200      @Override()
201      public String getToolDescription()
202      {
203        return "Intercept and decode LDAP communication.";
204      }
205    
206    
207    
208      /**
209       * Retrieves the version string for this tool.
210       *
211       * @return  The version string for this tool.
212       */
213      @Override()
214      public String getToolVersion()
215      {
216        return Version.NUMERIC_VERSION_STRING;
217      }
218    
219    
220    
221      /**
222       * Adds the arguments used by this program that aren't already provided by the
223       * generic {@code LDAPCommandLineTool} framework.
224       *
225       * @param  parser  The argument parser to which the arguments should be added.
226       *
227       * @throws  ArgumentException  If a problem occurs while adding the arguments.
228       */
229      @Override()
230      public void addNonLDAPArguments(final ArgumentParser parser)
231             throws ArgumentException
232      {
233        String description = "The address on which to listen for client " +
234             "connections.  If this is not provided, then it will listen on " +
235             "all interfaces.";
236        listenAddress = new StringArgument('a', "listenAddress", false, 1,
237             "{address}", description);
238        parser.addArgument(listenAddress);
239    
240    
241        description = "The port on which to listen for client connections.  If " +
242             "no value is provided, then a free port will be automatically " +
243             "selected.";
244        listenPort = new IntegerArgument('L', "listenPort", true, 1, "{port}",
245             description, 0, 65535, 0);
246        parser.addArgument(listenPort);
247    
248    
249        description = "Use SSL when accepting client connections.  This is " +
250             "independent of the '--useSSL' option, which applies only to " +
251             "communication between the LDAP debugger and the backend server.";
252        listenUsingSSL = new BooleanArgument('S', "listenUsingSSL", 1,
253             description);
254        parser.addArgument(listenUsingSSL);
255    
256    
257        description = "The path to the output file to be written.  If no value " +
258             "is provided, then the output will be written to standard output.";
259        outputFile = new FileArgument('f', "outputFile", false, 1, "{path}",
260             description, false, true, true, false);
261        parser.addArgument(outputFile);
262      }
263    
264    
265    
266      /**
267       * Performs the actual processing for this tool.  In this case, it gets a
268       * connection to the directory server and uses it to perform the requested
269       * search.
270       *
271       * @return  The result code for the processing that was performed.
272       */
273      @Override()
274      public ResultCode doToolProcessing()
275      {
276        // Create the proxy request handler that will be used to forward requests to
277        // a remote directory.
278        final ProxyRequestHandler proxyHandler;
279        try
280        {
281          proxyHandler = new ProxyRequestHandler(createServerSet());
282        }
283        catch (final LDAPException le)
284        {
285          err("Unable to prepare to connect to the target server:  ",
286               le.getMessage());
287          return le.getResultCode();
288        }
289    
290    
291        // Create the log handler to use for the output.
292        final Handler logHandler;
293        if (outputFile.isPresent())
294        {
295          try
296          {
297            logHandler = new FileHandler(outputFile.getValue().getAbsolutePath());
298          }
299          catch (final IOException ioe)
300          {
301            err("Unable to open the output file for writing:  ",
302                 StaticUtils.getExceptionMessage(ioe));
303            return ResultCode.LOCAL_ERROR;
304          }
305        }
306        else
307        {
308          logHandler = new ConsoleHandler();
309        }
310        logHandler.setLevel(Level.INFO);
311        logHandler.setFormatter(new MinimalLogFormatter(
312             MinimalLogFormatter.DEFAULT_TIMESTAMP_FORMAT, false, false, true));
313    
314    
315        // Create the debugger request handler that will be used to write the
316        // debug output.
317        final LDAPDebuggerRequestHandler debuggingHandler =
318             new LDAPDebuggerRequestHandler(logHandler, proxyHandler);
319    
320    
321        // Create and start the LDAP listener.
322        final LDAPListenerConfig config =
323             new LDAPListenerConfig(listenPort.getValue(), debuggingHandler);
324        if (listenAddress.isPresent())
325        {
326          try
327          {
328            config.setListenAddress(
329                 InetAddress.getByName(listenAddress.getValue()));
330          }
331          catch (final Exception e)
332          {
333            err("Unable to resolve '", listenAddress.getValue(),
334                "' as a valid address:  ", StaticUtils.getExceptionMessage(e));
335            return ResultCode.PARAM_ERROR;
336          }
337        }
338    
339        if (listenUsingSSL.isPresent())
340        {
341          try
342          {
343            config.setServerSocketFactory(
344                 createSSLUtil(true).createSSLServerSocketFactory());
345          }
346          catch (final Exception e)
347          {
348            err("Unable to create a server socket factory to accept SSL-based " +
349                 "client connections:  ", StaticUtils.getExceptionMessage(e));
350            return ResultCode.LOCAL_ERROR;
351          }
352        }
353    
354        listener = new LDAPListener(config);
355    
356        try
357        {
358          listener.startListening();
359        }
360        catch (final Exception e)
361        {
362          err("Unable to start listening for client connections:  ",
363              StaticUtils.getExceptionMessage(e));
364          return ResultCode.LOCAL_ERROR;
365        }
366    
367    
368        // Display a message with information about the port on which it is
369        // listening for connections.
370        int port = listener.getListenPort();
371        while (port <= 0)
372        {
373          try
374          {
375            Thread.sleep(1L);
376          } catch (final Exception e) {}
377    
378          port = listener.getListenPort();
379        }
380    
381        if (listenUsingSSL.isPresent())
382        {
383          out("Listening for SSL-based LDAP client connections on port ", port);
384        }
385        else
386        {
387          out("Listening for LDAP client connections on port ", port);
388        }
389    
390        // Note that at this point, the listener will continue running in a
391        // separate thread, so we can return from this thread without exiting the
392        // program.  However, we'll want to register a shutdown hook so that we can
393        // close the logger.
394        shutdownListener = new LDAPDebuggerShutdownListener(listener, logHandler);
395        Runtime.getRuntime().addShutdownHook(shutdownListener);
396    
397        return ResultCode.SUCCESS;
398      }
399    
400    
401    
402      /**
403       * {@inheritDoc}
404       */
405      @Override()
406      public LinkedHashMap<String[],String> getExampleUsages()
407      {
408        final LinkedHashMap<String[],String> examples =
409             new LinkedHashMap<String[],String>();
410    
411        final String[] args =
412        {
413          "--hostname", "server.example.com",
414          "--port", "389",
415          "--listenPort", "1389",
416          "--outputFile", "/tmp/ldap-debugger.log"
417        };
418        final String description =
419             "Listen for client connections on port 1389 on all interfaces and " +
420             "forward any traffic received to server.example.com:389.  The " +
421             "decoded LDAP communication will be written to the " +
422             "/tmp/ldap-debugger.log log file.";
423        examples.put(args, description);
424    
425        return examples;
426      }
427    
428    
429    
430      /**
431       * Retrieves the LDAP listener used to decode the communication.
432       *
433       * @return  The LDAP listener used to decode the communication, or
434       *          {@code null} if the tool is not running.
435       */
436      public LDAPListener getListener()
437      {
438        return listener;
439      }
440    
441    
442    
443      /**
444       * Indicates that the associated listener should shut down.
445       */
446      public void shutDown()
447      {
448        Runtime.getRuntime().removeShutdownHook(shutdownListener);
449        shutdownListener.run();
450      }
451    }