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