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