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}