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