Merge branch '3.4.x'

Closes gh-43891
This commit is contained in:
Andy Wilkinson 2025-01-21 12:10:35 +00:00
commit 049fe3eadd
7 changed files with 195 additions and 11 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2012-2024 the original author or authors. * Copyright 2012-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,7 +21,7 @@ import java.util.Map;
import org.apache.catalina.connector.Connector; import org.apache.catalina.connector.Connector;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.coyote.ProtocolHandler; import org.apache.coyote.ProtocolHandler;
import org.apache.coyote.http11.AbstractHttp11JsseProtocol; import org.apache.coyote.http11.AbstractHttp11Protocol;
import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.apache.tomcat.util.net.SSLHostConfigCertificate;
import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type;
@ -58,7 +58,7 @@ class SslConnectorCustomizer {
} }
void update(String serverName, SslBundle updatedSslBundle) { void update(String serverName, SslBundle updatedSslBundle) {
AbstractHttp11JsseProtocol<?> protocol = (AbstractHttp11JsseProtocol<?>) this.connector.getProtocolHandler(); AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) this.connector.getProtocolHandler();
String host = (serverName != null) ? serverName : protocol.getDefaultSSLHostConfigName(); String host = (serverName != null) ? serverName : protocol.getDefaultSSLHostConfigName();
this.logger.debug("SSL Bundle for host " + host + " has been updated, reloading SSL configuration"); this.logger.debug("SSL Bundle for host " + host + " has been updated, reloading SSL configuration");
addSslHostConfig(protocol, host, updatedSslBundle); addSslHostConfig(protocol, host, updatedSslBundle);
@ -66,20 +66,20 @@ class SslConnectorCustomizer {
void customize(SslBundle sslBundle, Map<String, SslBundle> serverNameSslBundles) { void customize(SslBundle sslBundle, Map<String, SslBundle> serverNameSslBundles) {
ProtocolHandler handler = this.connector.getProtocolHandler(); ProtocolHandler handler = this.connector.getProtocolHandler();
Assert.state(handler instanceof AbstractHttp11JsseProtocol, Assert.state(handler instanceof AbstractHttp11Protocol,
"To use SSL, the connector's protocol handler must be an AbstractHttp11JsseProtocol subclass"); "To use SSL, the connector's protocol handler must be an AbstractHttp11Protocol subclass");
configureSsl((AbstractHttp11JsseProtocol<?>) handler, sslBundle, serverNameSslBundles); configureSsl((AbstractHttp11Protocol<?>) handler, sslBundle, serverNameSslBundles);
this.connector.setScheme("https"); this.connector.setScheme("https");
this.connector.setSecure(true); this.connector.setSecure(true);
} }
/** /**
* Configure Tomcat's {@link AbstractHttp11JsseProtocol} for SSL. * Configure Tomcat's {@link AbstractHttp11Protocol} for SSL.
* @param protocol the protocol * @param protocol the protocol
* @param sslBundle the SSL bundle * @param sslBundle the SSL bundle
* @param serverNameSslBundles the SSL bundles for specific SNI host names * @param serverNameSslBundles the SSL bundles for specific SNI host names
*/ */
private void configureSsl(AbstractHttp11JsseProtocol<?> protocol, SslBundle sslBundle, private void configureSsl(AbstractHttp11Protocol<?> protocol, SslBundle sslBundle,
Map<String, SslBundle> serverNameSslBundles) { Map<String, SslBundle> serverNameSslBundles) {
protocol.setSSLEnabled(true); protocol.setSSLEnabled(true);
if (sslBundle != null) { if (sslBundle != null) {
@ -88,7 +88,7 @@ class SslConnectorCustomizer {
serverNameSslBundles.forEach((serverName, bundle) -> addSslHostConfig(protocol, serverName, bundle)); serverNameSslBundles.forEach((serverName, bundle) -> addSslHostConfig(protocol, serverName, bundle));
} }
private void addSslHostConfig(AbstractHttp11JsseProtocol<?> protocol, String serverName, SslBundle sslBundle) { private void addSslHostConfig(AbstractHttp11Protocol<?> protocol, String serverName, SslBundle sslBundle) {
SSLHostConfig sslHostConfig = new SSLHostConfig(); SSLHostConfig sslHostConfig = new SSLHostConfig();
sslHostConfig.setHostName(serverName); sslHostConfig.setHostName(serverName);
configureSslClientAuth(sslHostConfig); configureSslClientAuth(sslHostConfig);
@ -96,8 +96,7 @@ class SslConnectorCustomizer {
protocol.addSslHostConfig(sslHostConfig, true); protocol.addSslHostConfig(sslHostConfig, true);
} }
private void applySslBundle(AbstractHttp11JsseProtocol<?> protocol, SSLHostConfig sslHostConfig, private void applySslBundle(AbstractHttp11Protocol<?> protocol, SSLHostConfig sslHostConfig, SslBundle sslBundle) {
SslBundle sslBundle) {
SslBundleKey key = sslBundle.getKey(); SslBundleKey key = sslBundle.getKey();
SslStoreBundle stores = sslBundle.getStores(); SslStoreBundle stores = sslBundle.getStores();
SslOptions options = sslBundle.getOptions(); SslOptions options = sslBundle.getOptions();

View File

@ -0,0 +1,21 @@
plugins {
id "java"
}
description = "Spring Boot Tomcat 11 SSL smoke test"
configurations.all {
resolutionStrategy.eachDependency {
if (it.requested.group == 'org.apache.tomcat' || it.requested.group == 'org.apache.tomcat.embed') {
it.useVersion '11.0.0'
}
}
}
dependencies {
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web"))
implementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-actuator"))
testImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test"))
testImplementation("org.apache.httpcomponents.client5:httpclient5")
}

View File

@ -0,0 +1,29 @@
/*
* Copyright 2012-2025 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 smoketest.tomcat.ssl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SampleTomcat11SslApplication {
public static void main(String[] args) {
SpringApplication.run(SampleTomcat11SslApplication.class, args);
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 2012-2025 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 smoketest.tomcat.ssl.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class SampleController {
@GetMapping("/")
public String helloWorld() {
return "Hello, world";
}
}

View File

@ -0,0 +1,13 @@
server.port=8443
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
management.health.ssl.certificate-validity-warning-threshold=7d
management.health.ssl.enabled=true
management.info.ssl.enabled=true
server.ssl.bundle=ssldemo
spring.ssl.bundle.jks.ssldemo.keystore.location=classpath:sample.jks
spring.ssl.bundle.jks.ssldemo.keystore.password=secret
spring.ssl.bundle.jks.ssldemo.keystore.type=JKS
spring.ssl.bundle.jks.ssldemo.key.password=password

View File

@ -0,0 +1,92 @@
/*
* Copyright 2012-2025 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 smoketest.tomcat.ssl;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.json.JsonContent;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class SampleTomcat11SslApplicationTests {
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private AbstractConfigurableWebServerFactory webServerFactory;
@Test
void testSsl() {
assertThat(this.webServerFactory.getSsl().isEnabled()).isTrue();
}
@Test
void testHome() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("Hello, world");
}
@Test
void testSslInfo() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/actuator/info", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
JsonContent body = new JsonContent(entity.getBody());
assertThat(body).extractingPath("ssl.bundles[0].name").isEqualTo("ssldemo");
assertThat(body).extractingPath("ssl.bundles[0].certificateChains[0].alias")
.isEqualTo("spring-boot-ssl-sample");
assertThat(body).extractingPath("ssl.bundles[0].certificateChains[0].certificates[0].issuer")
.isEqualTo("CN=localhost,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown");
assertThat(body).extractingPath("ssl.bundles[0].certificateChains[0].certificates[0].subject")
.isEqualTo("CN=localhost,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown");
assertThat(body).extractingPath("ssl.bundles[0].certificateChains[0].certificates[0].validity.status")
.isEqualTo("EXPIRED");
assertThat(body).extractingPath("ssl.bundles[0].certificateChains[0].certificates[0].validity.message")
.asString()
.startsWith("Not valid after ");
}
@Test
void testSslHealth() {
ResponseEntity<String> entity = this.restTemplate.getForEntity("/actuator/health", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
JsonContent body = new JsonContent(entity.getBody());
assertThat(body).extractingPath("status").isEqualTo("OUT_OF_SERVICE");
assertThat(body).extractingPath("components.ssl.status").isEqualTo("OUT_OF_SERVICE");
assertThat(body).extractingPath("components.ssl.details.invalidChains[0].alias")
.isEqualTo("spring-boot-ssl-sample");
assertThat(body).extractingPath("components.ssl.details.invalidChains[0].certificates[0].issuer")
.isEqualTo("CN=localhost,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown");
assertThat(body).extractingPath("components.ssl.details.invalidChains[0].certificates[0].subject")
.isEqualTo("CN=localhost,OU=Unknown,O=Unknown,L=Unknown,ST=Unknown,C=Unknown");
assertThat(body).extractingPath("components.ssl.details.invalidChains[0].certificates[0].validity.status")
.isEqualTo("EXPIRED");
assertThat(body).extractingPath("components.ssl.details.invalidChains[0].certificates[0].validity.message")
.asString()
.startsWith("Not valid after ");
}
}