001/*
002 * Copyright 2010-2014 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2010-2014 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 */
021package com.unboundid.ldap.sdk.examples;
022
023
024
025import java.io.IOException;
026import java.io.OutputStream;
027import java.io.Serializable;
028import java.net.InetAddress;
029import java.util.LinkedHashMap;
030import java.util.logging.ConsoleHandler;
031import java.util.logging.FileHandler;
032import java.util.logging.Handler;
033import java.util.logging.Level;
034
035import com.unboundid.ldap.listener.LDAPDebuggerRequestHandler;
036import com.unboundid.ldap.listener.LDAPListener;
037import com.unboundid.ldap.listener.LDAPListenerConfig;
038import com.unboundid.ldap.listener.ProxyRequestHandler;
039import com.unboundid.ldap.sdk.LDAPException;
040import com.unboundid.ldap.sdk.ResultCode;
041import com.unboundid.ldap.sdk.Version;
042import com.unboundid.util.LDAPCommandLineTool;
043import com.unboundid.util.MinimalLogFormatter;
044import com.unboundid.util.StaticUtils;
045import com.unboundid.util.ThreadSafety;
046import com.unboundid.util.ThreadSafetyLevel;
047import com.unboundid.util.args.ArgumentException;
048import com.unboundid.util.args.ArgumentParser;
049import com.unboundid.util.args.BooleanArgument;
050import com.unboundid.util.args.FileArgument;
051import com.unboundid.util.args.IntegerArgument;
052import 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)
089public 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}