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}