001/*
002 * Copyright 2010-2017 UnboundID Corp.
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2010-2017 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.ldap.sdk.ResultCode;
040import com.unboundid.ldap.sdk.extensions.NoticeOfDisconnectionExtendedResult;
041import com.unboundid.util.Debug;
042import com.unboundid.util.InternalUseOnly;
043import com.unboundid.util.ThreadSafety;
044import com.unboundid.util.ThreadSafetyLevel;
045
046import 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)
095public 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          if (e instanceof InterruptedException)
368          {
369            Thread.currentThread().interrupt();
370          }
371        }
372
373        if (t.isAlive())
374        {
375
376          try
377          {
378            t.interrupt();
379          }
380          catch (final Exception e)
381          {
382            Debug.debugException(e);
383          }
384        }
385      }
386    }
387
388    if (closeExisting)
389    {
390      closeAllConnections(false);
391    }
392  }
393
394
395
396  /**
397   * Retrieves the address on which this listener is accepting client
398   * connections.  Note that if no explicit listen address was configured, then
399   * the address returned may not be usable by clients.  In the event that the
400   * {@code InetAddress.isAnyLocalAddress} method returns {@code true}, then
401   * clients should generally use {@code localhost} to attempt to establish
402   * connections.
403   *
404   * @return  The address on which this listener is accepting client
405   *          connections, or {@code null} if it is not currently listening for
406   *          client connections.
407   */
408  public InetAddress getListenAddress()
409  {
410    final ServerSocket s = serverSocket.get();
411    if (s == null)
412    {
413      return null;
414    }
415    else
416    {
417      return s.getInetAddress();
418    }
419  }
420
421
422
423  /**
424   * Retrieves the port on which this listener is accepting client connections.
425   *
426   * @return  The port on which this listener is accepting client connections,
427   *          or -1 if it is not currently listening for client connections.
428   */
429  public int getListenPort()
430  {
431    final ServerSocket s = serverSocket.get();
432    if (s == null)
433    {
434      return -1;
435    }
436    else
437    {
438      return s.getLocalPort();
439    }
440  }
441
442
443
444  /**
445   * Retrieves the configuration in use for this listener.  It must not be
446   * altered in any way.
447   *
448   * @return  The configuration in use for this listener.
449   */
450  LDAPListenerConfig getConfig()
451  {
452    return config;
453  }
454
455
456
457  /**
458   * Retrieves the connection ID that should be used for the next connection
459   * accepted by this listener.
460   *
461   * @return  The connection ID that should be used for the next connection
462   *          accepted by this listener.
463   */
464  long nextConnectionID()
465  {
466    return nextConnectionID.getAndIncrement();
467  }
468
469
470
471  /**
472   * Indicates that the provided client connection has been closed and is no
473   * longer listening for client connections.
474   *
475   * @param  connection  The connection that has been closed.
476   */
477  void connectionClosed(final LDAPListenerClientConnection connection)
478  {
479    establishedConnections.remove(connection.getConnectionID());
480  }
481}