001/* 002 * Copyright 2010-2019 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2010-2019 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.StaticUtils; 044import com.unboundid.util.ThreadSafety; 045import com.unboundid.util.ThreadSafetyLevel; 046 047import static com.unboundid.ldap.listener.ListenerMessages.*; 048 049 050 051/** 052 * This class provides a framework that may be used to accept connections from 053 * LDAP clients and ensure that any requests received on those connections will 054 * be processed appropriately. It can be used to easily allow applications to 055 * accept LDAP requests, to create a simple proxy that can intercept and 056 * examine LDAP requests and responses passing between a client and server, or 057 * helping to test LDAP clients. 058 * <BR><BR> 059 * <H2>Example</H2> 060 * The following example demonstrates the process that can be used to create an 061 * LDAP listener that will listen for LDAP requests on a randomly-selected port 062 * and immediately respond to them with a "success" result: 063 * <PRE> 064 * // Create a canned response request handler that will always return a 065 * // "SUCCESS" result in response to any request. 066 * CannedResponseRequestHandler requestHandler = 067 * new CannedResponseRequestHandler(ResultCode.SUCCESS, null, null, 068 * null); 069 * 070 * // A listen port of zero indicates that the listener should 071 * // automatically pick a free port on the system. 072 * int listenPort = 0; 073 * 074 * // Create and start an LDAP listener to accept requests and blindly 075 * // return success results. 076 * LDAPListenerConfig listenerConfig = new LDAPListenerConfig(listenPort, 077 * requestHandler); 078 * LDAPListener listener = new LDAPListener(listenerConfig); 079 * listener.startListening(); 080 * 081 * // Establish a connection to the listener and verify that a search 082 * // request will get a success result. 083 * LDAPConnection connection = new LDAPConnection("localhost", 084 * listener.getListenPort()); 085 * SearchResult searchResult = connection.search("dc=example,dc=com", 086 * SearchScope.BASE, Filter.createPresenceFilter("objectClass")); 087 * LDAPTestUtils.assertResultCodeEquals(searchResult, 088 * ResultCode.SUCCESS); 089 * 090 * // Close the connection and stop the listener. 091 * connection.close(); 092 * listener.shutDown(true); 093 * </PRE> 094 */ 095@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 096public final class LDAPListener 097 extends Thread 098{ 099 // Indicates whether a request has been received to stop running. 100 private final AtomicBoolean stopRequested; 101 102 // The connection ID value that should be assigned to the next connection that 103 // is established. 104 private final AtomicLong nextConnectionID; 105 106 // The server socket that is being used to accept connections. 107 private final AtomicReference<ServerSocket> serverSocket; 108 109 // The thread that is currently listening for new client connections. 110 private final AtomicReference<Thread> thread; 111 112 // A map of all established connections. 113 private final ConcurrentHashMap<Long,LDAPListenerClientConnection> 114 establishedConnections; 115 116 // The latch used to wait for the listener to have started. 117 private final CountDownLatch startLatch; 118 119 // The configuration to use for this listener. 120 private final LDAPListenerConfig config; 121 122 123 124 /** 125 * Creates a new {@code LDAPListener} object with the provided configuration. 126 * The {@link #startListening} method must be called after creating the object 127 * to actually start listening for requests. 128 * 129 * @param config The configuration to use for this listener. 130 */ 131 public LDAPListener(final LDAPListenerConfig config) 132 { 133 this.config = config.duplicate(); 134 135 stopRequested = new AtomicBoolean(false); 136 nextConnectionID = new AtomicLong(0L); 137 serverSocket = new AtomicReference<>(null); 138 thread = new AtomicReference<>(null); 139 startLatch = new CountDownLatch(1); 140 establishedConnections = 141 new ConcurrentHashMap<>(StaticUtils.computeMapCapacity(20)); 142 setName("LDAP Listener Thread (not listening"); 143 } 144 145 146 147 /** 148 * Creates the server socket for this listener and starts listening for client 149 * connections. This method will return after the listener has stated. 150 * 151 * @throws IOException If a problem occurs while creating the server socket. 152 */ 153 public void startListening() 154 throws IOException 155 { 156 final ServerSocketFactory f = config.getServerSocketFactory(); 157 final InetAddress a = config.getListenAddress(); 158 final int p = config.getListenPort(); 159 if (a == null) 160 { 161 serverSocket.set(f.createServerSocket(config.getListenPort(), 128)); 162 } 163 else 164 { 165 serverSocket.set(f.createServerSocket(config.getListenPort(), 128, a)); 166 } 167 168 final int receiveBufferSize = config.getReceiveBufferSize(); 169 if (receiveBufferSize > 0) 170 { 171 serverSocket.get().setReceiveBufferSize(receiveBufferSize); 172 } 173 174 setName("LDAP Listener Thread (listening on port " + 175 serverSocket.get().getLocalPort() + ')'); 176 177 start(); 178 179 try 180 { 181 startLatch.await(); 182 } 183 catch (final Exception e) 184 { 185 Debug.debugException(e); 186 } 187 } 188 189 190 191 /** 192 * Operates in a loop, waiting for client connections to arrive and ensuring 193 * that they are handled properly. This method is for internal use only and 194 * must not be called by third-party code. 195 */ 196 @InternalUseOnly() 197 @Override() 198 public void run() 199 { 200 thread.set(Thread.currentThread()); 201 final LDAPListenerExceptionHandler exceptionHandler = 202 config.getExceptionHandler(); 203 204 try 205 { 206 startLatch.countDown(); 207 while (! stopRequested.get()) 208 { 209 final Socket s; 210 try 211 { 212 s = serverSocket.get().accept(); 213 } 214 catch (final Exception e) 215 { 216 Debug.debugException(e); 217 218 if ((e instanceof SocketException) && 219 serverSocket.get().isClosed()) 220 { 221 return; 222 } 223 224 if (exceptionHandler != null) 225 { 226 exceptionHandler.connectionCreationFailure(null, e); 227 } 228 229 continue; 230 } 231 232 final LDAPListenerClientConnection c; 233 try 234 { 235 c = new LDAPListenerClientConnection(this, s, 236 config.getRequestHandler(), config.getExceptionHandler()); 237 } 238 catch (final LDAPException le) 239 { 240 Debug.debugException(le); 241 242 if (exceptionHandler != null) 243 { 244 exceptionHandler.connectionCreationFailure(s, le); 245 } 246 247 continue; 248 } 249 250 final int maxConnections = config.getMaxConnections(); 251 if ((maxConnections > 0) && 252 (establishedConnections.size() >= maxConnections)) 253 { 254 c.close(new LDAPException(ResultCode.BUSY, 255 ERR_LDAP_LISTENER_MAX_CONNECTIONS_ESTABLISHED.get( 256 maxConnections))); 257 continue; 258 } 259 260 establishedConnections.put(c.getConnectionID(), c); 261 c.start(); 262 } 263 } 264 finally 265 { 266 final ServerSocket s = serverSocket.getAndSet(null); 267 if (s != null) 268 { 269 try 270 { 271 s.close(); 272 } 273 catch (final Exception e) 274 { 275 Debug.debugException(e); 276 } 277 } 278 279 serverSocket.set(null); 280 thread.set(null); 281 } 282 } 283 284 285 286 /** 287 * Closes all connections that are currently established to this listener. 288 * This has no effect on the ability to accept new connections. 289 * 290 * @param sendNoticeOfDisconnection Indicates whether to send the client a 291 * notice of disconnection unsolicited 292 * notification before closing the 293 * connection. 294 */ 295 public void closeAllConnections(final boolean sendNoticeOfDisconnection) 296 { 297 final NoticeOfDisconnectionExtendedResult noticeOfDisconnection = 298 new NoticeOfDisconnectionExtendedResult(ResultCode.OTHER, null); 299 300 final ArrayList<LDAPListenerClientConnection> connList = 301 new ArrayList<>(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}