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