001 /*
002 * Copyright 2010-2013 UnboundID Corp.
003 * All Rights Reserved.
004 */
005 /*
006 * Copyright (C) 2010-2013 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 }