/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions copyright [year] [name of copyright owner]". * * Copyright 2026 3A Systems, LLC. */ package org.opends.server.protocols.ldap; import static org.opends.messages.ProtocolMessages.*; import static org.testng.Assert.*; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.Socket; import java.net.SocketTimeoutException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.RunnableScheduledFuture; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.function.Function; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.opendj.config.DefinedDefaultBehaviorProvider; import org.forgerock.opendj.config.PropertyDefinition; import org.forgerock.opendj.ldap.DN; import org.forgerock.opendj.reactive.LDAPClientConnection2; import org.forgerock.opendj.reactive.LDAPConnectionHandler2; import org.forgerock.opendj.server.config.meta.GlobalCfgDefn; import org.forgerock.opendj.server.config.meta.LDAPConnectionHandlerCfgDefn; import org.forgerock.opendj.server.config.server.LDAPConnectionHandlerCfg; import org.opends.server.DirectoryServerTestCase; import org.opends.server.TestCaseUtils; import org.opends.server.api.ClientConnection; import org.opends.server.api.ConnectionHandler; import org.opends.server.api.plugin.InternalDirectoryServerPlugin; import org.opends.server.api.plugin.PluginResult; import org.opends.server.api.plugin.PluginType; import org.opends.server.core.DirectoryServer; import org.opends.server.extensions.InitializationUtils; import org.opends.server.tools.LDAPReader; import org.opends.server.types.DisconnectReason; import org.opends.server.types.Entry; import org.testng.SkipException; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; /** * The limits which keep unauthenticated clients from holding every connection of the server: an LDAP connection * handler closes a connection on which no complete request arrives within {@code first-request-time-limit}, and rejects * a connection beyond {@code max-client-connections-per-address} from one address; the administration connector stays * reachable when the regular connections reach {@code max-allowed-client-connections}; and a new server ships with both * server-wide limits set. */ @SuppressWarnings("javadoc") @Test(groups = { "precommit" }, sequential = true) public class LDAPConnectionLimitsTestCase extends DirectoryServerTestCase { private static final LocalizableMessage STOP_REASON = LocalizableMessage.raw("Stopped by the limits test."); private static final String NOTICE_OF_DISCONNECTION_OID = "1.3.6.1.4.1.1466.20036"; private static final long TIMEOUT_MS = 10000; /** How long a connection which must stay open is watched. */ private static final int STAYS_OPEN_MS = 2500; private static final long FIRST_REQUEST_TIME_LIMIT_MS = 1000; /** An anonymous simple bind request, message ID 1. */ private static final byte[] ANONYMOUS_BIND = { 0x30, 0x0c, 0x02, 0x01, 0x01, 0x60, 0x07, 0x02, 0x01, 0x03, 0x04, 0x00, (byte) 0x80, 0x00 }; @BeforeClass public void setUp() throws Exception { TestCaseUtils.startServer(); } @Test public void silentConnectionIsClosedOnceTheFirstRequestTimeLimitIsSpent() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-first-request-time-limit: 1 seconds")); try (Socket client = new Socket("127.0.0.1", port)) { final long connected = System.currentTimeMillis(); assertNoticeOfDisconnection( client, INFO_CONNHANDLER_FIRST_REQUEST_TIME_LIMIT_EXCEEDED.get(FIRST_REQUEST_TIME_LIMIT_MS)); assertTrue(System.currentTimeMillis() - connected >= FIRST_REQUEST_TIME_LIMIT_MS - 100, "the connection was closed before the limit was spent"); awaitClientConnections(handler, 0); } finally { stop(handler); } } @Test public void partOfARequestIsNotAFirstRequest() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-first-request-time-limit: 1 seconds")); try (Socket client = new Socket("127.0.0.1", port)) { final OutputStream out = client.getOutputStream(); out.write(ANONYMOUS_BIND, 0, ANONYMOUS_BIND.length - 3); out.flush(); assertNoticeOfDisconnection( client, INFO_CONNHANDLER_FIRST_REQUEST_TIME_LIMIT_EXCEEDED.get(FIRST_REQUEST_TIME_LIMIT_MS)); } finally { stop(handler); } } @Test public void connectionWhichSentARequestOutlivesTheFirstRequestTimeLimit() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-first-request-time-limit: 1 seconds")); try (Socket client = new Socket("127.0.0.1", port)) { bindAnonymously(client); assertStaysOpen(client); assertEquals(handler.getClientConnections().size(), 1); } finally { stop(handler); } } @Test public void zeroFirstRequestTimeLimitKeepsASilentConnection() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-first-request-time-limit: 0 seconds")); try (Socket client = new Socket("127.0.0.1", port)) { awaitClientConnections(handler, 1); assertStaysOpen(client); } finally { stop(handler); } } /** * A first request time limit still pending when the handler is finalized must not keep its finalizer thread, which * the server shutdown waits for, alive until the limit is spent. */ @Test public void pendingFirstRequestTimeLimitDoesNotOutliveTheHandler() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-first-request-time-limit: 60 seconds")); final Thread finalizer = finalizerThread(handler); try (Socket client = new Socket("127.0.0.1", port)) { awaitClientConnections(handler, 1); awaitFirstRequestTimer(finalizerQueue(handler)); stop(handler); finalizer.join(TIMEOUT_MS); assertFalse(finalizer.isAlive(), "the finalizer thread waits for the first request time limit"); } finally { stop(handler); } } /** * The first request time limit of a connection leaves the finalizer queue as soon as the first request arrives or the * connection ends, rather than holding the connection until the limit is spent. */ @Test public void firstRequestTimeLimitIsCancelledByTheFirstRequestAndByTheEndOfTheConnection() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-first-request-time-limit: 60 seconds")); try { final Queue queue = finalizerQueue(handler); try (Socket client = new Socket("127.0.0.1", port)) { awaitClientConnections(handler, 1); final RunnableScheduledFuture timer = awaitFirstRequestTimer(queue); bindAnonymously(client); awaitTimerCancelled(queue, timer, "the first request"); } awaitClientConnections(handler, 0); final RunnableScheduledFuture timer; try (Socket client = new Socket("127.0.0.1", port)) { awaitClientConnections(handler, 1); timer = awaitFirstRequestTimer(queue); } awaitTimerCancelled(queue, timer, "the end of the connection"); } finally { stop(handler); } } /** * A connection accepted after the handler is finalized, before its listener stops, is still released when it * closes: the handler has no finalizer left to time it. */ @Test public void connectionAcceptedWhileTheHandlerIsFinalizedIsReleasedWhenItCloses() throws Exception { for (int attempt = 0; attempt < 5; attempt++) { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port)); try { // The listener stops only when run() next wakes up, up to a second later. handler.finalizeConnectionHandler(STOP_REASON); try (Socket client = new Socket("127.0.0.1", port)) { if (!awaitClientConnectionsQuietly(handler, 1)) { continue; } } catch (IOException listenerAlreadyStopped) { continue; } awaitClientConnections(handler, 0); return; } finally { handler.join(TIMEOUT_MS); } } throw new SkipException("the listener stopped before a connection could reach the finalized handler"); } /** * A finalized handler has no finalizer left: a connection it accepts before its listener stops gets no first request * time limit, rather than failing its accept. */ @Test public void finalizedHandlerSetsNoFirstRequestTimeLimit() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-first-request-time-limit: 60 seconds")); try (Socket client = new Socket("127.0.0.1", port)) { awaitClientConnections(handler, 1); final LDAPClientConnection2 connection = (LDAPClientConnection2) handler.getClientConnections().iterator().next(); handler.finalizeConnectionHandler(STOP_REASON); final Method limit = LDAPConnectionHandler2.class.getDeclaredMethod("limitTimeToFirstRequest", LDAPClientConnection2.class); limit.setAccessible(true); limit.invoke(handler, connection); } finally { handler.join(TIMEOUT_MS); } } @Test public void handlerLimitsDefaultToAMinuteForTheFirstRequestAnd1024ConnectionsPerAddress() throws Exception { final LDAPConnectionHandlerCfg config = configuration(TestCaseUtils.findFreePort()); assertEquals(config.getFirstRequestTimeLimit(), 60000L); assertEquals(config.getMaxClientConnectionsPerAddress(), 1024); } /** * The administration connector is not bounded by max-allowed-client-connections, so its own limits are what keep a * client from holding it. */ @Test public void administrationConnectorLimitsAMinuteForTheFirstRequestAnd1024ConnectionsPerAddress() throws Exception { final LDAPConnectionHandlerCfg config = currentConfig(administrationConnectionHandler()); assertEquals(config.getFirstRequestTimeLimit(), 60000L); assertEquals(config.getMaxClientConnectionsPerAddress(), 1024); } @Test public void connectionBeyondTheLimitOfItsAddressIsRejected() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-max-client-connections-per-address: 2")); try (Socket first = new Socket("127.0.0.1", port); Socket second = new Socket("127.0.0.1", port)) { awaitClientConnections(handler, 2); try (Socket third = new Socket("127.0.0.1", port)) { assertNoticeOfDisconnection(third, ERR_CONNHANDLER_CLIENT_ADDRESS_LIMIT_REACHED.get( "127.0.0.1:" + third.getLocalPort(), "127.0.0.1:" + port, 2)); } assertEquals(handler.getClientConnections().size(), 2); assertStaysOpen(first); } finally { stop(handler); } } @Test public void closedConnectionGivesBackItsPlaceToItsAddress() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-max-client-connections-per-address: 2")); try (Socket first = new Socket("127.0.0.1", port)) { try (Socket second = new Socket("127.0.0.1", port)) { awaitClientConnections(handler, 2); } awaitClientConnections(handler, 1); // The closed connection leaves the handler before it gives back its place. awaitPlacesHeld(handler, 1); try (Socket third = new Socket("127.0.0.1", port)) { awaitClientConnections(handler, 2); bindAnonymously(third); } } finally { stop(handler); } } @Test public void rejectedConnectionDoesNotTakeAPlaceOfItsAddress() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-max-client-connections-per-address: 1")); try { try (Socket first = new Socket("127.0.0.1", port)) { awaitClientConnections(handler, 1); for (int i = 0; i < 3; i++) { try (Socket rejected = new Socket("127.0.0.1", port)) { rejected.setSoTimeout((int) TIMEOUT_MS); assertNotNull(new LDAPReader(rejected).readMessage(), "the rejected connection got no notice"); } } } // Only the first connection held a place: once it is closed, a new one is accepted. awaitClientConnections(handler, 0); try (Socket next = new Socket("127.0.0.1", port)) { bindAnonymously(next); } } finally { stop(handler); } } /** A connection which takes a place and is then rejected by a post-connect plugin gives the place back. */ @Test public void connectionRejectedAfterTakingAPlaceGivesItBack() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-max-client-connections-per-address: 1")); final LocalizableMessage rejection = LocalizableMessage.raw("Rejected by the limits test plugin."); final PostConnectPlugin plugin = new PostConnectPlugin(handler, connection -> PluginResult.PostConnect.disconnectClient(DisconnectReason.CLOSED_BY_PLUGIN, true, rejection)); DirectoryServer.registerInternalPlugin(plugin); try { for (int i = 0; i < 2; i++) { try (Socket rejected = new Socket("127.0.0.1", port)) { assertNoticeOfDisconnection(rejected, rejection); } // The notice goes out before the rejection unwinds to the place being given back. awaitPlacesHeld(handler, 0); } } finally { DirectoryServer.deregisterInternalPlugin(plugin); stop(handler); } } /** An error, not only an exception, thrown once the connection took its place gives the place back. */ @Test public void connectionFailingWithAnErrorGivesBackItsPlace() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-max-client-connections-per-address: 1")); final PostConnectPlugin plugin = new PostConnectPlugin(handler, connection -> { throw new NoClassDefFoundError("Thrown by the limits test plugin."); }); DirectoryServer.registerInternalPlugin(plugin); try { try (Socket failed = new Socket("127.0.0.1", port)) { failed.setSoTimeout((int) TIMEOUT_MS); assertEquals(failed.getInputStream().read(), -1, "the failed connection stayed open"); } awaitPlacesHeld(handler, 0); } finally { DirectoryServer.deregisterInternalPlugin(plugin); stop(handler); } } /** * A connection which a post-connect plugin ends, and still lets through, ends before the handler listens to it: it * still leaves the handler and gives back its place. */ @Test public void connectionEndedBeforeTheHandlerListensToItGivesBackItsPlace() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-max-client-connections-per-address: 1")); final PostConnectPlugin plugin = new PostConnectPlugin(handler, connection -> { connection.disconnect(DisconnectReason.CLOSED_BY_PLUGIN, false, null); return PluginResult.PostConnect.continueConnectProcessing(); }); DirectoryServer.registerInternalPlugin(plugin); try { try (Socket ended = new Socket("127.0.0.1", port)) { ended.setSoTimeout((int) TIMEOUT_MS); assertEquals(ended.getInputStream().read(), -1, "the ended connection stayed open"); } awaitClientConnections(handler, 0); awaitPlacesHeld(handler, 0); } finally { DirectoryServer.deregisterInternalPlugin(plugin); stop(handler); } } /** A first request time limit set on a connection which has ended already is cancelled at once. */ @Test public void firstRequestTimeLimitSetOnAnEndedConnectionIsCancelled() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port)); try { final LDAPClientConnection2 connection; try (Socket client = new Socket("127.0.0.1", port)) { awaitClientConnections(handler, 1); connection = (LDAPClientConnection2) handler.getClientConnections().iterator().next(); } awaitClientConnections(handler, 0); final Method setTimer = LDAPClientConnection2.class.getDeclaredMethod("setFirstRequestTimer", Future.class); setTimer.setAccessible(true); final CompletableFuture timer = new CompletableFuture<>(); setTimer.invoke(connection, timer); assertTrue(timer.isCancelled(), "a time limit set on an ended connection was not cancelled"); } finally { stop(handler); } } /** Connections are counted whatever the limit, so a limit set on a running handler covers those already open. */ @Test public void limitPerAddressSetAtRuntimeCoversTheOpenConnections() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-max-client-connections-per-address: 0")); try (Socket first = new Socket("127.0.0.1", port); Socket second = new Socket("127.0.0.1", port)) { awaitClientConnections(handler, 2); handler.applyConfigurationChange(configuration(port, "ds-cfg-max-client-connections-per-address: 2")); try (Socket third = new Socket("127.0.0.1", port)) { assertNoticeOfDisconnection(third, ERR_CONNHANDLER_CLIENT_ADDRESS_LIMIT_REACHED.get( "127.0.0.1:" + third.getLocalPort(), "127.0.0.1:" + port, 2)); } } finally { stop(handler); } } @Test public void zeroLimitPerAddressAcceptsEveryConnection() throws Exception { final int port = TestCaseUtils.findFreePort(); final LDAPConnectionHandler2 handler = start(configuration(port, "ds-cfg-max-client-connections-per-address: 0")); final List clients = new ArrayList<>(); try { for (int i = 0; i < 5; i++) { clients.add(new Socket("127.0.0.1", port)); } awaitClientConnections(handler, 5); } finally { for (Socket client : clients) { client.close(); } stop(handler); } } /** * Once the regular connections reach max-allowed-client-connections, the administrator must still be able to * connect to the administration connector, or nothing short of a restart can raise the limit. */ @Test public void administrationConnectorIsReachableWhenTheConnectionLimitIsReached() throws Exception { final List clients = new ArrayList<>(); // The limit the server runs with, 0 when it has none, to set back afterwards. final long configured = Math.max(DirectoryServer.getCoreConfigManager().getMaxAllowedConnections(), 0); final long before = DirectoryServer.getCurrentConnections(); final long limit = before + 3; TestCaseUtils.dsconfig("set-global-configuration-prop", "--set", "max-allowed-client-connections:" + limit); try { // The connection of dsconfig, like any connection still closing, is uncounted asynchronously: fill the limit // only once it is gone, or its uncounting would leave a place for the connection which must be rejected. awaitCurrentConnectionsAtMost(before); while (DirectoryServer.getCurrentConnections() < limit) { final Socket client = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort()); clients.add(client); bindAnonymously(client); } try (Socket rejected = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort())) { rejected.setSoTimeout((int) TIMEOUT_MS); final LDAPMessage notice = new LDAPReader(rejected).readMessage(); assertNotNull(notice, "the connection beyond the limit got no notice"); assertEquals(notice.getExtendedResponseProtocolOp().getOID(), NOTICE_OF_DISCONNECTION_OID, "the regular connections did not reach the limit"); } try (SSLSocket admin = (SSLSocket) trustAll().getSocketFactory().createSocket( "127.0.0.1", TestCaseUtils.getServerAdminPort())) { admin.startHandshake(); bindAnonymously(admin); // Exempt from the limit, but counted. assertEquals(DirectoryServer.getCurrentConnections(), limit + 1, "connections counted with the administrator's"); } catch (IOException e) { fail("the administration connector refused the connection once the limit was reached", e); } } finally { for (Socket client : clients) { client.close(); } final long deadline = System.currentTimeMillis() + TIMEOUT_MS; while (DirectoryServer.getCurrentConnections() >= limit && System.currentTimeMillis() < deadline) { Thread.sleep(50); } TestCaseUtils.dsconfig("set-global-configuration-prop", "--set", "max-allowed-client-connections:" + configured); } } @Test public void newServerLimitsTheConnectionsAndTheirIdleTime() throws Exception { final File template = new File(TestCaseUtils.getBuildRoot(), "resource/config/config.ldif"); final List lines = Files.readAllLines(template.toPath(), StandardCharsets.UTF_8); assertTrue(lines.contains("ds-cfg-max-allowed-client-connections: 4096"), "max-allowed-client-connections of " + template); assertTrue(lines.contains("ds-cfg-idle-time-limit: 600 seconds"), "idle-time-limit of " + template); } /** A server without a value, for example after {@code dsconfig --reset}, falls back to the same limits. */ @Test public void serverWithoutAValueLimitsTheConnectionsAndTheirIdleTime() throws Exception { final GlobalCfgDefn global = GlobalCfgDefn.getInstance(); assertEquals(defaultValues(global.getMaxAllowedClientConnectionsPropertyDefinition()), Arrays.asList("4096")); assertEquals(defaultValues(global.getIdleTimeLimitPropertyDefinition()), Arrays.asList("600 seconds")); } private static List defaultValues(PropertyDefinition property) { return new ArrayList<>(((DefinedDefaultBehaviorProvider) property.getDefaultBehaviorProvider()) .getDefaultValues()); } /** Runs the given post-connect phase on every connection to the given handler. */ private static final class PostConnectPlugin extends InternalDirectoryServerPlugin { private final LDAPConnectionHandler2 handler; private final Function postConnect; private PostConnectPlugin(LDAPConnectionHandler2 handler, Function postConnect) { super(DN.valueOf("cn=Limits Test Post Connect Plugin"), EnumSet.of(PluginType.POST_CONNECT), false); this.handler = handler; this.postConnect = postConnect; } @Override public PluginResult.PostConnect doPostConnect(ClientConnection clientConnection) { if (clientConnection.getConnectionHandler() == handler) { return postConnect.apply(clientConnection); } return PluginResult.PostConnect.continueConnectProcessing(); } } private static LDAPConnectionHandler2 administrationConnectionHandler() { for (ConnectionHandler handler : DirectoryServer.getConnectionHandlers()) { if (handler.isAdminConnectionHandler()) { return (LDAPConnectionHandler2) handler; } } throw new AssertionError("no administration connector"); } private static LDAPConnectionHandlerCfg currentConfig(LDAPConnectionHandler2 handler) throws Exception { final Field field = LDAPConnectionHandler2.class.getDeclaredField("currentConfig"); field.setAccessible(true); return (LDAPConnectionHandlerCfg) field.get(handler); } private static Queue finalizerQueue(LDAPConnectionHandler2 handler) throws Exception { return ((ThreadPoolExecutor) finalizer(handler)).getQueue(); } /** Returns the thread of the connection finalizer, which runs its single-threaded executor. */ private static Thread finalizerThread(LDAPConnectionHandler2 handler) throws Exception { return finalizer(handler).submit(Thread::currentThread).get(TIMEOUT_MS, TimeUnit.MILLISECONDS); } private static ExecutorService finalizer(LDAPConnectionHandler2 handler) throws Exception { final Field field = LDAPConnectionHandler2.class.getDeclaredField("connectionFinalizer"); field.setAccessible(true); return (ExecutorService) field.get(handler); } /** Waits until the addresses of the clients hold the given number of places in all. */ private static void awaitPlacesHeld(LDAPConnectionHandler2 handler, int expected) throws Exception { final Field field = LDAPConnectionHandler2.class.getDeclaredField("connectionsPerAddress"); field.setAccessible(true); final Map connectionsPerAddress = (Map) field.get(handler); final long deadline = System.currentTimeMillis() + TIMEOUT_MS; while (connectionsPerAddress.values().stream().mapToInt(held -> (Integer) held).sum() != expected) { assertTrue(System.currentTimeMillis() < deadline, "places held: " + connectionsPerAddress + ", not " + expected + " in all"); Thread.sleep(50); } } /** * Waits for the first request time limit of the only connection in the finalizer queue. The queue cannot be counted * instead: it does not hold the periodic task of the finalizer while that task runs. */ private static RunnableScheduledFuture awaitFirstRequestTimer(Queue queue) throws Exception { final long deadline = System.currentTimeMillis() + TIMEOUT_MS; while (true) { for (Runnable task : queue) { final RunnableScheduledFuture scheduled = (RunnableScheduledFuture) task; if (!scheduled.isPeriodic()) { return scheduled; } } assertTrue(System.currentTimeMillis() < deadline, "no first request time limit in the finalizer queue"); Thread.sleep(50); } } /** Waits until the given first request time limit is cancelled and out of the finalizer queue. */ private static void awaitTimerCancelled(Queue queue, RunnableScheduledFuture timer, String by) throws Exception { final long deadline = System.currentTimeMillis() + TIMEOUT_MS; while (!timer.isCancelled() || queue.contains(timer)) { assertTrue(System.currentTimeMillis() < deadline, by + " did not cancel the first request time limit, or " + "left it in the finalizer queue: cancelled " + timer.isCancelled()); Thread.sleep(50); } } private static void awaitCurrentConnectionsAtMost(long expected) throws Exception { final long deadline = System.currentTimeMillis() + TIMEOUT_MS; while (DirectoryServer.getCurrentConnections() > expected) { assertTrue(System.currentTimeMillis() < deadline, "the server counts " + DirectoryServer.getCurrentConnections() + " connections, more than " + expected); Thread.sleep(10); } } private static void bindAnonymously(Socket client) throws Exception { final OutputStream out = client.getOutputStream(); out.write(ANONYMOUS_BIND); out.flush(); client.setSoTimeout((int) TIMEOUT_MS); final LDAPMessage response = new LDAPReader(client).readMessage(); assertNotNull(response, "the connection was closed instead of answering the bind"); assertEquals(response.getBindResponseProtocolOp().getResultCode(), LDAPResultCode.SUCCESS, "bind result"); } /** Checks that the server neither writes to the connection nor closes it for a while. */ private static void assertStaysOpen(Socket client) throws Exception { client.setSoTimeout(STAYS_OPEN_MS); try { final int read = client.getInputStream().read(); fail("the connection did not stay open: read " + read); } catch (SocketTimeoutException expected) { // Neither data nor the end of the stream within the time. } } /** Reads the next message from the client, checks it is the expected notice of disconnection, then the end. */ private static void assertNoticeOfDisconnection(Socket client, LocalizableMessage reason) throws Exception { client.setSoTimeout((int) TIMEOUT_MS); final LDAPMessage message; try { message = new LDAPReader(client).readMessage(); } catch (SocketTimeoutException e) { throw new AssertionError("the connection is still open", e); } assertNotNull(message, "the connection was closed without a notice of disconnection"); final ExtendedResponseProtocolOp notice = message.getExtendedResponseProtocolOp(); assertEquals(notice.getOID(), NOTICE_OF_DISCONNECTION_OID); assertEquals(String.valueOf(notice.getErrorMessage()), reason.toString(), "diagnostic message"); assertEquals(client.getInputStream().read(), -1, "the connection stayed open after the notice"); } private static boolean awaitClientConnectionsQuietly(LDAPConnectionHandler2 handler, int expected) throws Exception { final long deadline = System.currentTimeMillis() + TIMEOUT_MS; while (handler.getClientConnections().size() != expected) { if (System.currentTimeMillis() >= deadline) { return false; } Thread.sleep(50); } return true; } private static void awaitClientConnections(LDAPConnectionHandler2 handler, int expected) throws Exception { final long deadline = System.currentTimeMillis() + TIMEOUT_MS; while (handler.getClientConnections().size() != expected) { assertTrue(System.currentTimeMillis() < deadline, "the handler holds " + handler.getClientConnections().size() + " connections, not " + expected); Thread.sleep(50); } } private static SSLContext trustAll() throws Exception { final SSLContext context = SSLContext.getInstance("TLS"); context.init(null, new TrustManager[] { new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { // The test trusts the self-signed certificate of the administration connector. } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { // The test trusts the self-signed certificate of the administration connector. } @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } }, null); return context; } private static LDAPConnectionHandlerCfg configuration(int port, String... limits) throws Exception { final List lines = new ArrayList<>(); lines.add("dn: cn=Limits Test Handler,cn=Connection Handlers,cn=config"); lines.add("objectClass: top"); lines.add("objectClass: ds-cfg-connection-handler"); lines.add("objectClass: ds-cfg-ldap-connection-handler"); lines.add("cn: Limits Test Handler"); lines.add("ds-cfg-java-class: " + LDAPConnectionHandler2.class.getName()); lines.add("ds-cfg-enabled: true"); lines.add("ds-cfg-listen-address: 127.0.0.1"); lines.add("ds-cfg-listen-port: " + port); lines.add("ds-cfg-keep-stats: false"); lines.add("ds-cfg-use-ssl: false"); lines.add("ds-cfg-allow-start-tls: false"); lines.add("ds-cfg-allow-ldap-v2: false"); lines.add("ds-cfg-send-rejection-notice: true"); for (String limit : limits) { lines.add(limit); } final Entry entry = TestCaseUtils.makeEntry(lines.toArray(new String[0])); return InitializationUtils.getConfiguration(LDAPConnectionHandlerCfgDefn.getInstance(), entry); } private static LDAPConnectionHandler2 start(LDAPConnectionHandlerCfg config) throws Exception { final LDAPConnectionHandler2 handler = new LDAPConnectionHandler2(); handler.initializeConnectionHandler(DirectoryServer.getInstance().getServerContext(), config); handler.start(); return handler; } private static void stop(LDAPConnectionHandler2 handler) throws InterruptedException { if (!handler.isAlive()) { return; } handler.processServerShutdown(STOP_REASON); handler.finalizeConnectionHandler(STOP_REASON); handler.join(TIMEOUT_MS); assertFalse(handler.isAlive(), "the connection handler thread is still running"); } }