From 5ced345128dde870c1087f7a398608e860072238 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 28 Sep 2026 13:19:19 +0000
Subject: [PATCH] [#1111] Refuse an HTTPS configuration without a key the handler can present, and leave a running HTTP handler as it is when one is applied (#1114)

---
 opendj-server-legacy/src/main/java/org/opends/server/protocols/http/HTTPConnectionHandler.java |  127 +++++++++++++++++++++++++++++-------------
 1 files changed, 88 insertions(+), 39 deletions(-)

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 2b118d5..9f94105 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
@@ -24,6 +24,7 @@
 
 import java.io.IOException;
 import java.net.InetAddress;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
@@ -211,9 +212,10 @@
     }
 
     // Reconfigure SSL if needed.
+    final List<LocalizableMessage> noKeyReasons;
     try
     {
-      configureSSL(config);
+      noKeyReasons = configureSSL(config);
     }
     catch (DirectoryException e)
     {
@@ -222,6 +224,20 @@
       ccr.addMessage(e.getMessageObject());
       return ccr;
     }
+    if (!noKeyReasons.isEmpty())
+    {
+      // The check refuses such a configuration, but the key store may have changed since.
+      // The SSL settings take effect only when the handler restarts, and it will not start with these.
+      for (LocalizableMessage reason : noKeyReasons)
+      {
+        logger.error(reason);
+        ccr.addMessage(reason);
+      }
+      final LocalizableMessage warning = WARN_CONNHANDLER_NO_KEY_UNTIL_RESTART.get(friendlyName);
+      logger.warn(warning);
+      ccr.addMessage(warning);
+      ccr.setAdminActionRequired(true);
+    }
 
     if (config.isEnabled() && this.currentConfig.isEnabled() && isListening())
     {
@@ -242,7 +258,9 @@
 
     this.initConfig = config;
     this.currentConfig = config;
-    this.enabled = this.currentConfig.isEnabled();
+    // Without a key it can present, a running handler keeps serving with the SSL settings it started with,
+    // and a handler that is down does not start.
+    this.enabled = config.isEnabled() && (noKeyReasons.isEmpty() || isListening());
 
     return ccr;
   }
@@ -277,18 +295,22 @@
     return b1 == b2;
   }
 
-  private void configureSSL(HTTPConnectionHandlerCfg config)
+  /**
+   * Sets the SSL engine configurator that the handler starts its HTTP server with.
+   *
+   * @param config
+   *          the configuration to take the SSL settings from
+   * @return why the configuration leaves the handler without a key it can present, empty when it has one
+   * @throws DirectoryException
+   *           if the SSL context cannot be created
+   */
+  private List<LocalizableMessage> configureSSL(HTTPConnectionHandlerCfg config)
       throws DirectoryException
   {
     protocol = config.isUseSSL() ? "HTTPS" : "HTTP";
-    if (config.isUseSSL())
-    {
-      sslEngineConfigurator = createSSLEngineConfigurator(config, true);
-    }
-    else
-    {
-      sslEngineConfigurator = null;
-    }
+    final List<LocalizableMessage> noKeyReasons = new ArrayList<>();
+    sslEngineConfigurator = createSSLEngineConfigurator(config, noKeyReasons);
+    return noKeyReasons;
   }
 
   @Override
@@ -435,16 +457,26 @@
     handlerName = getHandlerName(config);
 
     // Configure SSL if needed.
+    final List<LocalizableMessage> noKeyReasons;
     try
     {
-      // This call may disable the connector if wrong SSL settings
-      configureSSL(config);
+      noKeyReasons = configureSSL(config);
     }
     catch (DirectoryException e)
     {
       logger.traceException(e);
       throw new InitializationException(e.getMessageObject());
     }
+    if (!noKeyReasons.isEmpty())
+    {
+      // A handler without a key it can present does not start.
+      for (LocalizableMessage reason : noKeyReasons)
+      {
+        logger.error(reason);
+      }
+      logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
+      this.enabled = false;
+    }
 
     // Create and register monitors.
     statTracker = new HTTPStatistics(handlerName + " Statistics");
@@ -495,9 +527,10 @@
 
     if (config.isEnabled() && config.isUseSSL())
     {
+      final List<LocalizableMessage> noKeyReasons = new ArrayList<>();
       try
       {
-        createSSLEngineConfigurator(config, false);
+        createSSLEngineConfigurator(config, noKeyReasons);
       }
       catch (DirectoryException e)
       {
@@ -505,6 +538,12 @@
         unacceptableReasons.add(e.getMessageObject());
         return false;
       }
+      if (!noKeyReasons.isEmpty())
+      {
+        // The SSL settings take effect only when the handler restarts, and it would not start with these.
+        unacceptableReasons.addAll(noKeyReasons);
+        return false;
+      }
     }
 
     return true;
@@ -822,17 +861,15 @@
    *
    * @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
+   * @param noKeyReasons
+   *          receives why the configuration leaves the handler without a key it can present; the
+   *          caller decides what that means for the handler, which this method leaves 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
+  private SSLEngineConfigurator createSSLEngineConfigurator(HTTPConnectionHandlerCfg config,
+      List<LocalizableMessage> noKeyReasons) throws DirectoryException
   {
     if (!config.isUseSSL())
     {
@@ -841,7 +878,7 @@
 
     try
     {
-      SSLContext sslContext = createSSLContext(config, forUse);
+      SSLContext sslContext = createSSLContext(config, noKeyReasons);
       SSLEngineConfigurator configurator = new SSLEngineConfigurator(sslContext);
       configurator.setClientMode(false);
 
@@ -889,16 +926,20 @@
     }
   }
 
-  private void disableAndWarn(boolean forUse)
-  {
-    if (forUse)
-    {
-      logger.warn(INFO_DISABLE_CONNECTION, friendlyName);
-      enabled = false;
-    }
-  }
-
-  private SSLContext createSSLContext(HTTPConnectionHandlerCfg config, boolean forUse) throws Exception
+  /**
+   * Creates the SSL context for the provided configuration. The configuration is named by its own name
+   * rather than by {@code friendlyName}, which is not set yet when the check runs on a new instance.
+   *
+   * @param config
+   *          the configuration to create the SSL context for
+   * @param noKeyReasons
+   *          receives why the configuration leaves the handler without a key it can present
+   * @return the SSL context, or {@code null} if the configuration does not use SSL
+   * @throws Exception
+   *           if the SSL context cannot be created
+   */
+  private SSLContext createSSLContext(HTTPConnectionHandlerCfg config, List<LocalizableMessage> noKeyReasons)
+      throws Exception
   {
     if (!config.isUseSSL())
     {
@@ -909,14 +950,12 @@
     KeyManagerProvider<?> keyManagerProvider = serverContext.getKeyManagerProvider(keyMgrDN);
     if (keyManagerProvider == null)
     {
-      logger.error(ERR_NULL_KEY_PROVIDER_MANAGER, keyMgrDN, friendlyName);
+      noKeyReasons.add(ERR_NULL_KEY_PROVIDER_MANAGER.get(keyMgrDN, config.name()));
       keyManagerProvider = new NullKeyManagerProvider();
-      disableAndWarn(forUse);
     }
     else if (!keyManagerProvider.containsAtLeastOneKey())
     {
-      logger.error(ERR_INVALID_KEYSTORE, friendlyName);
-      disableAndWarn(forUse);
+      noKeyReasons.add(ERR_INVALID_KEYSTORE.get(config.name()));
     }
 
     final SortedSet<String> aliases = new TreeSet<>(config.getSSLCertNickname());
@@ -927,18 +966,28 @@
     }
     else
     {
+      final List<LocalizableMessage> missingAliases = new ArrayList<>();
       final Iterator<String> it = aliases.iterator();
       while (it.hasNext())
       {
-        if (!keyManagerProvider.containsKeyWithAlias(it.next()))
+        final String alias = it.next();
+        if (!keyManagerProvider.containsKeyWithAlias(alias))
         {
-          logger.error(ERR_KEYSTORE_DOES_NOT_CONTAIN_ALIAS, aliases, friendlyName);
+          missingAliases.add(ERR_KEYSTORE_DOES_NOT_CONTAIN_ALIAS.get(alias, config.name()));
           it.remove();
         }
       }
       if (aliases.isEmpty())
       {
-        disableAndWarn(forUse);
+        noKeyReasons.addAll(missingAliases);
+      }
+      else
+      {
+        // One of the other aliases is there, so the handler still has a key it can present.
+        for (LocalizableMessage missingAlias : missingAliases)
+        {
+          logger.error(missingAlias);
+        }
       }
       keyManagers = SelectableCertificateKeyManager.wrap(keyManagerProvider.getKeyManagers(), aliases, friendlyName);
     }

--
Gitblit v1.10.0