001/* 002 * Copyright 2010-2020 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2010-2020 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-2020 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.StaticUtils; 059import com.unboundid.util.ThreadSafety; 060import com.unboundid.util.ThreadSafetyLevel; 061 062import static com.unboundid.ldap.listener.ListenerMessages.*; 063 064 065 066/** 067 * This class provides a framework that may be used to accept connections from 068 * LDAP clients and ensure that any requests received on those connections will 069 * be processed appropriately. It can be used to easily allow applications to 070 * accept LDAP requests, to create a simple proxy that can intercept and 071 * examine LDAP requests and responses passing between a client and server, or 072 * helping to test LDAP clients. 073 * <BR><BR> 074 * <H2>Example</H2> 075 * The following example demonstrates the process that can be used to create an 076 * LDAP listener that will listen for LDAP requests on a randomly-selected port 077 * and immediately respond to them with a "success" result: 078 * <PRE> 079 * // Create a canned response request handler that will always return a 080 * // "SUCCESS" result in response to any request. 081 * CannedResponseRequestHandler requestHandler = 082 * new CannedResponseRequestHandler(ResultCode.SUCCESS, null, null, 083 * null); 084 * 085 * // A listen port of zero indicates that the listener should 086 * // automatically pick a free port on the system. 087 * int listenPort = 0; 088 * 089 * // Create and start an LDAP listener to accept requests and blindly 090 * // return success results. 091 * LDAPListenerConfig listenerConfig = new LDAPListenerConfig(listenPort, 092 * requestHandler); 093 * LDAPListener listener = new LDAPListener(listenerConfig); 094 * listener.startListening(); 095 * 096 * // Establish a connection to the listener and verify that a search 097 * // request will get a success result. 098 * LDAPConnection connection = new LDAPConnection("localhost", 099 * listener.getListenPort()); 100 * SearchResult searchResult = connection.search("dc=example,dc=com", 101 * SearchScope.BASE, Filter.createPresenceFilter("objectClass")); 102 * LDAPTestUtils.assertResultCodeEquals(searchResult, 103 * ResultCode.SUCCESS); 104 * 105 * // Close the connection and stop the listener. 106 * connection.close(); 107 * listener.shutDown(true); 108 * </PRE> 109 */ 110@ThreadSafety(level=ThreadSafetyLevel.NOT_THREADSAFE) 111public final class LDAPListener 112 extends Thread 113{ 114 // Indicates whether a request has been received to stop running. 115 private final AtomicBoolean stopRequested; 116 117 // The connection ID value that should be assigned to the next connection that 118 // is established. 119 private final AtomicLong nextConnectionID; 120 121 // The server socket that is being used to accept connections. 122 private final AtomicReference<ServerSocket> serverSocket; 123 124 // The thread that is currently listening for new client connections. 125 private final AtomicReference<Thread> thread; 126 127 // A map of all established connections. 128 private final ConcurrentHashMap<Long,LDAPListenerClientConnection> 129 establishedConnections; 130 131 // The latch used to wait for the listener to have started. 132 private final CountDownLatch startLatch; 133 134 // The configuration to use for this listener. 135 private final LDAPListenerConfig config; 136 137 138 139 /** 140 * Creates a new {@code LDAPListener} object with the provided configuration. 141 * The {@link #startListening} method must be called after creating the object 142 * to actually start listening for requests. 143 * 144 * @param config The configuration to use for this listener. 145 */ 146 public LDAPListener(final LDAPListenerConfig config) 147 { 148 this.config = config.duplicate(); 149 150 stopRequested = new AtomicBoolean(false); 151 nextConnectionID = new AtomicLong(0L); 152 serverSocket = new AtomicReference<>(null); 153 thread = new AtomicReference<>(null); 154 startLatch = new CountDownLatch(1); 155 establishedConnections = 156 new ConcurrentHashMap<>(StaticUtils.computeMapCapacity(20)); 157 setName("LDAP Listener Thread (not listening"); 158 } 159 160 161 162 /** 163 * Creates the server socket for this listener and starts listening for client 164 * connections. This method will return after the listener has stated. 165 * 166 * @throws IOException If a problem occurs while creating the server socket. 167 */ 168 public void startListening() 169 throws IOException 170 { 171 final ServerSocketFactory f = config.getServerSocketFactory(); 172 final InetAddress a = config.getListenAddress(); 173 final int p = config.getListenPort(); 174 if (a == null) 175 { 176 serverSocket.set(f.createServerSocket(config.getListenPort(), 128)); 177 } 178 else 179 { 180 serverSocket.set(f.createServerSocket(config.getListenPort(), 128, a)); 181 } 182 183 final int receiveBufferSize = config.getReceiveBufferSize(); 184 if (receiveBufferSize > 0) 185 { 186 serverSocket.get().setReceiveBufferSize(receiveBufferSize); 187 } 188 189 setName("LDAP Listener Thread (listening on port " + 190 serverSocket.get().getLocalPort() + ')'); 191 192 start(); 193 194 try 195 { 196 startLatch.await(); 197 } 198 catch (final Exception e) 199 { 200 Debug.debugException(e); 201 } 202 } 203 204 205 206 /** 207 * Operates in a loop, waiting for client connections to arrive and ensuring 208 * that they are handled properly. This method is for internal use only and 209 * must not be called by third-party code. 210 */ 211 @InternalUseOnly() 212 @Override() 213 public void run() 214 { 215 thread.set(Thread.currentThread()); 216 final LDAPListenerExceptionHandler exceptionHandler = 217 config.getExceptionHandler(); 218 219 try 220 { 221 startLatch.countDown(); 222 while (! stopRequested.get()) 223 { 224 final Socket s; 225 try 226 { 227 s = serverSocket.get().accept(); 228 } 229 catch (final Exception e) 230 { 231 Debug.debugException(e); 232 233 if ((e instanceof SocketException) && 234 serverSocket.get().isClosed()) 235 { 236 return; 237 } 238 239 if (exceptionHandler != null) 240 { 241 exceptionHandler.connectionCreationFailure(null, e); 242 } 243 244 continue; 245 } 246 247 final LDAPListenerClientConnection c; 248 try 249 { 250 c = new LDAPListenerClientConnection(this, s, 251 config.getRequestHandler(), config.getExceptionHandler()); 252 } 253 catch (final LDAPException le) 254 { 255 Debug.debugException(le); 256 257 if (exceptionHandler != null) 258 { 259 exceptionHandler.connectionCreationFailure(s, le); 260 } 261 262 continue; 263 } 264 265 final int maxConnections = config.getMaxConnections(); 266 if ((maxConnections > 0) && 267 (establishedConnections.size() >= maxConnections)) 268 { 269 c.close(new LDAPException(ResultCode.BUSY, 270 ERR_LDAP_LISTENER_MAX_CONNECTIONS_ESTABLISHED.get( 271 maxConnections))); 272 continue; 273 } 274 275 establishedConnections.put(c.getConnectionID(), c); 276 c.start(); 277 } 278 } 279 finally 280 { 281 final ServerSocket s = serverSocket.getAndSet(null); 282 if (s != null) 283 { 284 try 285 { 286 s.close(); 287 } 288 catch (final Exception e) 289 { 290 Debug.debugException(e); 291 } 292 } 293 294 serverSocket.set(null); 295 thread.set(null); 296 } 297 } 298 299 300 301 /** 302 * Closes all connections that are currently established to this listener. 303 * This has no effect on the ability to accept new connections. 304 * 305 * @param sendNoticeOfDisconnection Indicates whether to send the client a 306 * notice of disconnection unsolicited 307 * notification before closing the 308 * connection. 309 */ 310 public void closeAllConnections(final boolean sendNoticeOfDisconnection) 311 { 312 final NoticeOfDisconnectionExtendedResult noticeOfDisconnection = 313 new NoticeOfDisconnectionExtendedResult(ResultCode.OTHER, null); 314 315 final ArrayList<LDAPListenerClientConnection> connList = 316 new ArrayList<>(establishedConnections.values()); 317 for (final LDAPListenerClientConnection c : connList) 318 { 319 if (sendNoticeOfDisconnection) 320 { 321 try 322 { 323 c.sendUnsolicitedNotification(noticeOfDisconnection); 324 } 325 catch (final Exception e) 326 { 327 Debug.debugException(e); 328 } 329 } 330 331 try 332 { 333 c.close(); 334 } 335 catch (final Exception e) 336 { 337 Debug.debugException(e); 338 } 339 } 340 } 341 342 343 344 /** 345 * Indicates that this listener should stop accepting connections. It may 346 * optionally also terminate any existing connections that are already 347 * established. 348 * 349 * @param closeExisting Indicates whether to close existing connections that 350 * may already be established. 351 */ 352 public void shutDown(final boolean closeExisting) 353 { 354 stopRequested.set(true); 355 356 final ServerSocket s = serverSocket.get(); 357 if (s != null) 358 { 359 try 360 { 361 s.close(); 362 } 363 catch (final Exception e) 364 { 365 Debug.debugException(e); 366 } 367 } 368 369 final Thread t = thread.get(); 370 if (t != null) 371 { 372 while (t.isAlive()) 373 { 374 try 375 { 376 t.join(100L); 377 } 378 catch (final Exception e) 379 { 380 Debug.debugException(e); 381 382 if (e instanceof InterruptedException) 383 { 384 Thread.currentThread().interrupt(); 385 } 386 } 387 388 if (t.isAlive()) 389 { 390 391 try 392 { 393 t.interrupt(); 394 } 395 catch (final Exception e) 396 { 397 Debug.debugException(e); 398 } 399 } 400 } 401 } 402 403 if (closeExisting) 404 { 405 closeAllConnections(false); 406 } 407 } 408 409 410 411 /** 412 * Retrieves the address on which this listener is accepting client 413 * connections. Note that if no explicit listen address was configured, then 414 * the address returned may not be usable by clients. In the event that the 415 * {@code InetAddress.isAnyLocalAddress} method returns {@code true}, then 416 * clients should generally use {@code localhost} to attempt to establish 417 * connections. 418 * 419 * @return The address on which this listener is accepting client 420 * connections, or {@code null} if it is not currently listening for 421 * client connections. 422 */ 423 public InetAddress getListenAddress() 424 { 425 final ServerSocket s = serverSocket.get(); 426 if (s == null) 427 { 428 return null; 429 } 430 else 431 { 432 return s.getInetAddress(); 433 } 434 } 435 436 437 438 /** 439 * Retrieves the port on which this listener is accepting client connections. 440 * 441 * @return The port on which this listener is accepting client connections, 442 * or -1 if it is not currently listening for client connections. 443 */ 444 public int getListenPort() 445 { 446 final ServerSocket s = serverSocket.get(); 447 if (s == null) 448 { 449 return -1; 450 } 451 else 452 { 453 return s.getLocalPort(); 454 } 455 } 456 457 458 459 /** 460 * Retrieves the configuration in use for this listener. It must not be 461 * altered in any way. 462 * 463 * @return The configuration in use for this listener. 464 */ 465 LDAPListenerConfig getConfig() 466 { 467 return config; 468 } 469 470 471 472 /** 473 * Retrieves the connection ID that should be used for the next connection 474 * accepted by this listener. 475 * 476 * @return The connection ID that should be used for the next connection 477 * accepted by this listener. 478 */ 479 long nextConnectionID() 480 { 481 return nextConnectionID.getAndIncrement(); 482 } 483 484 485 486 /** 487 * Indicates that the provided client connection has been closed and is no 488 * longer listening for client connections. 489 * 490 * @param connection The connection that has been closed. 491 */ 492 void connectionClosed(final LDAPListenerClientConnection connection) 493 { 494 establishedConnections.remove(connection.getConnectionID()); 495 } 496}