001/*
002 * Copyright 2010-2019 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2010-2019 Ping Identity Corporation
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.StaticUtils;
044import com.unboundid.util.ThreadSafety;
045import com.unboundid.util.ThreadSafetyLevel;
046
047import static com.unboundid.ldap.listener.ListenerMessages.*;
048
049
050
051/**
052 * This class provides a framework that may be used to accept connections from
053 * LDAP clients and ensure that any requests received on those connections will
054 * be processed appropriately.  It can be used to easily allow applications to
055 * accept LDAP requests, to create a simple proxy that can intercept and
056 * examine LDAP requests and responses passing between a client and server, or
057 * helping to test LDAP clients.
058 * <BR><BR>
059 * <H2>Example</H2>
060 * The following example demonstrates the process that can be used to create an
061 * LDAP listener that will listen for LDAP requests on a randomly-selected port
062 * and immediately respond to them with a "success" result:
063 * <PRE>
064 * // Create a canned response request handler that will always return a
065 * // "SUCCESS" result in response to any request.
066 * CannedResponseRequestHandler requestHandler =
067 *    new CannedResponseRequestHandler(ResultCode.SUCCESS, null, null,
068 *         null);
069 *
070 * // A listen port of zero indicates that the listener should
071 * // automatically pick a free port on the system.
072 * int listenPort = 0;
073 *
074 * // Create and start an LDAP listener to accept requests and blindly
075 * // return success results.
076 * LDAPListenerConfig listenerConfig = new LDAPListenerConfig(listenPort,
077 *      requestHandler);
078 * LDAPListener listener = new LDAPListener(listenerConfig);
079 * listener.startListening();
080 *
081 * // Establish a connection to the listener and verify that a search
082 * // request will get a success result.
083 * LDAPConnection connection = new LDAPConnection("localhost",
084 *      listener.getListenPort());
085 * SearchResult searchResult = connection.search("dc=example,dc=com",
086 *      SearchScope.BASE, Filter.createPresenceFilter("objectClass"));
087 * LDAPTestUtils.assertResultCodeEquals(searchResult,
088 *      ResultCode.SUCCESS);
089 *
090 * // Close the connection and stop the listener.
091 * connection.close();
092 * listener.shutDown(true);
093 * </PRE>
094 */
095@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE)
096public final class LDAPListener
097       extends Thread
098{
099  // Indicates whether a request has been received to stop running.
100  private final AtomicBoolean stopRequested;
101
102  // The connection ID value that should be assigned to the next connection that
103  // is established.
104  private final AtomicLong nextConnectionID;
105
106  // The server socket that is being used to accept connections.
107  private final AtomicReference<ServerSocket> serverSocket;
108
109  // The thread that is currently listening for new client connections.
110  private final AtomicReference<Thread> thread;
111
112  // A map of all established connections.
113  private final ConcurrentHashMap<Long,LDAPListenerClientConnection>
114       establishedConnections;
115
116  // The latch used to wait for the listener to have started.
117  private final CountDownLatch startLatch;
118
119  // The configuration to use for this listener.
120  private final LDAPListenerConfig config;
121
122
123
124  /**
125   * Creates a new {@code LDAPListener} object with the provided configuration.
126   * The {@link #startListening} method must be called after creating the object
127   * to actually start listening for requests.
128   *
129   * @param  config  The configuration to use for this listener.
130   */
131  public LDAPListener(final LDAPListenerConfig config)
132  {
133    this.config = config.duplicate();
134
135    stopRequested = new AtomicBoolean(false);
136    nextConnectionID = new AtomicLong(0L);
137    serverSocket = new AtomicReference<>(null);
138    thread = new AtomicReference<>(null);
139    startLatch = new CountDownLatch(1);
140    establishedConnections =
141         new ConcurrentHashMap<>(StaticUtils.computeMapCapacity(20));
142    setName("LDAP Listener Thread (not listening");
143  }
144
145
146
147  /**
148   * Creates the server socket for this listener and starts listening for client
149   * connections.  This method will return after the listener has stated.
150   *
151   * @throws  IOException  If a problem occurs while creating the server socket.
152   */
153  public void startListening()
154         throws IOException
155  {
156    final ServerSocketFactory f = config.getServerSocketFactory();
157    final InetAddress a = config.getListenAddress();
158    final int p = config.getListenPort();
159    if (a == null)
160    {
161      serverSocket.set(f.createServerSocket(config.getListenPort(), 128));
162    }
163    else
164    {
165      serverSocket.set(f.createServerSocket(config.getListenPort(), 128, a));
166    }
167
168    final int receiveBufferSize = config.getReceiveBufferSize();
169    if (receiveBufferSize > 0)
170    {
171      serverSocket.get().setReceiveBufferSize(receiveBufferSize);
172    }
173
174    setName("LDAP Listener Thread (listening on port " +
175         serverSocket.get().getLocalPort() + ')');
176
177    start();
178
179    try
180    {
181      startLatch.await();
182    }
183    catch (final Exception e)
184    {
185      Debug.debugException(e);
186    }
187  }
188
189
190
191  /**
192   * Operates in a loop, waiting for client connections to arrive and ensuring
193   * that they are handled properly.  This method is for internal use only and
194   * must not be called by third-party code.
195   */
196  @InternalUseOnly()
197  @Override()
198  public void run()
199  {
200    thread.set(Thread.currentThread());
201    final LDAPListenerExceptionHandler exceptionHandler =
202         config.getExceptionHandler();
203
204    try
205    {
206      startLatch.countDown();
207      while (! stopRequested.get())
208      {
209        final Socket s;
210        try
211        {
212          s = serverSocket.get().accept();
213        }
214        catch (final Exception e)
215        {
216          Debug.debugException(e);
217
218          if ((e instanceof SocketException) &&
219              serverSocket.get().isClosed())
220          {
221            return;
222          }
223
224          if (exceptionHandler != null)
225          {
226            exceptionHandler.connectionCreationFailure(null, e);
227          }
228
229          continue;
230        }
231
232        final LDAPListenerClientConnection c;
233        try
234        {
235          c = new LDAPListenerClientConnection(this, s,
236               config.getRequestHandler(), config.getExceptionHandler());
237        }
238        catch (final LDAPException le)
239        {
240          Debug.debugException(le);
241
242          if (exceptionHandler != null)
243          {
244            exceptionHandler.connectionCreationFailure(s, le);
245          }
246
247          continue;
248        }
249
250        final int maxConnections = config.getMaxConnections();
251        if ((maxConnections > 0) &&
252            (establishedConnections.size() >= maxConnections))
253        {
254          c.close(new LDAPException(ResultCode.BUSY,
255               ERR_LDAP_LISTENER_MAX_CONNECTIONS_ESTABLISHED.get(
256                    maxConnections)));
257          continue;
258        }
259
260        establishedConnections.put(c.getConnectionID(), c);
261        c.start();
262      }
263    }
264    finally
265    {
266      final ServerSocket s = serverSocket.getAndSet(null);
267      if (s != null)
268      {
269        try
270        {
271          s.close();
272        }
273        catch (final Exception e)
274        {
275          Debug.debugException(e);
276        }
277      }
278
279      serverSocket.set(null);
280      thread.set(null);
281    }
282  }
283
284
285
286  /**
287   * Closes all connections that are currently established to this listener.
288   * This has no effect on the ability to accept new connections.
289   *
290   * @param  sendNoticeOfDisconnection  Indicates whether to send the client a
291   *                                    notice of disconnection unsolicited
292   *                                    notification before closing the
293   *                                    connection.
294   */
295  public void closeAllConnections(final boolean sendNoticeOfDisconnection)
296  {
297    final NoticeOfDisconnectionExtendedResult noticeOfDisconnection =
298         new NoticeOfDisconnectionExtendedResult(ResultCode.OTHER, null);
299
300    final ArrayList<LDAPListenerClientConnection> connList =
301         new ArrayList<>(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}