001/* 002 * Copyright 2010-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2010-2017 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 */ 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<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 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}