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}