mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valera V Harseko
23 hours ago 8b7cbc2dcced1334e2829a722d456cfa4105cf1c
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-connection-handlers.adoc
@@ -12,7 +12,7 @@
  information: "Portions copyright [year] [name of copyright owner]".
 
  Copyright 2017 ForgeRock AS.
  Portions Copyright 2024-2025 3A Systems LLC.
  Portions Copyright 2024-2026 3A Systems LLC.
////
:figure-caption!:
@@ -757,7 +757,7 @@
----
`max-allowed-client-connections`::
Restricts the number of concurrent client connections to the directory server. Default: 0, meaning no limit is set.
Restricts the number of concurrent client connections to the directory server. Connections to the administration connector count toward the limit but are never rejected by it. Default: 4096. A value of 0 sets no limit.
+
To set a limit of 32768 use the following command:
@@ -814,6 +814,7 @@
----
--
Each LDAP connection handler also limits what a single client can hold before it authenticates. The `first-request-time-limit` property closes a connection on which no complete request arrives in time, 60 seconds by default, and the `max-client-connections-per-address` property limits the connections from one client IP address, 1024 by default. For details, see xref:chap-resource-limits.adoc#limit-unauthenticated-connections["Limiting Connections Before Authentication"].
[#tls-protocols-cipher-suites]
opendj-doc-generated-ref/src/main/asciidoc/admin-guide/chap-resource-limits.adoc
@@ -12,7 +12,7 @@
  information: "Portions copyright [year] [name of copyright owner]".
 
  Copyright 2017 ForgeRock AS.
  Portions Copyright 2024 3A Systems LLC.
  Portions Copyright 2024-2026 3A Systems LLC.
////
:figure-caption!:
@@ -29,6 +29,8 @@
* Limit how long connections can remain idle before they are dropped
* Limit the connections a client can hold before it authenticates
* Limit the size of directory server requests
@@ -58,7 +60,7 @@
* The __idle time limit__ defines how long OpenDJ allows idle connections to remain open.
+
No default idle time limit is set. You can set an idle time limit by using the global server property `idle-time-limit`.
The default idle time limit, which is set by using the global server property `idle-time-limit`, is 600 seconds. Servers set up with earlier versions keep the explicit `0 seconds`, no limit at all, they were set up with; a server with no value, for example after `dsconfig set-global-configuration-prop --reset idle-time-limit`, gets the new default.
+
You can override the limit for a particular user by changing the operational attribute, `ds-rlim-idle-time-limit`, on the user's entry. Times for `ds-rlim-idle-time-limit` are expressed in seconds.
@@ -152,7 +154,7 @@
[#limit-idle-time]
=== Limiting Idle Time
If you have applications that leave connections open for long periods, OpenDJ can end up devoting resources to maintaining connections that are no longer used. If your network does not drop such connections eventually, you can configure OpenDJ to drop them by setting the global configuration property, `idle-time-limit`. By default, no idle time limit is set.
If you have applications that leave connections open for long periods, OpenDJ can end up devoting resources to maintaining connections that are no longer used. If your network does not drop such connections eventually, OpenDJ drops them once they have been idle for the time set by the global configuration property, `idle-time-limit`. By default, the limit is 600 seconds; a value of `0 seconds` sets no limit. Servers set up with earlier versions have no limit until you set one.
If your network load balancer is configured to drop connections that have been idle for some time, make sure you set the OpenDJ idle time limit to a lower value than the idle time limit for the load balancer. This helps to ensure that idle connections are shut down in orderly fashion. Setting the OpenDJ limit lower than the load balancer limit is particularly useful with load balancers that drop idle connections without cleanly closing the connection and notifying the client and server.
@@ -176,6 +178,34 @@
The example shown sets the idle time limit to 24 hours.
[#limit-unauthenticated-connections]
=== Limiting Connections Before Authentication
A client does not need to authenticate to open a connection, so a client that opens connections and never uses them can take the connections, and the file descriptors, that other clients need. The following settings bound what such a client can hold:
* The global server property `max-allowed-client-connections` limits the number of concurrent client connections. By default, the limit is 4096; a value of `0` sets no limit. Connections to the administration connector count toward the limit but are never rejected by it, so that you can still connect to change the configuration once the other connections reach the limit. Servers set up with earlier versions keep the explicit `0` they were set up with; a server with no value, for example after `dsconfig set-global-configuration-prop --reset max-allowed-client-connections`, gets the new default.
* The LDAP connection handler property `first-request-time-limit` sets how long a new connection may stay open before a complete request arrives on it. By default, a connection on which no complete request arrives within 60 seconds is closed; a value of `0 seconds` sets no limit. Once the first request has arrived, `idle-time-limit` applies.
* The LDAP connection handler property `max-client-connections-per-address` limits the number of connections that a single client IP address can hold through the connection handler, so that one client cannot take all the connections allowed by `max-allowed-client-connections`. By default, the limit is 1024; a value of `0` sets no limit. The limit only protects the other clients while it stays below `max-allowed-client-connections`: a limit at or above it never applies, because the server-wide limit rejects the connection first. Clients behind a load balancer or a network address translation device share its address, so set the limit above the number of connections they need together.
The following example limits each client address to 256 connections through the LDAP connection handler:
[source, console]
----
$ dsconfig \
 set-connection-handler-prop \
 --port 4444 \
 --hostname opendj.example.com \
 --bindDN "cn=Directory Manager" \
 --bindPassword password \
 --handler-name "LDAP Connection Handler" \
 --set max-client-connections-per-address:256 \
 --trustAll \
 --no-prompt
----
[#limit-max-request-size]
=== Limiting Maximum Request Size
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/GlobalConfiguration.xml
@@ -728,7 +728,7 @@
    </adm:description>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>0 seconds</adm:value>
        <adm:value>600 seconds</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
@@ -804,10 +804,13 @@
    </adm:synopsis>
    <adm:description>
      A value of 0 indicates that unlimited client connection is allowed.
      Connections to the administration connector are counted but never
      rejected by this limit, so that the server can still be administered
      once the other connections have reached it.
    </adm:description>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>0</adm:value>
        <adm:value>4096</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
opendj-maven-plugin/src/main/resources/config/xml/org/forgerock/opendj/server/config/LDAPConnectionHandlerConfiguration.xml
@@ -649,4 +649,69 @@
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="first-request-time-limit">
    <adm:synopsis>
      Specifies the maximum length of time that a client connection may
      remain established before the client sends its first complete
      LDAP request.
    </adm:synopsis>
    <adm:description>
      A connection on which no complete request has arrived within this
      length of time is terminated, so that clients which connect and then
      send nothing, or only part of a request, cannot hold connections open.
      Once the first request has arrived, only the server-wide
      idle-time-limit applies. On a connection which uses SSL, the time
      also covers the handshake. A value of "0 seconds" indicates that no
      limit is enforced. Changes to this property only apply to connections
      accepted after the change.
    </adm:description>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>60 seconds</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:duration base-unit="ms" lower-limit="0" />
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-first-request-time-limit</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
  <adm:property name="max-client-connections-per-address">
    <adm:synopsis>
      Specifies the maximum number of client connections that may be
      established at any given time through this
      <adm:user-friendly-name />
      from a single client IP address.
    </adm:synopsis>
    <adm:description>
      A connection which would exceed the limit is rejected as soon as it
      is accepted. Kept below the server-wide
      max-allowed-client-connections, as the defaults are, this limit
      prevents a single client address from taking every connection the
      server allows. A limit at or above a server-wide limit other than 0
      never applies, because the server-wide limit rejects the connection
      first. Clients behind a load balancer or a network address
      translation device share its address, so set the limit above the
      number of connections all of them need together.
      A value of 0 indicates that no limit is enforced.
    </adm:description>
    <adm:default-behavior>
      <adm:defined>
        <adm:value>1024</adm:value>
      </adm:defined>
    </adm:default-behavior>
    <adm:syntax>
      <adm:integer lower-limit="0">
        <adm:unit-synopsis>connections</adm:unit-synopsis>
      </adm:integer>
    </adm:syntax>
    <adm:profile name="ldap">
      <ldap:attribute>
        <ldap:name>ds-cfg-max-client-connections-per-address</ldap:name>
      </ldap:attribute>
    </adm:profile>
  </adm:property>
</adm:managed-object>
opendj-server-legacy/resource/config/config.ldif
@@ -40,10 +40,10 @@
ds-cfg-reject-unauthenticated-requests: false
ds-cfg-default-password-policy: cn=Default Password Policy,cn=Password Policies,cn=config
ds-cfg-return-bind-error-messages: false
ds-cfg-idle-time-limit: 0 seconds
ds-cfg-idle-time-limit: 600 seconds
ds-cfg-save-config-on-successful-startup: true
ds-cfg-etime-resolution: milliseconds
ds-cfg-max-allowed-client-connections: 0
ds-cfg-max-allowed-client-connections: 4096
ds-cfg-max-psearches: -1
ds-cfg-allowed-task: org.opends.server.tasks.AddSchemaFileTask
ds-cfg-allowed-task: org.opends.server.tasks.BackupTask
opendj-server-legacy/resource/schema/02-config.ldif
@@ -4118,6 +4118,18 @@
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  SINGLE-VALUE
  X-ORIGIN 'OpenDJ Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.60142.2.1.1.3
  NAME 'ds-cfg-first-request-time-limit'
  EQUALITY caseIgnoreMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  SINGLE-VALUE
  X-ORIGIN 'OpenDJ Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.60142.2.1.1.4
  NAME 'ds-cfg-max-client-connections-per-address'
  EQUALITY integerMatch
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.27
  SINGLE-VALUE
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
  NAME 'ds-cfg-access-control-handler'
  SUP top
@@ -4208,7 +4220,9 @@
        ds-cfg-ssl-protocol $
        ds-cfg-ssl-cipher-suite $
        ds-cfg-max-blocked-write-time-limit $
        ds-cfg-buffer-size )
        ds-cfg-buffer-size $
        ds-cfg-first-request-time-limit $
        ds-cfg-max-client-connections-per-address )
  X-ORIGIN 'OpenDS Directory Server' )
# OPENDJ-3048 attributes 'ds-cfg-config-file' and 'ds-cfg-authentication-required'
# are kept for backward compatibility to avoid breaking replication
opendj-server-legacy/src/main/java/org/forgerock/opendj/reactive/LDAPClientConnection2.java
@@ -38,6 +38,7 @@
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@@ -136,6 +137,10 @@
    /** The time that the last operation was completed. */
    private final AtomicLong lastCompletionTime;
    /** Whether a complete request has arrived on this connection, which ends its first request time limit. */
    private volatile boolean requestReceived;
    /** The task which closes this connection unless its first request arrives in time, or {@code null}. */
    private volatile Future<?> firstRequestTimer;
    /** The next operation ID that should be used for this connection. */
    private final AtomicLong nextOperationID;
@@ -645,6 +650,7 @@
        // Indicate that this connection is no longer valid.
        connectionValid = false;
        cancelFirstRequestTimer();
        final LocalizableMessage cancelMessage;
        if (message != null) {
@@ -943,6 +949,39 @@
    }
    /**
     * Indicates whether a complete request has arrived on this connection.
     *
     * @return {@code true} once the first complete request has arrived, {@code false} before
     */
    boolean hasReceivedRequest() {
        return requestReceived;
    }
    /**
     * Sets the task which closes this connection unless its first request arrives in time. The task is cancelled once
     * the first request arrives or the connection ends, so that neither it nor this connection waits out the limit.
     *
     * @param timer
     *            the scheduled task
     */
    void setFirstRequestTimer(final Future<?> timer) {
        firstRequestTimer = timer;
        // The connection may have ended before the task was set. Its first request cannot have come before: the task
        // is set while the connection is accepted, and its requests are handled only once it is.
        if (!connectionValid) {
            cancelFirstRequestTimer();
        }
    }
    private void cancelFirstRequestTimer() {
        final Future<?> timer = firstRequestTimer;
        if (timer != null) {
            firstRequestTimer = null;
            timer.cancel(false);
        }
    }
    /**
     * Returns the total number of operations initiated on this connection.
     *
     * @return the total number of operations on this connection
@@ -966,6 +1005,10 @@
     */
    @Override
    public Stream<Response> handle(final QueueingStrategy queueingStrategy, final LdapRequestEnvelope message) {
        if (!requestReceived) {
            requestReceived = true;
            cancelFirstRequestTimer();
        }
        return streamFromPublisher(
                new BlockingBackpressureSubscription(connectionHandler.getMaxBlockedWriteTimeLimit(),
                        Flowable.create(new FlowableOnSubscribe<Response>() {
opendj-server-legacy/src/main/java/org/forgerock/opendj/reactive/LDAPConnectionHandler2.java
@@ -43,8 +43,8 @@
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -422,6 +422,12 @@
            newSetFromMap(new ConcurrentHashMap<ClientConnection, Boolean>());
    /**
     * The number of connections held by each client address, against {@code max-client-connections-per-address}.
     * Counted whatever the limit, so that a limit set while connections are open applies to them as well.
     */
    private final ConcurrentHashMap<InetAddress, Integer> connectionsPerAddress = new ConcurrentHashMap<>();
    /**
     * Creates a new instance of this LDAP connection handler. It must be initialized before it may be used.
     */
    public LDAPConnectionHandler2() {
@@ -735,8 +741,13 @@
        // Create and start a connection finalizer thread for this
        // connection handler.
        connectionFinalizer = Executors.newSingleThreadScheduledExecutor(new DirectoryThread.Factory(
        final ScheduledThreadPoolExecutor finalizer = new ScheduledThreadPoolExecutor(1, new DirectoryThread.Factory(
                "LDAP Connection Finalizer for connection handler " + toString()));
        // The first request time limits still pending must not keep the thread, and the server shutdown, waiting.
        finalizer.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
        // A connection which sent its first request, or closed, takes its time limit and itself out of the queue.
        finalizer.setRemoveOnCancelPolicy(true);
        connectionFinalizer = finalizer;
        connectionFinalizerActiveJobQueue = new ArrayList<>();
        connectionFinalizerPendingJobQueue = new ArrayList<>();
@@ -865,7 +876,10 @@
    private void connectionClosed(ClientConnection connection, Collection<ClientConnection> accepted) {
        accepted.remove(connection);
        clientConnections.remove(connection);
        // Several events may report the end of the same connection: only the first gives back its place.
        if (clientConnections.remove(connection)) {
            releasePlaceOfAddress(connection.getRemoteAddress());
        }
        for (TransportDrain drain : drains) {
            drain.connectionClosed();
        }
@@ -936,6 +950,14 @@
                                connectionClosed(conn, accepted);
                            }
                        });
                        // The context reports a close only to the listeners it has at that time: a connection which
                        // ended before this listener was added, for example from a post-connect plugin, was never
                        // reported to it. A close reported twice gives back the place of its address only once.
                        if (!conn.isConnectionValid()) {
                            connectionClosed(conn, accepted);
                        }
                        // Once the listener is in place, so that a connection the time limit closes is reported to it.
                        limitTimeToFirstRequest(conn);
                        return new ReactiveHandler<LDAPClientContext, LdapRequestEnvelope, Stream<Response>>() {
                            @Override
                            public Stream<Response> handle(final LDAPClientContext context,
@@ -1100,39 +1122,111 @@
                            clientConnection.getServerHostPort()));
            throw LdapException.newLdapException(ResultCode.CONSTRAINT_VIOLATION);
        }
        // Check that the address of the client does not hold all the connections it is allowed already.
        final int maxConnectionsPerAddress = currentConfig.getMaxClientConnectionsPerAddress();
        if (!takePlaceOfAddress(clientAddr, maxConnectionsPerAddress)) {
            clientConnection.disconnect(
                    DisconnectReason.ADMIN_LIMIT_EXCEEDED,
                    currentConfig.isSendRejectionNotice(),
                    ERR_CONNHANDLER_CLIENT_ADDRESS_LIMIT_REACHED.get(clientConnection.getClientHostPort(),
                            clientConnection.getServerHostPort(), maxConnectionsPerAddress));
            throw LdapException.newLdapException(ResultCode.ADMIN_LIMIT_EXCEEDED);
        }
        // If we've gotten here, then we'll take the
        // connection so invoke the post-connect plugins and
        // register the client connection with a request
        // handler.
        boolean accepted = false;
        try {
            PluginConfigManager pluginManager = DirectoryServer.getPluginConfigManager();
            PluginResult.PostConnect pluginResult = pluginManager.invokePostConnectPlugins(clientConnection);
            if (!pluginResult.continueProcessing()) {
                clientConnection.disconnect(pluginResult.getDisconnectReason(),
                        pluginResult.sendDisconnectNotification(), pluginResult.getErrorMessage());
                throw LdapException.newLdapException(ResultCode.CONSTRAINT_VIOLATION);
            }
        } catch (Exception e) {
            logger.traceException(e);
            LocalizableMessage message = INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(
                    clientConnection.getClientHostPort(), clientConnection.getServerHostPort(), getExceptionMessage(e));
            logger.debug(message);
            clientConnection.disconnect(DisconnectReason.SERVER_ERROR, currentConfig.isSendRejectionNotice(), message);
            throw LdapException.newLdapException(ResultCode.OPERATIONS_ERROR);
        }
        if (useSSL()) {
            // If we've gotten here, then we'll take the
            // connection so invoke the post-connect plugins and
            // register the client connection with a request
            // handler.
            try {
                clientContext.enableTLS(createSSLEngine(), false);
            } catch (DirectoryException e) {
                throw LdapException.newLdapException(e.getResultCode(), e);
                PluginConfigManager pluginManager = DirectoryServer.getPluginConfigManager();
                PluginResult.PostConnect pluginResult = pluginManager.invokePostConnectPlugins(clientConnection);
                if (!pluginResult.continueProcessing()) {
                    clientConnection.disconnect(pluginResult.getDisconnectReason(),
                            pluginResult.sendDisconnectNotification(), pluginResult.getErrorMessage());
                    throw LdapException.newLdapException(ResultCode.CONSTRAINT_VIOLATION);
                }
            } catch (Exception e) {
                logger.traceException(e);
                LocalizableMessage message = INFO_CONNHANDLER_UNABLE_TO_REGISTER_CLIENT.get(
                        clientConnection.getClientHostPort(), clientConnection.getServerHostPort(),
                        getExceptionMessage(e));
                logger.debug(message);
                clientConnection.disconnect(
                        DisconnectReason.SERVER_ERROR, currentConfig.isSendRejectionNotice(), message);
                throw LdapException.newLdapException(ResultCode.OPERATIONS_ERROR);
            }
            if (useSSL()) {
                try {
                    clientContext.enableTLS(createSSLEngine(), false);
                } catch (DirectoryException e) {
                    throw LdapException.newLdapException(e.getResultCode(), e);
                }
            }
            accepted = true;
            return clientConnection;
        } finally {
            // The connection is not accepted after all, whatever was thrown: it gives back the place it took.
            if (!accepted) {
                releasePlaceOfAddress(clientAddr);
            }
        }
    }
        return clientConnection;
    /**
     * Counts a new connection from the given address, unless the address holds the given number of connections
     * already.
     *
     * @return {@code false} if the connection would exceed the limit, which is then not counted
     */
    private boolean takePlaceOfAddress(final InetAddress address, final int maxConnections) {
        final AtomicBoolean taken = new AtomicBoolean(true);
        connectionsPerAddress.compute(address, (key, held) -> {
            final int count = held != null ? held : 0;
            if (maxConnections > 0 && count >= maxConnections) {
                taken.set(false);
                return held;
            }
            return count + 1;
        });
        return taken.get();
    }
    /** Uncounts a connection from the given address, which no longer appears once it holds none. */
    private void releasePlaceOfAddress(final InetAddress address) {
        if (address != null) {
            connectionsPerAddress.computeIfPresent(address, (key, held) -> held > 1 ? held - 1 : null);
        }
    }
    /**
     * Closes the connection unless a complete request arrives on it within {@code first-request-time-limit}, so that
     * clients which send nothing, or only part of a request, cannot hold connections open.
     */
    private void limitTimeToFirstRequest(final LDAPClientConnection2 connection) {
        final long timeLimit = currentConfig.getFirstRequestTimeLimit();
        if (timeLimit <= 0) {
            return;
        }
        synchronized (connectionFinalizerLock) {
            // Accepted while this handler is finalized, before its listener stops: nothing is left to time the
            // connection. A disabled handler is no longer registered, so idle-time-limit does not reach it either:
            // like every connection the handler leaves open, it lasts until the client closes it or the server stops.
            if (connectionFinalizer == null) {
                return;
            }
            connection.setFirstRequestTimer(connectionFinalizer.schedule(() -> {
                if (!connection.hasReceivedRequest()) {
                    connection.disconnect(DisconnectReason.IDLE_TIME_LIMIT_EXCEEDED, true,
                            INFO_CONNHANDLER_FIRST_REQUEST_TIME_LIMIT_EXCEEDED.get(timeLimit));
                }
            }, timeLimit, TimeUnit.MILLISECONDS));
        }
    }
    /**
opendj-server-legacy/src/main/java/org/opends/server/config/AdministrationConnector.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.config;
@@ -100,6 +101,13 @@
  private static final int ADMIN_MAX_REQUEST_SIZE = 5000000;
  private static final int ADMIN_WRITE_BUFFER_SIZE = 4096;
  private static final int ADMIN_NUM_REQUEST_HANDLERS = 4;
  /** 1mn, the default of an LDAP connection handler. */
  private static final long ADMIN_FIRST_REQUEST_TIME_LIMIT = 60000;
  /**
   * The default of an LDAP connection handler: the only bound on the connections from one address, since
   * max-allowed-client-connections never rejects a connection to the administration connector.
   */
  private static final int ADMIN_MAX_CLIENT_CONNECTIONS_PER_ADDRESS = 1024;
  private static final boolean ADMIN_SEND_REJECTION_NOTICE = true;
  private static final boolean ADMIN_USE_TCP_KEEP_ALIVE = true;
  private static final boolean ADMIN_USE_TCP_NO_DELAY = true;
@@ -289,6 +297,18 @@
    }
    @Override
    public long getFirstRequestTimeLimit()
    {
      return ADMIN_FIRST_REQUEST_TIME_LIMIT;
    }
    @Override
    public int getMaxClientConnectionsPerAddress()
    {
      return ADMIN_MAX_CLIENT_CONNECTIONS_PER_ADDRESS;
    }
    @Override
    public long getBufferSize()
    {
      return ADMIN_WRITE_BUFFER_SIZE;
opendj-server-legacy/src/main/java/org/opends/server/core/DirectoryServer.java
@@ -4492,8 +4492,11 @@
        }
      }
      // The administration connector is exempt, or clients holding every connection would lock out the
      // administrator who has to raise the limit.
      final long maxAllowed = getCoreConfigManager().getMaxAllowedConnections();
      if (0 < maxAllowed && maxAllowed <= directoryServer.currentConnections)
      if (0 < maxAllowed && maxAllowed <= directoryServer.currentConnections
          && !isAdministrationConnection(clientConnection))
      {
        return -1;
      }
@@ -4510,6 +4513,12 @@
    }
  }
  private static boolean isAdministrationConnection(ClientConnection clientConnection)
  {
    final ConnectionHandler<?> handler = clientConnection.getConnectionHandler();
    return handler != null && handler.isAdminConnectionHandler();
  }
  /**
   * Indicates that the specified client connection has been closed.
   *
opendj-server-legacy/src/messages/org/opends/messages/protocol.properties
@@ -870,3 +870,9 @@
 is not corrupted
ERR_HTTP_ERROR_WHILE_PROCESSING_REQUEST_1539=An error occurred while processing the request \
 %s: %s
INFO_CONNHANDLER_FIRST_REQUEST_TIME_LIMIT_EXCEEDED_1542=This connection has \
 been terminated because no request arrived on it within %d milliseconds
ERR_CONNHANDLER_CLIENT_ADDRESS_LIMIT_REACHED_1543=The connection attempt \
 from client %s to %s has been rejected because %d connections from the \
 address of the client are already established, which is the limit of this \
 connection handler
opendj-server-legacy/src/test/java/org/opends/server/protocols/ldap/LDAPConnectionLimitsTestCase.java
New file
@@ -0,0 +1,915 @@
/*
 * 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<Runnable> 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<Void> 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<Socket> 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<Socket> 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<String> 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<String> 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<ClientConnection, PluginResult.PostConnect> postConnect;
    private PostConnectPlugin(LDAPConnectionHandler2 handler,
        Function<ClientConnection, PluginResult.PostConnect> 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<Runnable> 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<Runnable> 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<Runnable> 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<String> 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");
  }
}