001/* 002 * Copyright 2010-2023 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2010-2023 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-2023 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}