From 8aeb269492719339d34ec4d00aeab8477a940cfe Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 6 Aug 2026 12:36:18 -0500 Subject: [PATCH] Retain SSL host config customizations when reloading SSL bundles Tomcat's SSL configuration is applied to an `SSLHostConfig` for each host name. When an SSL bundle was updated, a new `SSLHostConfig` was created and used to replace the existing one for that host name, discarding any customizations that had been applied to it (for example by a `TomcatConnectorCustomizer`). Reuse the existing `SSLHostConfig` for the host name when one is present, applying the updated bundle to it rather than replacing it. The existing `SSLHostConfigCertificate` is also reused, as adding a second certificate with an undefined type to an `SSLHostConfig` is rejected by Tomcat. Signed-off-by: Scott Frederick --- .../spring-boot-sni-reactive-app/build.gradle | 1 + .../boot/sni/server/SniServerApplication.java | 10 -- .../TomcatSslHostConfigConfiguration.java | 115 ++++++++++++++++++ .../spring-boot-sni-servlet-app/build.gradle | 1 + .../TomcatSslHostConfigConfiguration.java | 113 +++++++++++++++++ .../boot/sni/SniIntegrationTests.java | 25 ++++ .../boot/tomcat/SslConnectorCustomizer.java | 31 ++++- .../tomcat/SslConnectorCustomizerTests.java | 42 +++++++ .../TomcatServletWebServerFactoryTests.java | 28 +++++ 9 files changed, 353 insertions(+), 13 deletions(-) create mode 100644 integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/TomcatSslHostConfigConfiguration.java create mode 100644 integration-test/spring-boot-sni-integration-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/TomcatSslHostConfigConfiguration.java diff --git a/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/build.gradle b/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/build.gradle index 512abdfa0c0f..56ec2432f60c 100644 --- a/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/build.gradle +++ b/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/build.gradle @@ -46,6 +46,7 @@ configurations { dependencies { compileOnly("org.springframework:spring-webflux") + compileOnly("org.springframework.boot:spring-boot-starter-tomcat") implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) implementation("org.springframework.boot:spring-boot-starter") diff --git a/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java b/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java index 652be2d83cc7..6067af9ce3da 100644 --- a/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java +++ b/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java @@ -16,16 +16,6 @@ package org.springframework.boot.sni.server; -import java.io.File; -import java.io.IOException; -import java.io.UncheckedIOException; -import java.net.JarURLConnection; -import java.net.URL; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/TomcatSslHostConfigConfiguration.java b/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/TomcatSslHostConfigConfiguration.java new file mode 100644 index 000000000000..862c42f1a382 --- /dev/null +++ b/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/TomcatSslHostConfigConfiguration.java @@ -0,0 +1,115 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.sni.server; + +import java.util.function.Consumer; + +import org.apache.catalina.connector.Connector; +import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.tomcat.util.net.SSLHostConfig; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.ssl.SslBundleRegistry; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.tomcat.TomcatConnectorCustomizer; +import org.springframework.boot.tomcat.TomcatWebServer; +import org.springframework.boot.tomcat.TomcatWebServerFactory; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.server.context.WebServerInitializedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Applies a customization to each of Tomcat's {@code SSLHostConfig} instances and reports + * their state once the server is running and again after an SSL bundle has been reloaded. + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(TomcatWebServerFactory.class) +class TomcatSslHostConfigConfiguration { + + private static final int CUSTOMIZED_SESSION_TIMEOUT = 12345; + + @Bean + WebServerFactoryCustomizer sslHostConfigCustomizer() { + return (factory) -> factory.addConnectorCustomizers(new SslHostConfigCustomizer()); + } + + @Bean + ApplicationListener sslHostConfigReporter(SslBundles sslBundles) { + return new SslHostConfigReporter(sslBundles); + } + + private static void forEachSslHostConfig(Connector connector, Consumer action) { + if (connector.getProtocolHandler() instanceof AbstractHttp11Protocol protocol) { + for (SSLHostConfig sslHostConfig : protocol.findSslHostConfigs()) { + action.accept(sslHostConfig); + } + } + } + + static final class SslHostConfigCustomizer implements TomcatConnectorCustomizer { + + @Override + public void customize(Connector connector) { + forEachSslHostConfig(connector, + (sslHostConfig) -> sslHostConfig.setSessionTimeout(CUSTOMIZED_SESSION_TIMEOUT)); + } + + } + + static final class SslHostConfigReporter implements ApplicationListener { + + private final SslBundles sslBundles; + + SslHostConfigReporter(SslBundles sslBundles) { + this.sslBundles = sslBundles; + } + + @Override + public void onApplicationEvent(WebServerInitializedEvent event) { + WebServer webServer = event.getWebServer(); + if (!(webServer instanceof TomcatWebServer tomcatWebServer)) { + return; + } + Connector connector = tomcatWebServer.getTomcat().getConnector(); + report(connector, "start"); + reloadBundles(); + report(connector, "reload"); + } + + private void reloadBundles() { + if (!(this.sslBundles instanceof SslBundleRegistry registry)) { + return; + } + for (String name : new String[] { "default", "alt" }) { + registry.updateBundle(name, this.sslBundles.getBundle(name)); + } + } + + private void report(Connector connector, String phase) { + forEachSslHostConfig(connector, + (sslHostConfig) -> System.out.println(">>>>> on " + phase + ", host=" + + sslHostConfig.getHostName() + ", port=" + connector.getPort() + + ", sessionTimeout=" + sslHostConfig.getSessionTimeout() + + ", certificates.size=" + sslHostConfig.getCertificates().size())); + } + + } + +} diff --git a/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-servlet-app/build.gradle b/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-servlet-app/build.gradle index 785c426eb33a..9149889f1dac 100644 --- a/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-servlet-app/build.gradle +++ b/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-servlet-app/build.gradle @@ -43,6 +43,7 @@ configurations { dependencies { compileOnly("jakarta.servlet:jakarta.servlet-api:6.0.0") + compileOnly("org.springframework.boot:spring-boot-starter-tomcat") implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) implementation("org.springframework.boot:spring-boot-starter-webmvc") { diff --git a/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/TomcatSslHostConfigConfiguration.java b/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/TomcatSslHostConfigConfiguration.java new file mode 100644 index 000000000000..a4af1b871329 --- /dev/null +++ b/integration-test/spring-boot-sni-integration-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/TomcatSslHostConfigConfiguration.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.sni.server; + +import java.util.function.Consumer; + +import org.apache.catalina.connector.Connector; +import org.apache.coyote.http11.AbstractHttp11Protocol; +import org.apache.tomcat.util.net.SSLHostConfig; + +import org.springframework.boot.ssl.SslBundleRegistry; +import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.tomcat.TomcatConnectorCustomizer; +import org.springframework.boot.tomcat.TomcatWebServer; +import org.springframework.boot.tomcat.TomcatWebServerFactory; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.boot.web.server.context.WebServerInitializedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Applies a customization to each of Tomcat's {@code SSLHostConfig} instances and reports + * their state once the server is running and again after an SSL bundle has been reloaded. + */ +@Configuration(proxyBeanMethods = false) +class TomcatSslHostConfigConfiguration { + + private static final int CUSTOMIZED_SESSION_TIMEOUT = 12345; + + @Bean + WebServerFactoryCustomizer sslHostConfigCustomizer() { + return (factory) -> factory.addConnectorCustomizers(new SslHostConfigCustomizer()); + } + + @Bean + ApplicationListener sslHostConfigReporter(SslBundles sslBundles) { + return new SslHostConfigReporter(sslBundles); + } + + private static void forEachSslHostConfig(Connector connector, Consumer action) { + if (connector.getProtocolHandler() instanceof AbstractHttp11Protocol protocol) { + for (SSLHostConfig sslHostConfig : protocol.findSslHostConfigs()) { + action.accept(sslHostConfig); + } + } + } + + static final class SslHostConfigCustomizer implements TomcatConnectorCustomizer { + + @Override + public void customize(Connector connector) { + forEachSslHostConfig(connector, + (sslHostConfig) -> sslHostConfig.setSessionTimeout(CUSTOMIZED_SESSION_TIMEOUT)); + } + + } + + static final class SslHostConfigReporter implements ApplicationListener { + + private final SslBundles sslBundles; + + SslHostConfigReporter(SslBundles sslBundles) { + this.sslBundles = sslBundles; + } + + @Override + public void onApplicationEvent(WebServerInitializedEvent event) { + WebServer webServer = event.getWebServer(); + if (!(webServer instanceof TomcatWebServer tomcatWebServer)) { + return; + } + Connector connector = tomcatWebServer.getTomcat().getConnector(); + report(connector, "start"); + reloadBundles(); + report(connector, "reload"); + } + + private void reloadBundles() { + if (!(this.sslBundles instanceof SslBundleRegistry registry)) { + return; + } + for (String name : new String[] { "default", "alt" }) { + registry.updateBundle(name, this.sslBundles.getBundle(name)); + } + } + + private void report(Connector connector, String phase) { + forEachSslHostConfig(connector, + (sslHostConfig) -> System.out.println(">>>>> on " + phase + ", host=" + + sslHostConfig.getHostName() + ", port=" + connector.getPort() + + ", sessionTimeout=" + sslHostConfig.getSessionTimeout() + + ", certificates.size=" + sslHostConfig.getCertificates().size())); + } + + } + +} diff --git a/integration-test/spring-boot-sni-integration-tests/src/intTest/java/org/springframework/boot/sni/SniIntegrationTests.java b/integration-test/spring-boot-sni-integration-tests/src/intTest/java/org/springframework/boot/sni/SniIntegrationTests.java index e1a77d67fc23..426adc60ca5f 100644 --- a/integration-test/spring-boot-sni-integration-tests/src/intTest/java/org/springframework/boot/sni/SniIntegrationTests.java +++ b/integration-test/spring-boot-sni-integration-tests/src/intTest/java/org/springframework/boot/sni/SniIntegrationTests.java @@ -48,6 +48,10 @@ class SniIntegrationTests { public static final String ALT_SERVER_NAME = "hello-alt.example.com"; + private static final String DEFAULT_SERVER_NAME = "_default_"; + + private static final int CUSTOMIZED_SESSION_TIMEOUT = 12345; + private static final Integer SERVER_PORT = 8443; private static final Network SHARED_NETWORK = Network.newNetwork(); @@ -66,6 +70,9 @@ void home(String webStack, String server) { } String serverLogs = serverContainer.getLogs(); assertThat(serverLogs).contains(SERVER_START_MESSAGES.get(server)); + if ("tomcat".equals(server)) { + assertSslHostConfigCustomizationsRetained(serverContainer); + } try (ApplicationContainer clientContainer = new ClientApplicationContainer()) { clientContainer.start(); Awaitility.await().atMost(Duration.ofSeconds(60)).until(() -> !clientContainer.isRunning()); @@ -78,6 +85,24 @@ void home(String webStack, String server) { } } + private void assertSslHostConfigCustomizationsRetained(ApplicationContainer serverContainer) { + for (String phase : new String[] { "start", "reload" }) { + for (String serverName : new String[] { PRIMARY_SERVER_NAME, ALT_SERVER_NAME, DEFAULT_SERVER_NAME }) { + String expected = ">>>>> on " + phase + ", host=" + serverName + ", port=" + SERVER_PORT + + ", sessionTimeout=" + CUSTOMIZED_SESSION_TIMEOUT + ", certificates.size=1"; + try { + Awaitility.await() + .atMost(Duration.ofSeconds(60)) + .until(() -> serverContainer.getLogs().contains(expected)); + } + catch (ConditionTimeoutException ex) { + assertThat(serverContainer.getLogs()).contains(expected); + throw ex; + } + } + } + } + private void assertServerCalledWithName(String clientLogs, String serverName) { assertThat(clientLogs).contains("Calling server at 'https://" + serverName + ":8443/'") .contains("Hello from https://" + serverName + ":8443/"); diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java index 590d3924c602..9ffc920c9e67 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/SslConnectorCustomizer.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.Set; import org.apache.catalina.connector.Connector; import org.apache.commons.logging.Log; @@ -66,7 +67,13 @@ public void update(@Nullable String serverName, SslBundle updatedSslBundle) { AbstractHttp11Protocol protocol = (AbstractHttp11Protocol) this.connector.getProtocolHandler(); String host = (serverName != null) ? serverName : protocol.getDefaultSSLHostConfigName(); this.logger.debug("SSL Bundle for host " + host + " has been updated, reloading SSL configuration"); - addSslHostConfig(protocol, host, updatedSslBundle); + SSLHostConfig sslHostConfig = findSslHostConfig(protocol, host); + if (sslHostConfig == null) { + addSslHostConfig(protocol, host, updatedSslBundle); + return; + } + applySslBundle(protocol, sslHostConfig, updatedSslBundle); + protocol.addSslHostConfig(sslHostConfig, true); } public void customize(SslBundle sslBundle, Map serverNameSslBundles) { @@ -101,12 +108,21 @@ private void addSslHostConfig(AbstractHttp11Protocol protocol, String serverN protocol.addSslHostConfig(sslHostConfig, true); } + private @Nullable SSLHostConfig findSslHostConfig(AbstractHttp11Protocol protocol, String serverName) { + for (SSLHostConfig candidate : protocol.findSslHostConfigs()) { + if (serverName.equalsIgnoreCase(candidate.getHostName())) { + return candidate; + } + } + return null; + } + private void applySslBundle(AbstractHttp11Protocol protocol, SSLHostConfig sslHostConfig, SslBundle sslBundle) { SslBundleKey key = sslBundle.getKey(); SslStoreBundle stores = sslBundle.getStores(); SslOptions options = sslBundle.getOptions(); sslHostConfig.setSslProtocol(sslBundle.getProtocol()); - SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED); + SSLHostConfigCertificate certificate = getCertificate(sslHostConfig); String keystorePassword = (stores.getKeyStorePassword() != null) ? stores.getKeyStorePassword() : ""; certificate.setCertificateKeystorePassword(keystorePassword); if (key.getPassword() != null) { @@ -115,12 +131,21 @@ private void applySslBundle(AbstractHttp11Protocol protocol, SSLHostConfig ss if (key.getAlias() != null) { certificate.setCertificateKeyAlias(key.getAlias()); } - sslHostConfig.addCertificate(certificate); configureCiphers(options, sslHostConfig); configureSslStores(sslHostConfig, certificate, stores); configureEnabledProtocols(sslHostConfig, options); } + private SSLHostConfigCertificate getCertificate(SSLHostConfig sslHostConfig) { + Set certificates = sslHostConfig.getCertificates(); + if (certificates.size() == 1) { + return certificates.iterator().next(); + } + SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED); + sslHostConfig.addCertificate(certificate); + return certificate; + } + private void configureCiphers(SslOptions options, SSLHostConfig sslHostConfig) { CipherConfiguration cipherConfiguration = CipherConfiguration.from(options); if (cipherConfiguration != null) { diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java index af81da187288..d362b4e1d5ca 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/SslConnectorCustomizerTests.java @@ -22,6 +22,7 @@ import org.apache.catalina.startup.Tomcat; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.apache.coyote.http11.AbstractHttp11Protocol; import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.openssl.ciphers.Cipher; import org.junit.jupiter.api.AfterEach; @@ -157,6 +158,47 @@ void sslEnabledProtocolsConfiguration() throws Exception { assertThat(sslHostConfig.getEnabledProtocols()).containsExactly("TLSv1.2"); } + @Test + @WithPackageResources("test.jks") + void updateRetainsCustomizationsAppliedToSslHostConfig() { + Ssl ssl = new Ssl(); + ssl.setKeyPassword("password"); + ssl.setKeyStore("classpath:test.jks"); + Connector connector = this.tomcat.getConnector(); + AbstractHttp11Protocol protocol = (AbstractHttp11Protocol) connector.getProtocolHandler(); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap()); + SSLHostConfig sslHostConfig = protocol.findSslHostConfigs()[0]; + sslHostConfig.setTruststoreProvider(MockPkcs11SecurityProvider.NAME); + customizer.update(null, WebServerSslBundle.get(ssl)); + assertThat(protocol.findSslHostConfigs()).hasSize(1); + SSLHostConfig updated = protocol.findSslHostConfigs()[0]; + assertThat(updated.getTruststoreProvider()).isEqualTo(MockPkcs11SecurityProvider.NAME); + assertThat(updated.getCertificates()).hasSize(1); + } + + @Test + @WithPackageResources("test.jks") + void updateAppliesUpdatedBundleToExistingSslHostConfig() throws Exception { + Ssl ssl = new Ssl(); + ssl.setKeyPassword("password"); + ssl.setKeyStore("classpath:test.jks"); + ssl.setEnabledProtocols(new String[] { "TLSv1.2" }); + Connector connector = this.tomcat.getConnector(); + AbstractHttp11Protocol protocol = (AbstractHttp11Protocol) connector.getProtocolHandler(); + SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); + customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap()); + this.tomcat.start(); + assertThat(protocol.findSslHostConfigs()[0].getEnabledProtocols()).containsExactly("TLSv1.2"); + Ssl updatedSsl = new Ssl(); + updatedSsl.setKeyPassword("password"); + updatedSsl.setKeyStore("classpath:test.jks"); + updatedSsl.setEnabledProtocols(new String[] { "TLSv1.3" }); + customizer.update(null, WebServerSslBundle.get(updatedSsl)); + SSLHostConfig sslHostConfig = protocol.findSslHostConfigs()[0]; + assertThat(sslHostConfig.getEnabledProtocols()).containsExactly("TLSv1.3"); + } + @Test void customizeWhenSslIsEnabledWithNoKeyStoreAndNotPkcs11ThrowsException() { assertThatIllegalStateException().isThrownBy(() -> { diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java index 906f45b239c2..61da7aae4dc9 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/servlet/TomcatServletWebServerFactoryTests.java @@ -72,6 +72,7 @@ import org.apache.jasper.servlet.JspServlet; import org.apache.tomcat.JarScanFilter; import org.apache.tomcat.JarScanType; +import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.scan.StandardJarScanFilter; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; import org.awaitility.Awaitility; @@ -697,6 +698,33 @@ void shouldUpdateSslWhenReloadingSslBundles() throws Exception { assertThat(verifier.getLastPrincipal()).isEqualTo("CN=2"); } + @Test + @WithPackageResources({ "1.crt", "1.key", "2.crt", "2.key" }) + void shouldRetainSslHostConfigCustomizationsWhenReloadingSslBundles() throws Exception { + TomcatServletWebServerFactory factory = getFactory(); + addTestTxtFile(factory); + DefaultSslBundleRegistry bundles = new DefaultSslBundleRegistry("test", + createPemSslBundle("classpath:1.crt", "classpath:1.key")); + factory.setSslBundles(bundles); + factory.setSsl(Ssl.forBundle("test")); + factory.addConnectorCustomizers((connector) -> { + if (connector.getProtocolHandler() instanceof AbstractHttp11Protocol protocol) { + for (SSLHostConfig sslHostConfig : protocol.findSslHostConfigs()) { + sslHostConfig.setSessionTimeout(12345); + } + } + }); + this.webServer = factory.getWebServer(); + this.webServer.start(); + bundles.updateBundle("test", createPemSslBundle("classpath:2.crt", "classpath:2.key")); + Connector connector = ((TomcatWebServer) this.webServer).getTomcat().getConnector(); + AbstractHttp11Protocol protocol = (AbstractHttp11Protocol) connector.getProtocolHandler(); + assertThat(protocol.findSslHostConfigs()).hasSize(1); + SSLHostConfig sslHostConfig = protocol.findSslHostConfigs()[0]; + assertThat(sslHostConfig.getSessionTimeout()).isEqualTo(12345); + assertThat(sslHostConfig.getCertificates()).hasSize(1); + } + @Test @WithPackageResources("test.jks") void sslWithHttp11Nio2Protocol() throws Exception {