001/*
002 * Copyright 2010-2018 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright (C) 2010-2018 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.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<>(null);
137    thread = new AtomicReference<>(null);
138    startLatch = new CountDownLatch(1);
139    establishedConnections = new ConcurrentHashMap<>(20);
140    setName("LDAP Listener Thread (not listening");
141  }
142
143
144
145  /**
146   * Creates the server socket for this listener and starts listening for client
147   * connections.  This method will return after the listener has stated.
148   *
149   * @throws  IOException  If a problem occurs while creating the server socket.
150   */
151  public void startListening()
152         throws IOException
153  {
154    final ServerSocketFactory f = config.getServerSocketFactory();
155    final InetAddress a = config.getListenAddress();
156    final int p = config.getListenPort();
157    if (a == null)
158    {
159      serverSocket.set(f.createServerSocket(config.getListenPort(), 128));
160    }
161    else
162    {
163      serverSocket.set(f.createServerSocket(config.getListenPort(), 128, a));
164    }
165
166    final int receiveBufferSize = config.getReceiveBufferSize();
167    if (receiveBufferSize > 0)
168    {
169      serverSocket.get().setReceiveBufferSize(receiveBufferSize);
170    }
171
172    setName("LDAP Listener Thread (listening on port " +
173         serverSocket.get().getLocalPort() + ')');
174
175    start();
176
177    try
178    {
179      startLatch.await();
180    }
181    catch (final Exception e)
182    {
183      Debug.debugException(e);
184    }
185  }
186
187
188
189  /**
190   * Operates in a loop, waiting for client connections to arrive and ensuring
191   * that they are handled properly.  This method is for internal use only and
192   * must not be called by third-party code.
193   */
194  @InternalUseOnly()
195  @Override()
196  public void run()
197  {
198    thread.set(Thread.currentThread());
199    final LDAPListenerExceptionHandler exceptionHandler =
200         config.getExceptionHandler();
201
202    try
203    {
204      startLatch.countDown();
205      while (! stopRequested.get())
206      {
207        final Socket s;
208        try
209        {
210          s = serverSocket.get().accept();
211        }
212        catch (final Exception e)
213        {
214          Debug.debugException(e);
215
216          if ((e instanceof SocketException) &&
217              serverSocket.get().isClosed())
218          {
219            return;
220          }
221
222          if (exceptionHandler != null)
223          {
224            exceptionHandler.connectionCreationFailure(null, e);
225          }
226
227          continue;
228        }
229
230        final LDAPListenerClientConnection c;
231        try
232        {
233          c = new LDAPListenerClientConnection(this, s,
234               config.getRequestHandler(), config.getExceptionHandler());
235        }
236        catch (final LDAPException le)
237        {
238          Debug.debugException(le);
239
240          if (exceptionHandler != null)
241          {
242            exceptionHandler.connectionCreationFailure(s, le);
243          }
244
245          continue;
246        }
247
248        final int maxConnections = config.getMaxConnections();
249        if ((maxConnections > 0) &&
250            (establishedConnections.size() >= maxConnections))
251        {
252          c.close(new LDAPException(ResultCode.BUSY,
253               ERR_LDAP_LISTENER_MAX_CONNECTIONS_ESTABLISHED.get(
254                    maxConnections)));
255          continue;
256        }
257
258        establishedConnections.put(c.getConnectionID(), c);
259        c.start();
260      }
261    }
262    finally
263    {
264      final ServerSocket s = serverSocket.getAndSet(null);
265      if (s != null)
266      {
267        try
268        {
269          s.close();
270        }
271        catch (final Exception e)
272        {
273          Debug.debugException(e);
274        }
275      }
276
277      serverSocket.set(null);
278      thread.set(null);
279    }
280  }
281
282
283
284  /**
285   * Closes all connections that are currently established to this listener.
286   * This has no effect on the ability to accept new connections.
287   *
288   * @param  sendNoticeOfDisconnection  Indicates whether to send the client a
289   *                                    notice of disconnection unsolicited
290   *                                    notification before closing the
291   *                                    connection.
292   */
293  public void closeAllConnections(final boolean sendNoticeOfDisconnection)
294  {
295    final NoticeOfDisconnectionExtendedResult noticeOfDisconnection =
296         new NoticeOfDisconnectionExtendedResult(ResultCode.OTHER, null);
297
298    final ArrayList<LDAPListenerClientConnection> connList =
299         new ArrayList<>(establishedConnections.values());
300    for (final LDAPListenerClientConnection c : connList)
301    {
302      if (sendNoticeOfDisconnection)
303      {
304        try
305        {
306          c.sendUnsolicitedNotification(noticeOfDisconnection);
307        }
308        catch (final Exception e)
309        {
310          Debug.debugException(e);
311        }
312      }
313
314      try
315      {
316        c.close();
317      }
318      catch (final Exception e)
319      {
320        Debug.debugException(e);
321      }
322    }
323  }
324
325
326
327  /**
328   * Indicates that this listener should stop accepting connections.  It may
329   * optionally also terminate any existing connections that are already
330   * established.
331   *
332   * @param  closeExisting  Indicates whether to close existing connections that
333   *                        may already be established.
334   */
335  public void shutDown(final boolean closeExisting)
336  {
337    stopRequested.set(true);
338
339    final ServerSocket s = serverSocket.get();
340    if (s != null)
341    {
342      try
343      {
344        s.close();
345      }
346      catch (final Exception e)
347      {
348        Debug.debugException(e);
349      }
350    }
351
352    final Thread t = thread.get();
353    if (t != null)
354    {
355      while (t.isAlive())
356      {
357        try
358        {
359          t.join(100L);
360        }
361        catch (final Exception e)
362        {
363          Debug.debugException(e);
364
365          if (e instanceof InterruptedException)
366          {
367            Thread.currentThread().interrupt();
368          }
369        }
370
371        if (t.isAlive())
372        {
373
374          try
375          {
376            t.interrupt();
377          }
378          catch (final Exception e)
379          {
380            Debug.debugException(e);
381          }
382        }
383      }
384    }
385
386    if (closeExisting)
387    {
388      closeAllConnections(false);
389    }
390  }
391
392
393
394  /**
395   * Retrieves the address on which this listener is accepting client
396   * connections.  Note that if no explicit listen address was configured, then
397   * the address returned may not be usable by clients.  In the event that the
398   * {@code InetAddress.isAnyLocalAddress} method returns {@code true}, then
399   * clients should generally use {@code localhost} to attempt to establish
400   * connections.
401   *
402   * @return  The address on which this listener is accepting client
403   *          connections, or {@code null} if it is not currently listening for
404   *          client connections.
405   */
406  public InetAddress getListenAddress()
407  {
408    final ServerSocket s = serverSocket.get();
409    if (s == null)
410    {
411      return null;
412    }
413    else
414    {
415      return s.getInetAddress();
416    }
417  }
418
419
420
421  /**
422   * Retrieves the port on which this listener is accepting client connections.
423   *
424   * @return  The port on which this listener is accepting client connections,
425   *          or -1 if it is not currently listening for client connections.
426   */
427  public int getListenPort()
428  {
429    final ServerSocket s = serverSocket.get();
430    if (s == null)
431    {
432      return -1;
433    }
434    else
435    {
436      return s.getLocalPort();
437    }
438  }
439
440
441
442  /**
443   * Retrieves the configuration in use for this listener.  It must not be
444   * altered in any way.
445   *
446   * @return  The configuration in use for this listener.
447   */
448  LDAPListenerConfig getConfig()
449  {
450    return config;
451  }
452
453
454
455  /**
456   * Retrieves the connection ID that should be used for the next connection
457   * accepted by this listener.
458   *
459   * @return  The connection ID that should be used for the next connection
460   *          accepted by this listener.
461   */
462  long nextConnectionID()
463  {
464    return nextConnectionID.getAndIncrement();
465  }
466
467
468
469  /**
470   * Indicates that the provided client connection has been closed and is no
471   * longer listening for client connections.
472   *
473   * @param  connection  The connection that has been closed.
474   */
475  void connectionClosed(final LDAPListenerClientConnection connection)
476  {
477    establishedConnections.remove(connection.getConnectionID());
478  }
479}