diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java index ea49d05dc75..cadb12d4242 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizer.java @@ -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"); * 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.commons.logging.Log; 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.SSLHostConfigCertificate; import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; @@ -58,7 +58,7 @@ class SslConnectorCustomizer { } 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(); this.logger.debug("SSL Bundle for host " + host + " has been updated, reloading SSL configuration"); addSslHostConfig(protocol, host, updatedSslBundle); @@ -66,20 +66,20 @@ class SslConnectorCustomizer { void customize(SslBundle sslBundle, Map serverNameSslBundles) { ProtocolHandler handler = this.connector.getProtocolHandler(); - Assert.state(handler instanceof AbstractHttp11JsseProtocol, - "To use SSL, the connector's protocol handler must be an AbstractHttp11JsseProtocol subclass"); - configureSsl((AbstractHttp11JsseProtocol) handler, sslBundle, serverNameSslBundles); + Assert.state(handler instanceof AbstractHttp11Protocol, + "To use SSL, the connector's protocol handler must be an AbstractHttp11Protocol subclass"); + configureSsl((AbstractHttp11Protocol) handler, sslBundle, serverNameSslBundles); this.connector.setScheme("https"); this.connector.setSecure(true); } /** - * Configure Tomcat's {@link AbstractHttp11JsseProtocol} for SSL. + * Configure Tomcat's {@link AbstractHttp11Protocol} for SSL. * @param protocol the protocol * @param sslBundle the SSL bundle * @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 serverNameSslBundles) { protocol.setSSLEnabled(true); if (sslBundle != null) { @@ -88,7 +88,7 @@ class SslConnectorCustomizer { 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.setHostName(serverName); configureSslClientAuth(sslHostConfig); @@ -96,8 +96,7 @@ class SslConnectorCustomizer { protocol.addSslHostConfig(sslHostConfig, true); } - private void applySslBundle(AbstractHttp11JsseProtocol protocol, SSLHostConfig sslHostConfig, - SslBundle sslBundle) { + private void applySslBundle(AbstractHttp11Protocol protocol, SSLHostConfig sslHostConfig, SslBundle sslBundle) { SslBundleKey key = sslBundle.getKey(); SslStoreBundle stores = sslBundle.getStores(); SslOptions options = sslBundle.getOptions(); diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/build.gradle new file mode 100644 index 00000000000..7744f64216a --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/build.gradle @@ -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") +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/java/smoketest/tomcat/ssl/SampleTomcat11SslApplication.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/java/smoketest/tomcat/ssl/SampleTomcat11SslApplication.java new file mode 100644 index 00000000000..26a66f7146e --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/java/smoketest/tomcat/ssl/SampleTomcat11SslApplication.java @@ -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); + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/java/smoketest/tomcat/ssl/web/SampleController.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/java/smoketest/tomcat/ssl/web/SampleController.java new file mode 100644 index 00000000000..56f8ba7d702 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/java/smoketest/tomcat/ssl/web/SampleController.java @@ -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"; + } + +} diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/resources/application.properties b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/resources/application.properties new file mode 100644 index 00000000000..c9f855cfcee --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/resources/application.properties @@ -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 diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/resources/sample.jks b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/resources/sample.jks new file mode 100644 index 00000000000..6aa9a28053a Binary files /dev/null and b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/main/resources/sample.jks differ diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/test/java/smoketest/tomcat/ssl/SampleTomcat11SslApplicationTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/test/java/smoketest/tomcat/ssl/SampleTomcat11SslApplicationTests.java new file mode 100644 index 00000000000..0c671c68455 --- /dev/null +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-tomcat11-ssl/src/test/java/smoketest/tomcat/ssl/SampleTomcat11SslApplicationTests.java @@ -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 entity = this.restTemplate.getForEntity("/", String.class); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(entity.getBody()).isEqualTo("Hello, world"); + } + + @Test + void testSslInfo() { + ResponseEntity 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 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 "); + } + +}