Merge branch '4.1.x'

Closes gh-51458
This commit is contained in:
Phillip Webb
2026-08-25 18:54:13 -07:00
9 changed files with 356 additions and 17 deletions
@@ -17,8 +17,10 @@
package org.springframework.boot.tomcat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.catalina.connector.Connector;
import org.apache.commons.logging.Log;
@@ -64,9 +66,15 @@ public class SslConnectorCustomizer {
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);
String hostName = (serverName != null) ? serverName : protocol.getDefaultSSLHostConfigName();
this.logger.debug("SSL Bundle for host " + hostName + " has been updated, reloading SSL configuration");
SSLHostConfig sslHostConfig = findSslHostConfig(protocol, hostName);
if (sslHostConfig == null) {
addSslHostConfig(protocol, hostName, updatedSslBundle);
return;
}
applySslBundle(protocol, sslHostConfig, updatedSslBundle);
protocol.addSslHostConfig(sslHostConfig, true);
}
public void customize(SslBundle sslBundle, Map<String, SslBundle> serverNameSslBundles) {
@@ -93,20 +101,27 @@ public class SslConnectorCustomizer {
serverNameSslBundles.forEach((serverName, bundle) -> addSslHostConfig(protocol, serverName, bundle));
}
private void addSslHostConfig(AbstractHttp11Protocol<?> protocol, String serverName, SslBundle sslBundle) {
private void addSslHostConfig(AbstractHttp11Protocol<?> protocol, String hostName, SslBundle sslBundle) {
SSLHostConfig sslHostConfig = new SSLHostConfig();
sslHostConfig.setHostName(serverName);
sslHostConfig.setHostName(hostName);
configureSslClientAuth(sslHostConfig);
applySslBundle(protocol, sslHostConfig, sslBundle);
protocol.addSslHostConfig(sslHostConfig, true);
}
private @Nullable SSLHostConfig findSslHostConfig(AbstractHttp11Protocol<?> protocol, String hostName) {
return Arrays.stream(protocol.findSslHostConfigs())
.filter((candidate) -> hostName.equalsIgnoreCase(candidate.getHostName()))
.findFirst()
.orElse(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 +130,21 @@ public class SslConnectorCustomizer {
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<SSLHostConfigCertificate> 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) {
@@ -22,6 +22,7 @@ import org.apache.catalina.connector.Connector;
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 @@ class SslConnectorCustomizerTests {
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(() -> {
@@ -72,6 +72,7 @@ import org.apache.hc.core5.ssl.SSLContextBuilder;
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;
@@ -700,6 +701,33 @@ class TomcatServletWebServerFactoryTests extends AbstractServletWebServerFactory
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 {