Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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<TomcatWebServerFactory> sslHostConfigCustomizer() {
return (factory) -> factory.addConnectorCustomizers(new SslHostConfigCustomizer());
}

@Bean
ApplicationListener<WebServerInitializedEvent> sslHostConfigReporter(SslBundles sslBundles) {
return new SslHostConfigReporter(sslBundles);
}

private static void forEachSslHostConfig(Connector connector, Consumer<SSLHostConfig> 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<WebServerInitializedEvent> {

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()));
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<TomcatWebServerFactory> sslHostConfigCustomizer() {
return (factory) -> factory.addConnectorCustomizers(new SslHostConfigCustomizer());
}

@Bean
ApplicationListener<WebServerInitializedEvent> sslHostConfigReporter(SslBundles sslBundles) {
return new SslHostConfigReporter(sslBundles);
}

private static void forEachSslHostConfig(Connector connector, Consumer<SSLHostConfig> 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<WebServerInitializedEvent> {

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()));
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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());
Expand All @@ -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/");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, SslBundle> serverNameSslBundles) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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<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) {
Expand Down
Loading