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.listener; 022 023 024 025import java.io.IOException; 026import java.net.InetAddress; 027import java.net.ServerSocket; 028import java.net.Socket; 029import java.net.SocketException; 030import java.util.ArrayList; 031import java.util.concurrent.ConcurrentHashMap; 032import java.util.concurrent.CountDownLatch; 033import java.util.concurrent.atomic.AtomicBoolean; 034import java.util.concurrent.atomic.AtomicLong; 035import java.util.concurrent.atomic.AtomicReference; 036import javax.net.ServerSocketFactory; 037 038import com.unboundid.ldap.sdk.LDAPException; 039import com.unboundid.util.Debug; 040import com.unboundid.util.InternalUseOnly; 041import com.unboundid.util.ThreadSafety; 042import com.unboundid.util.ThreadSafetyLevel; 043 044 045 046/** 047 * This class provides a framework that may be used to accept connections from 048 * LDAP clients and ensure that any requests received on those connections will 049 * be processed appropriately. It can be used to easily allow applications to 050 * accept LDAP requests, to create a simple proxy that can intercept and 051 * examine LDAP requests and responses passing between a client and server, or 052 * helping to test LDAP clients. 053 * <BR><BR> 054 * <H2>Example</H2> 055 * The following example demonstrates the process that can be used to create an 056 * LDAP listener that will listen for LDAP requests on a randomly-selected port 057 * and immediately respond to them with a "success" result: 058 * <PRE> 059 * // Create a canned response request handler that will always return a 060 * // "SUCCESS" result in response to any request. 061 * CannedResponseRequestHandler requestHandler = 062 * new CannedResponseRequestHandler(ResultCode.SUCCESS, null, null, 063 * null); 064 * 065 * // A listen port of zero indicates that the listener should 066 * // automatically pick a free port on the system. 067 * int listenPort = 0; 068 * 069 * // Create and start an LDAP listener to accept requests and blindly 070 * // return success results. 071 * LDAPListenerConfig listenerConfig = new LDAPListenerConfig(listenPort, 072 * requestHandler); 073 * LDAPListener listener = new LDAPListener(listenerConfig); 074 * listener.startListening(); 075 * 076 * // Establish a connection to the listener and verify that a search 077 * // request will get a success result. 078 * LDAPConnection connection = new LDAPConnection("localhost", 079 * listener.getListenPort()); 080 * SearchResult searchResult = connection.search("dc=example,dc=com", 081 * SearchScope.BASE, Filter.createPresenceFilter("objectClass")); 082 * LDAPTestUtils.assertResultCodeEquals(searchResult, 083 * ResultCode.SUCCESS); 084 * 085 * // Close the connection and stop the listener. 086 * connection.close(); 087 * listener.shutDown(true); 088 * </PRE> 089 */ 090@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 091public final class LDAPListener 092 extends Thread 093{ 094 // Indicates whether a request has been received to stop running. 095 private final AtomicBoolean stopRequested; 096 097 // The connection ID value that should be assigned to the next connection that 098 // is established. 099 private final AtomicLong nextConnectionID; 100 101 // The server socket that is being used to accept connections. 102 private final AtomicReference<ServerSocket> serverSocket; 103 104 // The thread that is currently listening for new client connections. 105 private final AtomicReference<Thread> thread; 106 107 // A map of all established connections. 108 private final ConcurrentHashMap<Long,LDAPListenerClientConnection> 109 establishedConnections; 110 111 // The latch used to wait for the listener to have started. 112 private final CountDownLatch startLatch; 113 114 // The configuration to use for this listener. 115 private final LDAPListenerConfig config; 116 117 118 119 /** 120 * Creates a new {@code LDAPListener} object with the provided configuration. 121 * The {@link #start} method must be called after creating the object to 122 * actually start listening for requests. 123 * 124 * @param config The configuration to use for this listener. 125 */ 126 public LDAPListener(final LDAPListenerConfig config) 127 { 128 this.config = config.duplicate(); 129 130 stopRequested = new AtomicBoolean(false); 131 nextConnectionID = new AtomicLong(0L); 132 serverSocket = new AtomicReference<ServerSocket>(null); 133 thread = new AtomicReference<Thread>(null); 134 startLatch = new CountDownLatch(1); 135 establishedConnections = 136 new ConcurrentHashMap<Long,LDAPListenerClientConnection>(); 137 setName("LDAP Listener Thread (not listening"); 138 } 139 140 141 142 /** 143 * Creates the server socket for this listener and starts listening for client 144 * connections. This method will return after the listener has stated. 145 * 146 * @throws IOException If a problem occurs while creating the server socket. 147 */ 148 public void startListening() 149 throws IOException 150 { 151 final ServerSocketFactory f = config.getServerSocketFactory(); 152 final InetAddress a = config.getListenAddress(); 153 final int p = config.getListenPort(); 154 if (a == null) 155 { 156 serverSocket.set(f.createServerSocket(config.getListenPort(), 128)); 157 } 158 else 159 { 160 serverSocket.set(f.createServerSocket(config.getListenPort(), 128, a)); 161 } 162 163 final int receiveBufferSize = config.getReceiveBufferSize(); 164 if (receiveBufferSize > 0) 165 { 166 serverSocket.get().setReceiveBufferSize(receiveBufferSize); 167 } 168 169 setName("LDAP Listener Thread (listening on port " + 170 serverSocket.get().getLocalPort() + ')'); 171 172 start(); 173 174 try 175 { 176 startLatch.await(); 177 } 178 catch (final Exception e) 179 { 180 Debug.debugException(e); 181 } 182 } 183 184 185 186 /** 187 * Operates in a loop, waiting for client connections to arrive and ensuring 188 * that they are handled properly. This method is for internal use only and 189 * must not be called by third-party code. 190 */ 191 @InternalUseOnly() 192 @Override() 193 public void run() 194 { 195 thread.set(Thread.currentThread()); 196 final LDAPListenerExceptionHandler exceptionHandler = 197 config.getExceptionHandler(); 198 199 try 200 { 201 startLatch.countDown(); 202 while (! stopRequested.get()) 203 { 204 final Socket s; 205 try 206 { 207 s = serverSocket.get().accept(); 208 } 209 catch (final Exception e) 210 { 211 Debug.debugException(e); 212 213 if ((e instanceof SocketException) && 214 serverSocket.get().isClosed()) 215 { 216 return; 217 } 218 219 if (exceptionHandler != null) 220 { 221 exceptionHandler.connectionCreationFailure(null, e); 222 } 223 224 continue; 225 } 226 227 228 final LDAPListenerClientConnection c; 229 try 230 { 231 c = new LDAPListenerClientConnection(this, s, 232 config.getRequestHandler(), config.getExceptionHandler()); 233 } 234 catch (final LDAPException le) 235 { 236 Debug.debugException(le); 237 238 if (exceptionHandler != null) 239 { 240 exceptionHandler.connectionCreationFailure(s, le); 241 } 242 243 continue; 244 } 245 246 establishedConnections.put(c.getConnectionID(), c); 247 c.start(); 248 } 249 } 250 finally 251 { 252 final ServerSocket s = serverSocket.getAndSet(null); 253 if (s != null) 254 { 255 try 256 { 257 s.close(); 258 } 259 catch (final Exception e) 260 { 261 Debug.debugException(e); 262 } 263 } 264 265 serverSocket.set(null); 266 thread.set(null); 267 } 268 } 269 270 271 272 /** 273 * Indicates that this listener should stop accepting connections. It may 274 * optionally also terminate any existing connections that are already 275 * established. 276 * 277 * @param closeExisting Indicates whether to close existing connections that 278 * may already be established. 279 */ 280 public void shutDown(final boolean closeExisting) 281 { 282 stopRequested.set(true); 283 284 final ServerSocket s = serverSocket.get(); 285 if (s != null) 286 { 287 try 288 { 289 s.close(); 290 } 291 catch (final Exception e) 292 { 293 Debug.debugException(e); 294 } 295 } 296 297 final Thread t = thread.get(); 298 if (t != null) 299 { 300 while (t.isAlive()) 301 { 302 try 303 { 304 t.join(100L); 305 } 306 catch (final Exception e) 307 { 308 Debug.debugException(e); 309 } 310 311 if (t.isAlive()) 312 { 313 314 try 315 { 316 t.interrupt(); 317 } 318 catch (final Exception e) 319 { 320 Debug.debugException(e); 321 } 322 } 323 } 324 } 325 326 if (closeExisting) 327 { 328 final ArrayList<LDAPListenerClientConnection> connList = 329 new ArrayList<LDAPListenerClientConnection>( 330 establishedConnections.values()); 331 for (final LDAPListenerClientConnection c : connList) 332 { 333 try 334 { 335 c.close(); 336 } 337 catch (final Exception e) 338 { 339 Debug.debugException(e); 340 } 341 } 342 } 343 } 344 345 346 347 /** 348 * Retrieves the address on which this listener is accepting client 349 * connections. Note that if no explicit listen address was configured, then 350 * the address returned may not be usable by clients. In the event that the 351 * {@code InetAddress.isAnyLocalAddress} method returns {@code true}, then 352 * clients should generally use {@code localhost} to attempt to establish 353 * connections. 354 * 355 * @return The address on which this listener is accepting client 356 * connections, or {@code null} if it is not currently listening for 357 * client connections. 358 */ 359 public InetAddress getListenAddress() 360 { 361 final ServerSocket s = serverSocket.get(); 362 if (s == null) 363 { 364 return null; 365 } 366 else 367 { 368 return s.getInetAddress(); 369 } 370 } 371 372 373 374 /** 375 * Retrieves the port on which this listener is accepting client connections. 376 * 377 * @return The port on which this listener is accepting client connections, 378 * or -1 if it is not currently listening for client connections. 379 */ 380 public int getListenPort() 381 { 382 final ServerSocket s = serverSocket.get(); 383 if (s == null) 384 { 385 return -1; 386 } 387 else 388 { 389 return s.getLocalPort(); 390 } 391 } 392 393 394 395 /** 396 * Retrieves the configuration in use for this listener. It must not be 397 * altered in any way. 398 * 399 * @return The configuration in use for this listener. 400 */ 401 LDAPListenerConfig getConfig() 402 { 403 return config; 404 } 405 406 407 408 /** 409 * Retrieves the connection ID that should be used for the next connection 410 * accepted by this listener. 411 * 412 * @return The connection ID that should be used for the next connection 413 * accepted by this listener. 414 */ 415 long nextConnectionID() 416 { 417 return nextConnectionID.getAndIncrement(); 418 } 419 420 421 422 /** 423 * Indicates that the provided client connection has been closed and is no 424 * longer listening for client connections. 425 * 426 * @param connection The connection that has been closed. 427 */ 428 void connectionClosed(final LDAPListenerClientConnection connection) 429 { 430 establishedConnections.remove(connection.getConnectionID()); 431 } 432}