From 0b846fb78e9bf21875b8b9b16d07be4b4beeaba0 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 28 Sep 2026 12:53:31 +0000
Subject: [PATCH] [#1109] Keep a connection handler listening when a change to it is rejected (#1110)

---
 opendj-server-legacy/src/test/java/org/opends/server/protocols/RejectedSSLConfigurationChangeTestCase.java |  402 ++++++++++++++++++++++++++++++++++++++++++++
 opendj-server-legacy/src/main/java/org/opends/server/protocols/ldap/LDAPConnectionHandler.java             |   30 ++
 opendj-server-legacy/src/main/java/org/forgerock/opendj/reactive/LDAPConnectionHandler2.java               |   29 ++
 opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java             |   43 +++-
 4 files changed, 477 insertions(+), 27 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/forgerock/opendj/reactive/LDAPConnectionHandler2.java b/opendj-server-legacy/src/main/java/org/forgerock/opendj/reactive/LDAPConnectionHandler2.java
index 482b318..5f40bd1 100644
--- a/opendj-server-legacy/src/main/java/org/forgerock/opendj/reactive/LDAPConnectionHandler2.java
+++ b/opendj-server-legacy/src/main/java/org/forgerock/opendj/reactive/LDAPConnectionHandler2.java
@@ -316,7 +316,7 @@
     private void configureSSL(LDAPConnectionHandlerCfg config) throws DirectoryException {
         protocol = config.isUseSSL() ? "LDAPS" : "LDAP";
         if (config.isUseSSL() || config.isAllowStartTLS()) {
-            sslContext = createSSLContext(config);
+            sslContext = createSSLContext(config, true);
             sslEngine = createSSLEngine(config, sslContext);
         } else {
             sslContext = null;
@@ -581,7 +581,7 @@
         // Check that the SSL configuration is valid.
                 && (config.isUseSSL() || config.isAllowStartTLS())) {
             try {
-                createSSLEngine(config, createSSLContext(config));
+                createSSLEngine(config, createSSLContext(config, false));
             } catch (DirectoryException e) {
                 logger.traceException(e);
 
@@ -937,26 +937,39 @@
         }
     }
 
-    private void disableAndWarnIfUseSSL(LDAPConnectionHandlerCfg config) {
-        if (config.isUseSSL()) {
+    private void disableAndWarnIfUseSSL(LDAPConnectionHandlerCfg config, boolean forUse) {
+        if (forUse && config.isUseSSL()) {
             logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
             enabled = false;
         }
     }
 
-    private SSLContext createSSLContext(LDAPConnectionHandlerCfg config) throws DirectoryException {
+    /**
+     * Creates the SSL context for the provided configuration.
+     *
+     * @param config
+     *            the configuration to create the SSL context for
+     * @param forUse
+     *            {@code true} when the handler is going to use the SSL context, at its start or when a change is
+     *            applied, so that an SSL handler without a usable key is disabled; {@code false} when the SSL context
+     *            only checks a proposed configuration, which must leave the running handler as it is
+     * @return the SSL context
+     * @throws DirectoryException
+     *             if the SSL context cannot be created
+     */
+    private SSLContext createSSLContext(LDAPConnectionHandlerCfg config, boolean forUse) throws DirectoryException {
         try {
             DN keyMgrDN = config.getKeyManagerProviderDN();
             final ServerContext serverContext = DirectoryServer.getInstance().getServerContext();
             KeyManagerProvider<?> keyManagerProvider = serverContext.getKeyManagerProvider(keyMgrDN);
             if (keyManagerProvider == null) {
                 logger.error(ERR_NULL_KEY_PROVIDER_MANAGER, keyMgrDN, friendlyName);
-                disableAndWarnIfUseSSL(config);
+                disableAndWarnIfUseSSL(config, forUse);
                 keyManagerProvider = new NullKeyManagerProvider();
                 // The SSL connection is unusable without a key manager provider
             } else if (!keyManagerProvider.containsAtLeastOneKey()) {
                 logger.error(ERR_INVALID_KEYSTORE, friendlyName);
-                disableAndWarnIfUseSSL(config);
+                disableAndWarnIfUseSSL(config, forUse);
             }
 
             final SortedSet<String> aliases = new TreeSet<>(config.getSSLCertNickname());
@@ -973,7 +986,7 @@
                 }
 
                 if (aliases.isEmpty()) {
-                    disableAndWarnIfUseSSL(config);
+                    disableAndWarnIfUseSSL(config, forUse);
                 }
                 keyManagers = SelectableCertificateKeyManager.wrap(keyManagerProvider.getKeyManagers(), aliases,
                         friendlyName);
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
index 7f9cce5..2b118d5 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java
@@ -283,7 +283,7 @@
     protocol = config.isUseSSL() ? "HTTPS" : "HTTP";
     if (config.isUseSSL())
     {
-      sslEngineConfigurator = createSSLEngineConfigurator(config);
+      sslEngineConfigurator = createSSLEngineConfigurator(config, true);
     }
     else
     {
@@ -497,7 +497,7 @@
     {
       try
       {
-        createSSLEngineConfigurator(config);
+        createSSLEngineConfigurator(config, false);
       }
       catch (DirectoryException e)
       {
@@ -817,7 +817,22 @@
     buffer.append(handlerName);
   }
 
-  private SSLEngineConfigurator createSSLEngineConfigurator(HTTPConnectionHandlerCfg config) throws DirectoryException
+  /**
+   * Creates the SSL engine configurator for the provided configuration.
+   *
+   * @param config
+   *          the configuration to create the SSL engine configurator for
+   * @param forUse
+   *          {@code true} when the handler is going to use the configurator: at its start a handler
+   *          without a usable key is disabled ({@link #applyConfigurationChange} sets {@code enabled}
+   *          from the configuration afterwards); {@code false} when the configurator only checks a
+   *          proposed configuration, which must leave the running handler as it is
+   * @return the SSL engine configurator, or {@code null} if the configuration does not use SSL
+   * @throws DirectoryException
+   *           if the SSL context cannot be created
+   */
+  private SSLEngineConfigurator createSSLEngineConfigurator(HTTPConnectionHandlerCfg config, boolean forUse)
+      throws DirectoryException
   {
     if (!config.isUseSSL())
     {
@@ -826,7 +841,7 @@
 
     try
     {
-      SSLContext sslContext = createSSLContext(config);
+      SSLContext sslContext = createSSLContext(config, forUse);
       SSLEngineConfigurator configurator = new SSLEngineConfigurator(sslContext);
       configurator.setClientMode(false);
 
@@ -874,7 +889,16 @@
     }
   }
 
-  private SSLContext createSSLContext(HTTPConnectionHandlerCfg config) throws Exception
+  private void disableAndWarn(boolean forUse)
+  {
+    if (forUse)
+    {
+      logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
+      enabled = false;
+    }
+  }
+
+  private SSLContext createSSLContext(HTTPConnectionHandlerCfg config, boolean forUse) throws Exception
   {
     if (!config.isUseSSL())
     {
@@ -886,15 +910,13 @@
     if (keyManagerProvider == null)
     {
       logger.error(ERR_NULL_KEY_PROVIDER_MANAGER, keyMgrDN, friendlyName);
-      logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
       keyManagerProvider = new NullKeyManagerProvider();
-      enabled = false;
+      disableAndWarn(forUse);
     }
     else if (!keyManagerProvider.containsAtLeastOneKey())
     {
       logger.error(ERR_INVALID_KEYSTORE, friendlyName);
-      logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
-      enabled = false;
+      disableAndWarn(forUse);
     }
 
     final SortedSet<String> aliases = new TreeSet<>(config.getSSLCertNickname());
@@ -916,8 +938,7 @@
       }
       if (aliases.isEmpty())
       {
-        logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
-        enabled = false;
+        disableAndWarn(forUse);
       }
       keyManagers = SelectableCertificateKeyManager.wrap(keyManagerProvider.getKeyManagers(), aliases, friendlyName);
     }
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/protocols/ldap/LDAPConnectionHandler.java b/opendj-server-legacy/src/main/java/org/opends/server/protocols/ldap/LDAPConnectionHandler.java
index f404bd9..e80cadc 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/protocols/ldap/LDAPConnectionHandler.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/protocols/ldap/LDAPConnectionHandler.java
@@ -331,7 +331,7 @@
     protocol = config.isUseSSL() ? "LDAPS" : "LDAP";
     if (config.isUseSSL() || config.isAllowStartTLS())
     {
-      sslContext = createSSLContext(config);
+      sslContext = createSSLContext(config, true);
       sslEngine = createSSLEngine(config, sslContext);
     }
     else
@@ -717,7 +717,7 @@
     {
       try
       {
-        createSSLEngine(config, createSSLContext(config));
+        createSSLEngine(config, createSSLContext(config, false));
       }
       catch (DirectoryException e)
       {
@@ -1291,16 +1291,30 @@
     }
   }
 
-  private void disableAndWarnIfUseSSL(LDAPConnectionHandlerCfg config)
+  private void disableAndWarnIfUseSSL(LDAPConnectionHandlerCfg config, boolean forUse)
   {
-    if (config.isUseSSL())
+    if (forUse && config.isUseSSL())
     {
       logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
       enabled = false;
     }
   }
 
-  private SSLContext createSSLContext(LDAPConnectionHandlerCfg config)
+  /**
+   * Creates the SSL context for the provided configuration.
+   *
+   * @param config
+   *          the configuration to create the SSL context for
+   * @param forUse
+   *          {@code true} when the handler is going to use the SSL context, at its start or when
+   *          a change is applied, so that an SSL handler without a usable key is disabled;
+   *          {@code false} when the SSL context only checks a proposed configuration, which must
+   *          leave the running handler as it is
+   * @return the SSL context
+   * @throws DirectoryException
+   *           if the SSL context cannot be created
+   */
+  private SSLContext createSSLContext(LDAPConnectionHandlerCfg config, boolean forUse)
       throws DirectoryException
   {
     try
@@ -1311,14 +1325,14 @@
       if (keyManagerProvider == null)
       {
         logger.error(ERR_NULL_KEY_PROVIDER_MANAGER, keyMgrDN, friendlyName);
-        disableAndWarnIfUseSSL(config);
+        disableAndWarnIfUseSSL(config, forUse);
         keyManagerProvider = new NullKeyManagerProvider();
         // The SSL connection is unusable without a key manager provider
       }
       else if (! keyManagerProvider.containsAtLeastOneKey())
       {
         logger.error(ERR_INVALID_KEYSTORE, friendlyName);
-        disableAndWarnIfUseSSL(config);
+        disableAndWarnIfUseSSL(config, forUse);
       }
 
       final SortedSet<String> aliases = new TreeSet<>(config.getSSLCertNickname());
@@ -1341,7 +1355,7 @@
 
         if (aliases.isEmpty())
         {
-          disableAndWarnIfUseSSL(config);
+          disableAndWarnIfUseSSL(config, forUse);
         }
         keyManagers = SelectableCertificateKeyManager.wrap(keyManagerProvider.getKeyManagers(), aliases, friendlyName);
       }
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/protocols/RejectedSSLConfigurationChangeTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/protocols/RejectedSSLConfigurationChangeTestCase.java
new file mode 100644
index 0000000..22ce5c3
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/protocols/RejectedSSLConfigurationChangeTestCase.java
@@ -0,0 +1,402 @@
+/*
+ * 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;
+
+import static org.opends.messages.ProtocolMessages.ERR_CONNHANDLER_SSL_CANNOT_INITIALIZE;
+import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection;
+import static org.opends.server.util.StaticUtils.*;
+import static org.testng.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.ConnectException;
+import java.net.Socket;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.TrustManager;
+
+import org.forgerock.i18n.LocalizableMessage;
+import org.forgerock.opendj.config.Configuration;
+import org.forgerock.opendj.config.server.ConfigChangeResult;
+import org.forgerock.opendj.config.server.ConfigurationChangeListener;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.reactive.LDAPConnectionHandler2;
+import org.forgerock.opendj.server.config.meta.HTTPConnectionHandlerCfgDefn;
+import org.forgerock.opendj.server.config.meta.LDAPConnectionHandlerCfgDefn;
+import org.forgerock.opendj.server.config.server.ConnectionHandlerCfg;
+import org.forgerock.opendj.server.config.server.HTTPConnectionHandlerCfg;
+import org.forgerock.opendj.server.config.server.LDAPConnectionHandlerCfg;
+import org.opends.admin.ads.util.BlindTrustManager;
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.api.ConnectionHandler;
+import org.opends.server.api.KeyManagerProvider;
+import org.opends.server.api.ServerShutdownListener;
+import org.opends.server.core.DeleteOperation;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ServerContext;
+import org.opends.server.extensions.InitializationUtils;
+import org.opends.server.protocols.http.HTTPConnectionHandler;
+import org.opends.server.protocols.ldap.LDAPConnectionHandler;
+import org.opends.server.types.Entry;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * A change to an SSL connection handler that is rejected because its key store cannot be loaded
+ * must leave the running handler as it was: listening, with the SSL context it had. The start of a handler, and a
+ * change applied to an LDAP handler, still disable it when it has no key it can present.
+ */
+@SuppressWarnings("javadoc")
+@Test(groups = { "precommit" }, sequential = true)
+public class RejectedSSLConfigurationChangeTestCase extends DirectoryServerTestCase
+{
+  private static final LocalizableMessage STOP_REASON = LocalizableMessage.raw("Don't need a reason.");
+  private static final DN KEY_MANAGER_DN = DN.valueOf("cn=Rejected Change Keys,cn=Key Manager Providers,cn=config");
+
+  /** How long the handler must keep serving TLS after the rejected change: its thread checks every second. */
+  private static final long KEEPS_SERVING_MS = TimeUnit.SECONDS.toMillis(3);
+  /** How long a handler that has been disabled may take to stop listening: its thread checks every second. */
+  private static final long STOPS_LISTENING_MS = TimeUnit.SECONDS.toMillis(10);
+
+  private enum Kind
+  {
+    LDAP2, LDAP_LEGACY, HTTP
+  }
+
+  private static final class NoChanges<C extends Configuration> implements ConfigurationChangeListener<C>
+  {
+    @Override
+    public boolean isConfigurationChangeAcceptable(C configuration, List<LocalizableMessage> unacceptableReasons)
+    {
+      return true;
+    }
+
+    @Override
+    public ConfigChangeResult applyConfigurationChange(C configuration)
+    {
+      return new ConfigChangeResult();
+    }
+  }
+
+  private File keyStore;
+
+  @BeforeClass
+  public void setUp() throws Exception
+  {
+    TestCaseUtils.startServer();
+    keyStore = File.createTempFile("rejected-change", ".keystore");
+    keyStore.deleteOnExit();
+    restoreKeyStore();
+    TestCaseUtils.addEntry(
+        "dn: " + KEY_MANAGER_DN,
+        "objectClass: top",
+        "objectClass: ds-cfg-key-manager-provider",
+        "objectClass: ds-cfg-file-based-key-manager-provider",
+        "cn: Rejected Change Keys",
+        "ds-cfg-java-class: org.opends.server.extensions.FileBasedKeyManagerProvider",
+        "ds-cfg-enabled: true",
+        "ds-cfg-key-store-type: JKS",
+        "ds-cfg-key-store-file: " + keyStore.getAbsolutePath(),
+        "ds-cfg-key-store-pin: password");
+  }
+
+  @AfterClass
+  public void tearDown() throws Exception
+  {
+    // A handler registering for changes to its configuration also registers a reference to its key
+    // manager provider, which keeps the provider from being deleted and outlives the handler.
+    // Registering for changes to the same entry without a key manager provider drops the reference.
+    final LDAPConnectionHandlerCfg ldap =
+        (LDAPConnectionHandlerCfg) configuration(Kind.LDAP2, 1, null, "5 megabytes", false);
+    final NoChanges<LDAPConnectionHandlerCfg> ldapListener = new NoChanges<>();
+    ldap.addLDAPChangeListener(ldapListener);
+    ldap.removeLDAPChangeListener(ldapListener);
+    final HTTPConnectionHandlerCfg http =
+        (HTTPConnectionHandlerCfg) configuration(Kind.HTTP, 1, null, "5 megabytes", false);
+    final NoChanges<HTTPConnectionHandlerCfg> httpListener = new NoChanges<>();
+    http.addHTTPChangeListener(httpListener);
+    http.removeHTTPChangeListener(httpListener);
+
+    final DeleteOperation delete = getRootConnection().processDelete(KEY_MANAGER_DN);
+    assertEquals(delete.getResultCode(), ResultCode.SUCCESS, String.valueOf(delete.getErrorMessage()));
+    Files.deleteIfExists(keyStore.toPath());
+  }
+
+  @BeforeMethod
+  public void restoreKeyStore() throws IOException
+  {
+    Files.copy(getFileForPath("config/server.keystore").toPath(), keyStore.toPath(),
+        StandardCopyOption.REPLACE_EXISTING);
+  }
+
+  @DataProvider
+  public Object[][] handlers()
+  {
+    return new Object[][] {
+      { Kind.LDAP2, null }, { Kind.LDAP2, "server-cert" },
+      { Kind.LDAP_LEGACY, null }, { Kind.LDAP_LEGACY, "server-cert" },
+      { Kind.HTTP, null }, { Kind.HTTP, "server-cert" },
+    };
+  }
+
+  @Test(dataProvider = "handlers")
+  public void rejectedChangeKeepsTheHandlerListening(Kind kind, String certNickname) throws Exception
+  {
+    final int port = TestCaseUtils.findFreePort();
+    final ConnectionHandler<?> handler = start(kind, configuration(kind, port, certNickname, "5 megabytes", true));
+    try
+    {
+      assertServesTLS(port);
+
+      Files.write(keyStore.toPath(), new byte[] { 1, 2, 3, 4 });
+      final List<LocalizableMessage> reasons = new ArrayList<>();
+      assertFalse(handler.isConfigurationAcceptable(configuration(kind, port, certNickname, "6 megabytes", true), reasons),
+          "a change needing a key store that cannot be loaded was accepted");
+      assertFalse(reasons.isEmpty(), "the change was rejected without a reason");
+      assertEquals(reasons.get(0).ordinal(), ERR_CONNHANDLER_SSL_CANNOT_INITIALIZE.ordinal(), String.valueOf(reasons));
+
+      assertKeepsServingTLS(port);
+    }
+    finally
+    {
+      stop(handler);
+    }
+  }
+
+  @DataProvider
+  public Object[][] kinds()
+  {
+    return new Object[][] { { Kind.LDAP2 }, { Kind.LDAP_LEGACY }, { Kind.HTTP } };
+  }
+
+  /**
+   * A check that finds no key manager provider registered under the configured DN leaves the running handler as it
+   * is. The configuration entry of an enabled provider whose key store could not be loaded at the start of the
+   * server exists, but the provider is not registered.
+   */
+  @Test(dataProvider = "kinds")
+  public void checkWithoutKeyManagerProviderKeepsTheHandlerListening(Kind kind) throws Exception
+  {
+    final int port = TestCaseUtils.findFreePort();
+    final ConnectionHandler<?> handler = start(kind, configuration(kind, port, null, "5 megabytes", true));
+    try
+    {
+      assertServesTLS(port);
+
+      final KeyManagerProvider<?> provider = DirectoryServer.getKeyManagerProvider(KEY_MANAGER_DN);
+      assertNotNull(provider, "the key manager provider of the test is not registered");
+      DirectoryServer.deregisterKeyManagerProvider(KEY_MANAGER_DN);
+      try
+      {
+        // Whether the change is accepted does not matter here: the running handler must not change either way.
+        handler.isConfigurationAcceptable(configuration(kind, port, null, "6 megabytes", true),
+            new ArrayList<LocalizableMessage>());
+      }
+      finally
+      {
+        DirectoryServer.registerKeyManagerProvider(KEY_MANAGER_DN, provider);
+      }
+
+      assertKeepsServingTLS(port);
+    }
+    finally
+    {
+      stop(handler);
+    }
+  }
+
+  /** The start of a handler still disables it when its key store holds no key it can present. */
+  @Test(dataProvider = "kinds")
+  public void handlerWithoutItsCertificateDoesNotListen(Kind kind) throws Exception
+  {
+    final int port = TestCaseUtils.findFreePort();
+    final ConnectionHandler<?> handler = start(kind, configuration(kind, port, "no-such-cert", "5 megabytes", true));
+    try (Socket socket = new Socket("127.0.0.1", port))
+    {
+      fail("the handler listens on port " + port + " without the certificate it is configured to present");
+    }
+    catch (ConnectException expected)
+    {
+      // disabled at its start
+    }
+    finally
+    {
+      stop(handler);
+    }
+  }
+
+  /**
+   * An applied change that leaves an LDAP handler without the certificate it is configured to present still disables
+   * it. The check accepts such a change, so dsconfig reaches this road. Only {@link LDAPConnectionHandler2} is
+   * covered: the legacy handler keeps accepting TCP connections while it is disabled, so a refused connect cannot
+   * tell, and the HTTP handler sets {@code enabled} from the configuration after the SSL context is built.
+   */
+  @SuppressWarnings("unchecked")
+  @Test
+  public void appliedChangeWithoutItsCertificateStopsListening() throws Exception
+  {
+    final int port = TestCaseUtils.findFreePort();
+    final ConnectionHandler<?> handler = start(Kind.LDAP2, configuration(Kind.LDAP2, port, null, "5 megabytes", true));
+    try
+    {
+      assertServesTLS(port);
+
+      final LDAPConnectionHandlerCfg change =
+          (LDAPConnectionHandlerCfg) configuration(Kind.LDAP2, port, "no-such-cert", "5 megabytes", true);
+      final List<LocalizableMessage> reasons = new ArrayList<>();
+      assertTrue(handler.isConfigurationAcceptable(change, reasons), String.valueOf(reasons));
+      ((ConfigurationChangeListener<LDAPConnectionHandlerCfg>) handler).applyConfigurationChange(change);
+
+      final long deadline = System.currentTimeMillis() + STOPS_LISTENING_MS;
+      while (true)
+      {
+        try (Socket socket = new Socket("127.0.0.1", port))
+        {
+          assertTrue(System.currentTimeMillis() < deadline,
+              "the handler still listens on port " + port + " after a change took its certificate away");
+        }
+        catch (ConnectException expected)
+        {
+          break;
+        }
+        Thread.sleep(250);
+      }
+    }
+    finally
+    {
+      stop(handler);
+    }
+  }
+
+  private static ConnectionHandlerCfg configuration(Kind kind, int port, String certNickname, String maxRequestSize,
+      boolean useSSL) throws Exception
+  {
+    final List<String> lines = new ArrayList<>();
+    lines.add("dn: cn=Rejected Change Handler,cn=Connection Handlers,cn=config");
+    lines.add("objectClass: top");
+    lines.add("objectClass: ds-cfg-connection-handler");
+    lines.add("cn: Rejected Change Handler");
+    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-accept-backlog: 128");
+    lines.add("ds-cfg-keep-stats: false");
+    lines.add("ds-cfg-use-tcp-keep-alive: true");
+    lines.add("ds-cfg-use-tcp-no-delay: true");
+    lines.add("ds-cfg-allow-tcp-reuse-address: true");
+    lines.add("ds-cfg-max-request-size: " + maxRequestSize);
+    lines.add("ds-cfg-use-ssl: " + useSSL);
+    lines.add("ds-cfg-ssl-client-auth-policy: disabled");
+    if (useSSL)
+    {
+      lines.add("ds-cfg-key-manager-provider: " + KEY_MANAGER_DN);
+    }
+    if (certNickname != null)
+    {
+      lines.add("ds-cfg-ssl-cert-nickname: " + certNickname);
+    }
+    if (kind == Kind.HTTP)
+    {
+      lines.add("objectClass: ds-cfg-http-connection-handler");
+      lines.add("ds-cfg-java-class: " + HTTPConnectionHandler.class.getName());
+      lines.add("ds-cfg-buffer-size: 4096 bytes");
+      lines.add("ds-cfg-max-blocked-write-time-limit: 2 minutes");
+      final Entry entry = TestCaseUtils.makeEntry(lines.toArray(new String[0]));
+      return InitializationUtils.getConfiguration(HTTPConnectionHandlerCfgDefn.getInstance(), entry);
+    }
+    lines.add("objectClass: ds-cfg-ldap-connection-handler");
+    lines.add("ds-cfg-java-class: "
+        + (kind == Kind.LDAP2 ? LDAPConnectionHandler2.class : LDAPConnectionHandler.class).getName());
+    lines.add("ds-cfg-allow-ldap-v2: false");
+    lines.add("ds-cfg-send-rejection-notice: true");
+    lines.add("ds-cfg-num-request-handlers: 2");
+    lines.add("ds-cfg-allow-start-tls: false");
+    final Entry entry = TestCaseUtils.makeEntry(lines.toArray(new String[0]));
+    return InitializationUtils.getConfiguration(LDAPConnectionHandlerCfgDefn.getInstance(), entry);
+  }
+
+  private static ConnectionHandler<?> start(Kind kind, ConnectionHandlerCfg config) throws Exception
+  {
+    final ServerContext serverContext = DirectoryServer.getInstance().getServerContext();
+    final ConnectionHandler<?> handler;
+    switch (kind)
+    {
+    case LDAP2:
+      final LDAPConnectionHandler2 ldap2 = new LDAPConnectionHandler2();
+      ldap2.initializeConnectionHandler(serverContext, (LDAPConnectionHandlerCfg) config);
+      handler = ldap2;
+      break;
+    case LDAP_LEGACY:
+      final LDAPConnectionHandler legacy = new LDAPConnectionHandler();
+      legacy.initializeConnectionHandler(serverContext, (LDAPConnectionHandlerCfg) config);
+      handler = legacy;
+      break;
+    default:
+      final HTTPConnectionHandler http = new HTTPConnectionHandler();
+      http.initializeConnectionHandler(serverContext, (HTTPConnectionHandlerCfg) config);
+      handler = http;
+      break;
+    }
+    handler.start();
+    return handler;
+  }
+
+  private static void stop(ConnectionHandler<?> handler) throws InterruptedException
+  {
+    ((ServerShutdownListener) handler).processServerShutdown(STOP_REASON);
+    handler.finalizeConnectionHandler(STOP_REASON);
+    handler.join(10000);
+    assertFalse(handler.isAlive(), "the connection handler thread is still running");
+  }
+
+  private static void assertKeepsServingTLS(int port) throws Exception
+  {
+    final long deadline = System.currentTimeMillis() + KEEPS_SERVING_MS;
+    do
+    {
+      assertServesTLS(port);
+      Thread.sleep(250);
+    }
+    while (System.currentTimeMillis() < deadline);
+  }
+
+  private static void assertServesTLS(int port) throws Exception
+  {
+    final SSLContext client = SSLContext.getInstance("TLS");
+    client.init(null, new TrustManager[] { new BlindTrustManager() }, null);
+    try (SSLSocket socket = (SSLSocket) client.getSocketFactory().createSocket("127.0.0.1", port))
+    {
+      socket.setSoTimeout(10000);
+      socket.startHandshake();
+      assertTrue(socket.getSession().getPeerCertificates().length > 0);
+    }
+    catch (IOException e)
+    {
+      fail("the handler does not serve TLS on port " + port + ": " + e, e);
+    }
+  }
+}

--
Gitblit v1.10.0