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 */
021 package com.unboundid.ldap.listener;
022
023
024
025 import java.io.IOException;
026 import java.net.InetAddress;
027 import java.net.ServerSocket;
028 import java.net.Socket;
029 import java.net.SocketException;
030 import java.util.ArrayList;
031 import java.util.concurrent.ConcurrentHashMap;
032 import java.util.concurrent.CountDownLatch;
033 import java.util.concurrent.atomic.AtomicBoolean;
034 import java.util.concurrent.atomic.AtomicLong;
035 import java.util.concurrent.atomic.AtomicReference;
036 import javax.net.ServerSocketFactory;
037
038 import com.unboundid.ldap.sdk.LDAPException;
039 import com.unboundid.util.Debug;
040 import com.unboundid.util.InternalUseOnly;
041 import com.unboundid.util.ThreadSafety;
042 import 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)
091 public 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 }