From ad79c373f827c6a27fd6261577279b0ac832ffc7 Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Thu, 28 Mar 2024 14:12:20 -0500 Subject: [PATCH] Add SNI support to web server SSL auto-configuration Properties under `server.ssl.server-name-bundles` and `management.server.ssl.server-name-bundles` can be used to configure mappings of host names to SSL bundles to support SNI in embedded web servers. Closes gh-26022 --- settings.gradle | 1 + ...itional-spring-configuration-metadata.json | 4 + ...itional-spring-configuration-metadata.json | 8 ++ .../modules/how-to/pages/webserver.adoc | 29 +++- .../netty/NettyRSocketServerFactory.java | 21 ++- .../jetty/JettyServletWebServerFactory.java | 3 + .../netty/NettyReactiveWebServerFactory.java | 21 ++- .../embedded/netty/SslServerCustomizer.java | 36 ++++- .../tomcat/SslConnectorCustomizer.java | 37 +++-- .../TomcatReactiveWebServerFactory.java | 14 +- .../tomcat/TomcatServletWebServerFactory.java | 14 +- .../undertow/SslBuilderCustomizer.java | 22 ++- .../UndertowReactiveWebServerFactory.java | 5 +- .../UndertowServletWebServerFactory.java | 5 +- .../UndertowWebServerFactoryDelegate.java | 10 +- .../AbstractConfigurableWebServerFactory.java | 10 ++ .../springframework/boot/web/server/Ssl.java | 22 ++- .../boot/web/server/WebServerSslBundle.java | 3 +- .../JettyServletWebServerFactoryTests.java | 16 +++ .../tomcat/SslConnectorCustomizerTests.java | 15 +- .../spring-boot-sni-tests/build.gradle | 86 ++++++++++++ .../spring-boot-sni-tests/create-certs.sh | 72 ++++++++++ .../spring-boot-sni-client-app/build.gradle | 17 +++ .../settings.gradle | 15 ++ .../boot/sni/client/SniClientApplication.java | 81 +++++++++++ .../src/main/resources/application.yml | 7 + .../src/main/resources/ca/test-ca.crt | 32 +++++ .../spring-boot-sni-reactive-app/build.gradle | 55 ++++++++ .../settings.gradle | 15 ++ .../boot/sni/server/HelloController.java | 31 +++++ .../boot/sni/server/SniServerApplication.java | 39 ++++++ .../resources/alt/test-hello-alt-server.crt | 26 ++++ .../resources/alt/test-hello-alt-server.key | 28 ++++ .../src/main/resources/application.yml | 42 ++++++ .../src/main/resources/ca/test-ca.crt | 32 +++++ .../resources/default/test-hello-server.crt | 26 ++++ .../resources/default/test-hello-server.key | 28 ++++ .../spring-boot-sni-servlet-app/build.gradle | 52 +++++++ .../settings.gradle | 15 ++ .../boot/sni/server/HelloController.java | 32 +++++ .../boot/sni/server/SniServerApplication.java | 39 ++++++ .../resources/alt/test-hello-alt-server.crt | 26 ++++ .../resources/alt/test-hello-alt-server.key | 28 ++++ .../src/main/resources/application.yml | 42 ++++++ .../src/main/resources/ca/test-ca.crt | 32 +++++ .../resources/default/test-hello-server.crt | 26 ++++ .../resources/default/test-hello-server.key | 28 ++++ .../boot/sni/SniIntegrationTests.java | 129 ++++++++++++++++++ 48 files changed, 1322 insertions(+), 55 deletions(-) create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/build.gradle create mode 100755 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/create-certs.sh create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/build.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/settings.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/java/org/springframework/boot/sni/client/SniClientApplication.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/resources/application.yml create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/resources/ca/test-ca.crt create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/build.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/settings.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/HelloController.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/alt/test-hello-alt-server.crt create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/alt/test-hello-alt-server.key create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/application.yml create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/ca/test-ca.crt create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/default/test-hello-server.crt create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/default/test-hello-server.key create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/build.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/settings.gradle create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/HelloController.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/alt/test-hello-alt-server.crt create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/alt/test-hello-alt-server.key create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/application.yml create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/ca/test-ca.crt create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/default/test-hello-server.crt create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/default/test-hello-server.key create mode 100644 spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/src/intTest/java/org/springframework/boot/sni/SniIntegrationTests.java diff --git a/settings.gradle b/settings.gradle index e47249b204a..f1c20b151c7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -79,6 +79,7 @@ include "spring-boot-tests:spring-boot-integration-tests:spring-boot-launch-scri include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-loader-classic-tests" include "spring-boot-tests:spring-boot-integration-tests:spring-boot-server-tests" +include "spring-boot-tests:spring-boot-integration-tests:spring-boot-sni-tests" include "spring-boot-system-tests:spring-boot-deployment-tests" include "spring-boot-system-tests:spring-boot-image-tests" diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index bc5ef5d3cbd..4bc097ee87a 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -2173,6 +2173,10 @@ "description": "SSL protocol to use.", "defaultValue": "TLS" }, + { + "name": "management.server.ssl.server-name-bundles", + "description": "Mapping of host names to SSL bundles for SNI configuration." + }, { "name": "management.server.ssl.trust-certificate", "description": "Path to a PEM-encoded SSL certificate authority file." diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 26d9e468e25..cf007d7f291 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -337,6 +337,10 @@ "description": "SSL protocol to use.", "defaultValue": "TLS" }, + { + "name": "server.ssl.server-name-bundles", + "description": "Mapping of host names to SSL bundles for SNI configuration." + }, { "name": "server.ssl.trust-certificate", "description": "Path to a PEM-encoded SSL certificate authority file." @@ -2676,6 +2680,10 @@ "description": "SSL protocol to use.", "defaultValue": "TLS" }, + { + "name": "spring.rsocket.server.ssl.server-name-bundles", + "description": "Mapping of host names to SSL bundles for SNI configuration." + }, { "name": "spring.rsocket.server.ssl.trust-certificate", "description": "Path to a PEM-encoded SSL certificate authority file." diff --git a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc index a1fc1a6469d..68dc370ce7a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc @@ -148,6 +148,8 @@ You can configure this behavior by setting the configprop:server.compression.mim == Configure SSL SSL can be configured declaratively by setting the various `+server.ssl.*+` properties, typically in `application.properties` or `application.yaml`. +See xref:api:java/org/springframework/boot/web/server/Ssl.html[`Ssl`] for details of all of the supported properties. + The following example shows setting SSL properties using a Java KeyStore file: [configprops,yaml] @@ -193,6 +195,9 @@ server: trust-certificate: "classpath:ca-cert.crt" ---- +[[howto.webserver.configure-ssl.bundles]] +=== Using SSL Bundles + Alternatively, the SSL trust material can be configured in an xref:reference:features/ssl.adoc[SSL bundle] and applied to the web server as shown in this example: [configprops,yaml] @@ -205,7 +210,29 @@ server: NOTE: The `server.ssl.bundle` property can not be combined with the discrete Java KeyStore or PEM property options under `server.ssl`. -See xref:api:java/org/springframework/boot/web/server/Ssl.html[`Ssl`] for details of all of the supported properties. +[[howto.webserver.configure-ssl.sni]] +=== Configure Server Name Indication + +Tomcat, Netty, and Undertow can be configured to use unique SSL trust material for individual host names to support Server Name Indication (SNI). +SNI configuration is not supported with Jetty, but Jetty can https://eclipse.dev/jetty/documentation/jetty-12/operations-guide/index.html#og-protocols-ssl-sni[automatically set up SNI] if multiple certificates are provided to it. + +Assuming xref:reference:features/ssl.adoc[SSL bundles] named `web`, `web-alt1`, and `web-alt2` have been configured, the following configuration can be used to assign each bundle to a host name served by the embedded web server: + +[configprops,yaml] +---- +server: + port: 8443 + ssl: + bundle: "web" + server-name-bundles: + - server-name: "alt1.example.com" + bundle: "web-alt1" + - server-name: "alt2.example.com" + bundle: "web-alt2" +---- + +The bundle specified with `server.ssl.bundle` will be used for the default host, and for any client that does support SNI. +This default bundle must be configured if any `server.ssl.server-name-bundles` are configured. diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java index 06840ac2ee4..62748e08703 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/rsocket/netty/NettyRSocketServerFactory.java @@ -23,6 +23,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import io.rsocket.SocketAcceptor; import io.rsocket.transport.ServerTransport; @@ -43,6 +45,7 @@ import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; import org.springframework.boot.web.embedded.netty.SslServerCustomizer; import org.springframework.boot.web.server.Ssl; +import org.springframework.boot.web.server.Ssl.ClientAuth; import org.springframework.boot.web.server.WebServerSslBundle; import org.springframework.http.client.ReactorResourceFactory; import org.springframework.util.Assert; @@ -181,7 +184,8 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur } private HttpServer customizeSslConfiguration(HttpServer httpServer) { - return new SslServerCustomizer(null, this.ssl.getClientAuth(), getSslBundle()).apply(httpServer); + return new SslServerCustomizer(null, this.ssl.getClientAuth(), getSslBundle(), getServerNameSslBundles()) + .apply(httpServer); } private ServerTransport createTcpTransport() { @@ -190,7 +194,8 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur tcpServer = tcpServer.runOn(this.resourceFactory.getLoopResources()); } if (Ssl.isEnabled(this.ssl)) { - tcpServer = new TcpSslServerCustomizer(this.ssl.getClientAuth(), getSslBundle()).apply(tcpServer); + tcpServer = new TcpSslServerCustomizer(this.ssl.getClientAuth(), getSslBundle(), getServerNameSslBundles()) + .apply(tcpServer); } return TcpServerTransport.create(tcpServer.bindAddress(this::getListenAddress)); } @@ -199,6 +204,13 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur return WebServerSslBundle.get(this.ssl, this.sslBundles); } + protected final Map getServerNameSslBundles() { + return this.ssl.getServerNameBundles() + .stream() + .collect(Collectors.toMap(Ssl.ServerNameSslBundle::serverName, + (serverNameSslBundle) -> this.sslBundles.getBundle(serverNameSslBundle.bundle()))); + } + private InetSocketAddress getListenAddress() { if (this.address != null) { return new InetSocketAddress(this.address.getHostAddress(), this.port); @@ -211,8 +223,9 @@ public class NettyRSocketServerFactory implements RSocketServerFactory, Configur private final SslBundle sslBundle; - private TcpSslServerCustomizer(Ssl.ClientAuth clientAuth, SslBundle sslBundle) { - super(null, clientAuth, sslBundle); + private TcpSslServerCustomizer(ClientAuth clientAuth, SslBundle sslBundle, + Map serverNameSslBundles) { + super(null, clientAuth, sslBundle, serverNameSslBundles); this.sslBundle = sslBundle; } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java index f7c34851b61..c28387c4940 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactory.java @@ -248,6 +248,9 @@ public class JettyServletWebServerFactory extends AbstractServletWebServerFactor } private void customizeSsl(Server server, InetSocketAddress address) { + if (!getSsl().getServerNameBundles().isEmpty()) { + throw new IllegalArgumentException("Server name SSL bundles are not supported with Jetty"); + } new SslServerCustomizer(getHttp2(), address, getSsl().getClientAuth(), getSslBundle()).customize(server); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java index ee8c014ec3d..73578b57368 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/NettyReactiveWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -44,6 +44,7 @@ import org.springframework.util.StringUtils; * * @author Brian Clozel * @author Moritz Halbritter + * @author Scott Frederick * @since 2.0.0 */ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFactory { @@ -172,14 +173,22 @@ public class NettyReactiveWebServerFactory extends AbstractReactiveWebServerFact } private HttpServer customizeSslConfiguration(HttpServer httpServer) { - SslServerCustomizer customizer = new SslServerCustomizer(getHttp2(), getSsl().getClientAuth(), getSslBundle()); - String bundleName = getSsl().getBundle(); - if (StringUtils.hasText(bundleName)) { - getSslBundles().addBundleUpdateHandler(bundleName, customizer::updateSslBundle); - } + SslServerCustomizer customizer = new SslServerCustomizer(getHttp2(), getSsl().getClientAuth(), getSslBundle(), + getServerNameSslBundles()); + addBundleUpdateHandler(null, getSsl().getBundle(), customizer); + getSsl().getServerNameBundles() + .forEach((serverNameSslBundle) -> addBundleUpdateHandler(serverNameSslBundle.serverName(), + serverNameSslBundle.bundle(), customizer)); return customizer.apply(httpServer); } + private void addBundleUpdateHandler(String hostName, String bundleName, SslServerCustomizer customizer) { + if (StringUtils.hasText(bundleName)) { + getSslBundles().addBundleUpdateHandler(bundleName, + (sslBundle) -> customizer.updateSslBundle(hostName, sslBundle)); + } + } + private HttpProtocol[] listProtocols() { List protocols = new ArrayList<>(); protocols.add(HttpProtocol.HTTP11); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java index 204868e0608..af32422e3c2 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/netty/SslServerCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -16,6 +16,9 @@ package org.springframework.boot.web.embedded.netty; +import java.util.HashMap; +import java.util.Map; + import io.netty.handler.ssl.ClientAuth; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -54,13 +57,17 @@ public class SslServerCustomizer implements NettyServerCustomizer { private volatile SslProvider sslProvider; + private final Map serverNameSslProviders; + private volatile SslBundle sslBundle; - public SslServerCustomizer(Http2 http2, Ssl.ClientAuth clientAuth, SslBundle sslBundle) { + public SslServerCustomizer(Http2 http2, Ssl.ClientAuth clientAuth, SslBundle sslBundle, + Map serverNameSslBundles) { this.http2 = http2; this.clientAuth = Ssl.ClientAuth.map(clientAuth, ClientAuth.NONE, ClientAuth.OPTIONAL, ClientAuth.REQUIRE); this.sslBundle = sslBundle; this.sslProvider = createSslProvider(sslBundle); + this.serverNameSslProviders = createServerNameSslProviders(serverNameSslBundles); } @Override @@ -69,14 +76,29 @@ public class SslServerCustomizer implements NettyServerCustomizer { } private void applySecurity(SslContextSpec spec) { - spec.sslContext(this.sslProvider.getSslContext()) - .setSniAsyncMappings((domainName, promise) -> promise.setSuccess(this.sslProvider)); + spec.sslContext(this.sslProvider.getSslContext()).setSniAsyncMappings((domainName, promise) -> { + SslProvider provider = (domainName != null) ? this.serverNameSslProviders.get(domainName) + : this.sslProvider; + return promise.setSuccess(provider); + }); } - void updateSslBundle(SslBundle sslBundle) { + void updateSslBundle(String hostName, SslBundle sslBundle) { logger.debug("SSL Bundle has been updated, reloading SSL configuration"); - this.sslBundle = sslBundle; - this.sslProvider = createSslProvider(sslBundle); + if (hostName == null) { + this.sslBundle = sslBundle; + this.sslProvider = createSslProvider(sslBundle); + } + else { + this.serverNameSslProviders.put(hostName, createSslProvider(sslBundle)); + } + } + + private Map createServerNameSslProviders(Map serverNameSslBundles) { + Map serverNameSslProviders = new HashMap<>(); + serverNameSslBundles + .forEach((hostName, sslBundle) -> serverNameSslProviders.put(hostName, createSslProvider(sslBundle))); + return serverNameSslProviders; } private SslProvider createSslProvider(SslBundle sslBundle) { 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 75601111c1d..dcc8b163e8e 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-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -16,6 +16,8 @@ package org.springframework.boot.web.embedded.tomcat; +import java.util.Map; + import org.apache.catalina.connector.Connector; import org.apache.commons.logging.Log; import org.apache.coyote.ProtocolHandler; @@ -56,36 +58,47 @@ class SslConnectorCustomizer { this.connector = connector; } - void update(SslBundle updatedSslBundle) { - this.logger.debug("SSL Bundle has been updated, reloading SSL configuration"); - customize(updatedSslBundle); + void update(String hostName, SslBundle updatedSslBundle) { + AbstractHttp11JsseProtocol protocol = (AbstractHttp11JsseProtocol) this.connector.getProtocolHandler(); + String host = (hostName != null) ? hostName : protocol.getDefaultSSLHostConfigName(); + this.logger.debug("SSL Bundle for host " + host + " has been updated, reloading SSL configuration"); + addSslHostConfig(protocol, host, updatedSslBundle); } - void customize(SslBundle sslBundle) { + 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(sslBundle, (AbstractHttp11JsseProtocol) handler); + configureSsl((AbstractHttp11JsseProtocol) handler, sslBundle, serverNameSslBundles); this.connector.setScheme("https"); this.connector.setSecure(true); } /** * Configure Tomcat's {@link AbstractHttp11JsseProtocol} for SSL. - * @param sslBundle the SSL bundle * @param protocol the protocol + * @param sslBundle the SSL bundle + * @param serverNameSslBundles the SSL bundles for specific SNI host names */ - private void configureSsl(SslBundle sslBundle, AbstractHttp11JsseProtocol protocol) { + private void configureSsl(AbstractHttp11JsseProtocol protocol, SslBundle sslBundle, + Map serverNameSslBundles) { protocol.setSSLEnabled(true); + if (sslBundle != null) { + addSslHostConfig(protocol, protocol.getDefaultSSLHostConfigName(), sslBundle); + } + serverNameSslBundles.forEach((hostName, bundle) -> addSslHostConfig(protocol, hostName, bundle)); + } + + private void addSslHostConfig(AbstractHttp11JsseProtocol protocol, String hostName, SslBundle sslBundle) { SSLHostConfig sslHostConfig = new SSLHostConfig(); - sslHostConfig.setHostName(protocol.getDefaultSSLHostConfigName()); + sslHostConfig.setHostName(hostName); configureSslClientAuth(sslHostConfig); - applySslBundle(sslBundle, protocol, sslHostConfig); + applySslBundle(protocol, sslHostConfig, sslBundle); protocol.addSslHostConfig(sslHostConfig, true); } - private void applySslBundle(SslBundle sslBundle, AbstractHttp11JsseProtocol protocol, - SSLHostConfig sslHostConfig) { + private void applySslBundle(AbstractHttp11JsseProtocol protocol, SSLHostConfig sslHostConfig, + SslBundle sslBundle) { SslBundleKey key = sslBundle.getKey(); SslStoreBundle stores = sslBundle.getStores(); SslOptions options = sslBundle.getOptions(); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java index 228135bfc0b..3b017e186b3 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatReactiveWebServerFactory.java @@ -61,6 +61,7 @@ import org.springframework.util.StringUtils; * @author Brian Clozel * @author HaiTao Zhang * @author Moritz Halbritter + * @author Scott Frederick * @since 2.0.0 */ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFactory @@ -237,10 +238,17 @@ public class TomcatReactiveWebServerFactory extends AbstractReactiveWebServerFac private void customizeSsl(Connector connector) { SslConnectorCustomizer customizer = new SslConnectorCustomizer(logger, connector, getSsl().getClientAuth()); - customizer.customize(getSslBundle()); - String sslBundleName = getSsl().getBundle(); + customizer.customize(getSslBundle(), getServerNameSslBundles()); + addBundleUpdateHandler(null, getSsl().getBundle(), customizer); + getSsl().getServerNameBundles() + .forEach((serverNameSslBundle) -> addBundleUpdateHandler(serverNameSslBundle.serverName(), + serverNameSslBundle.bundle(), customizer)); + } + + private void addBundleUpdateHandler(String hostName, String sslBundleName, SslConnectorCustomizer customizer) { if (StringUtils.hasText(sslBundleName)) { - getSslBundles().addBundleUpdateHandler(sslBundleName, customizer::update); + getSslBundles().addBundleUpdateHandler(sslBundleName, + (sslBundle) -> customizer.update(hostName, sslBundle)); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java index ae6ccf95fbd..a4a57213fe6 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/tomcat/TomcatServletWebServerFactory.java @@ -107,6 +107,7 @@ import org.springframework.util.StringUtils; * @author Christoffer Sawicki * @author Dawid Antecki * @author Moritz Halbritter + * @author Scott Frederick * @since 2.0.0 * @see #setPort(int) * @see #setContextLifecycleListeners(Collection) @@ -379,10 +380,17 @@ public class TomcatServletWebServerFactory extends AbstractServletWebServerFacto private void customizeSsl(Connector connector) { SslConnectorCustomizer customizer = new SslConnectorCustomizer(logger, connector, getSsl().getClientAuth()); - customizer.customize(getSslBundle()); - String sslBundleName = getSsl().getBundle(); + customizer.customize(getSslBundle(), getServerNameSslBundles()); + addBundleUpdateHandler(null, getSsl().getBundle(), customizer); + getSsl().getServerNameBundles() + .forEach((serverNameSslBundle) -> addBundleUpdateHandler(serverNameSslBundle.serverName(), + serverNameSslBundle.bundle(), customizer)); + } + + private void addBundleUpdateHandler(String hostName, String sslBundleName, SslConnectorCustomizer customizer) { if (StringUtils.hasText(sslBundleName)) { - getSslBundles().addBundleUpdateHandler(sslBundleName, customizer::update); + getSslBundles().addBundleUpdateHandler(sslBundleName, + (sslBundle) -> customizer.update(hostName, sslBundle)); } } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java index 4a93eb6c403..12d89a7ae1a 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/SslBuilderCustomizer.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -17,10 +17,13 @@ package org.springframework.boot.web.embedded.undertow; import java.net.InetAddress; +import java.util.Map; import javax.net.ssl.SSLContext; import io.undertow.Undertow; +import io.undertow.protocols.ssl.SNIContextMatcher; +import io.undertow.protocols.ssl.SNISSLContext; import org.xnio.Options; import org.xnio.Sequence; import org.xnio.SslClientAuthMode; @@ -47,18 +50,21 @@ class SslBuilderCustomizer implements UndertowBuilderCustomizer { private final SslBundle sslBundle; - SslBuilderCustomizer(int port, InetAddress address, ClientAuth clientAuth, SslBundle sslBundle) { + private final Map serverNameSslBundles; + + SslBuilderCustomizer(int port, InetAddress address, ClientAuth clientAuth, SslBundle sslBundle, + Map serverNameSslBundles) { this.port = port; this.address = address; this.clientAuth = clientAuth; this.sslBundle = sslBundle; + this.serverNameSslBundles = serverNameSslBundles; } @Override public void customize(Undertow.Builder builder) { SslOptions options = this.sslBundle.getOptions(); - SSLContext sslContext = this.sslBundle.createSslContext(); - builder.addHttpsListener(this.port, getListenAddress(), sslContext); + builder.addHttpsListener(this.port, getListenAddress(), createSslContext()); builder.setSocketOption(Options.SSL_CLIENT_AUTH_MODE, ClientAuth.map(this.clientAuth, SslClientAuthMode.NOT_REQUESTED, SslClientAuthMode.REQUESTED, SslClientAuthMode.REQUIRED)); if (options.getEnabledProtocols() != null) { @@ -69,6 +75,14 @@ class SslBuilderCustomizer implements UndertowBuilderCustomizer { } } + private SSLContext createSslContext() { + SNIContextMatcher.Builder builder = new SNIContextMatcher.Builder(); + builder.setDefaultContext(this.sslBundle.createSslContext()); + this.serverNameSslBundles + .forEach((server, sslBundle) -> builder.addMatch(server, sslBundle.createSslContext())); + return new SNISSLContext(builder.build()); + } + private String getListenAddress() { if (this.address == null) { return "0.0.0.0"; diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java index 75b3ab99667..9b731879b0b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowReactiveWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -31,6 +31,7 @@ import org.springframework.http.server.reactive.UndertowHttpHandlerAdapter; * {@link ReactiveWebServerFactory} that can be used to create {@link UndertowWebServer}s. * * @author Brian Clozel + * @author Scott Frederick * @since 2.0.0 */ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerFactory @@ -137,7 +138,7 @@ public class UndertowReactiveWebServerFactory extends AbstractReactiveWebServerF @Override public WebServer getWebServer(org.springframework.http.server.reactive.HttpHandler httpHandler) { - Undertow.Builder builder = this.delegate.createBuilder(this, this::getSslBundle); + Undertow.Builder builder = this.delegate.createBuilder(this, this::getSslBundle, this::getServerNameSslBundles); List httpHandlerFactories = this.delegate.createHttpHandlerFactories(this, (next) -> new UndertowHttpHandlerAdapter(httpHandler)); return new UndertowWebServer(builder, httpHandlerFactories, getPort() >= 0); diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java index 6c348987ca7..e03d06717ba 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowServletWebServerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -85,6 +85,7 @@ import org.springframework.util.CollectionUtils; * @author Andy Wilkinson * @author Marcos Barbero * @author EddĂș MelĂ©ndez + * @author Scott Frederick * @since 2.0.0 * @see UndertowServletWebServer */ @@ -295,7 +296,7 @@ public class UndertowServletWebServerFactory extends AbstractServletWebServerFac @Override public WebServer getWebServer(ServletContextInitializer... initializers) { - Builder builder = this.delegate.createBuilder(this, this::getSslBundle); + Builder builder = this.delegate.createBuilder(this, this::getSslBundle, this::getServerNameSslBundles); DeploymentManager manager = createManager(initializers); return getUndertowWebServer(builder, manager, getPort()); } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java index 742e332f140..cde105efcd8 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/embedded/undertow/UndertowWebServerFactoryDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -23,6 +23,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Supplier; @@ -46,6 +47,7 @@ import org.springframework.util.StringUtils; * * @author Phillip Webb * @author Andy Wilkinson + * @author Scott Frederick */ class UndertowWebServerFactoryDelegate { @@ -143,7 +145,8 @@ class UndertowWebServerFactoryDelegate { return this.useForwardHeaders; } - Builder createBuilder(AbstractConfigurableWebServerFactory factory, Supplier sslBundleSupplier) { + Builder createBuilder(AbstractConfigurableWebServerFactory factory, Supplier sslBundleSupplier, + Supplier> serverNameSslBundlesSupplier) { InetAddress address = factory.getAddress(); int port = factory.getPort(); Builder builder = Undertow.builder(); @@ -165,7 +168,8 @@ class UndertowWebServerFactoryDelegate { } Ssl ssl = factory.getSsl(); if (Ssl.isEnabled(ssl)) { - new SslBuilderCustomizer(factory.getPort(), address, ssl.getClientAuth(), sslBundleSupplier.get()) + new SslBuilderCustomizer(factory.getPort(), address, ssl.getClientAuth(), sslBundleSupplier.get(), + serverNameSslBundlesSupplier.get()) .customize(builder); } else { diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java index b77b4f56c26..c3dccc6bb1f 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/AbstractConfigurableWebServerFactory.java @@ -22,10 +22,13 @@ import java.net.InetAddress; import java.nio.file.Files; import java.util.Arrays; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; +import org.springframework.boot.web.server.Ssl.ServerNameSslBundle; import org.springframework.util.Assert; /** @@ -195,6 +198,13 @@ public abstract class AbstractConfigurableWebServerFactory implements Configurab return WebServerSslBundle.get(this.ssl, this.sslBundles); } + protected final Map getServerNameSslBundles() { + return this.ssl.getServerNameBundles() + .stream() + .collect(Collectors.toMap(ServerNameSslBundle::serverName, + (serverNameSslBundle) -> this.sslBundles.getBundle(serverNameSslBundle.bundle()))); + } + /** * Return the absolute temp dir for given web server. * @param prefix server name diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Ssl.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Ssl.java index bb17e6a26c4..49f7ee9b4bc 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Ssl.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/Ssl.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2024 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. @@ -16,6 +16,9 @@ package org.springframework.boot.web.server; +import java.util.ArrayList; +import java.util.List; + /** * Simple server-independent abstraction for SSL configuration. * @@ -67,6 +70,8 @@ public class Ssl { private String protocol = "TLS"; + private List serverNameBundles = new ArrayList<>(); + /** * Return whether to enable SSL support. * @return whether to enable SSL support @@ -325,6 +330,18 @@ public class Ssl { return (ssl != null) && ssl.isEnabled(); } + /** + * Return the mapping of host names to SSL bundles for SNI configuration. + * @return the host name to SSL bundle mapping + */ + public List getServerNameBundles() { + return this.serverNameBundles; + } + + public void setServerNameBundles(List serverNames) { + this.serverNameBundles = serverNames; + } + /** * Factory method to create an {@link Ssl} instance for a specific bundle name. * @param bundle the name of the bundle @@ -337,6 +354,9 @@ public class Ssl { return ssl; } + public record ServerNameSslBundle(String serverName, String bundle) { + } + /** * Client authentication types. */ diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java index 87c8727f832..5134881e77b 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/web/server/WebServerSslBundle.java @@ -206,7 +206,8 @@ public final class WebServerSslBundle implements SslBundle { private final String keyStorePassword; private WebServerSslStoreBundle(KeyStore keyStore, KeyStore trustStore, String keyStorePassword) { - Assert.state(keyStore != null || trustStore != null, "SSL is enabled but no trust material is configured"); + Assert.state(keyStore != null || trustStore != null, + "SSL is enabled but no trust material is configured for the default host"); this.keyStore = keyStore; this.trustStore = trustStore; this.keyStorePassword = keyStorePassword; diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java index 04d5d3a4bd1..d5f448d8888 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/jetty/JettyServletWebServerFactoryTests.java @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.EventListener; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -67,6 +68,7 @@ import org.springframework.boot.web.server.GracefulShutdownResult; import org.springframework.boot.web.server.PortInUseException; import org.springframework.boot.web.server.Shutdown; import org.springframework.boot.web.server.Ssl; +import org.springframework.boot.web.server.Ssl.ServerNameSslBundle; import org.springframework.boot.web.server.WebServerException; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactory; import org.springframework.boot.web.servlet.server.AbstractServletWebServerFactoryTests; @@ -74,6 +76,7 @@ import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.inOrder; @@ -283,6 +286,19 @@ class JettyServletWebServerFactoryTests extends AbstractServletWebServerFactoryT assertThat(sslContextFactory.getIncludeProtocols()).containsExactly("TLSv1.1"); } + @Test + void sslServerNameBundlesConfigurationThrowsException() { + Ssl ssl = new Ssl(); + ssl.setBundle("test"); + List bundles = List.of(new ServerNameSslBundle("first", "test1"), + new ServerNameSslBundle("second", "test2")); + ssl.setServerNameBundles(bundles); + JettyServletWebServerFactory factory = getFactory(); + factory.setSsl(ssl); + assertThatIllegalArgumentException().isThrownBy(() -> this.webServer = factory.getWebServer()) + .withMessageContaining("Server name SSL bundles are not supported with Jetty"); + } + private SslContextFactory extractSslContextFactory(SslConnectionFactory connectionFactory) { try { return connectionFactory.getSslContextFactory(); diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java index 0c1545b258a..bb509dea51d 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/web/embedded/tomcat/SslConnectorCustomizerTests.java @@ -16,6 +16,8 @@ package org.springframework.boot.web.embedded.tomcat; +import java.util.Collections; + import org.apache.catalina.connector.Connector; import org.apache.catalina.startup.Tomcat; import org.apache.commons.logging.Log; @@ -76,7 +78,7 @@ class SslConnectorCustomizerTests { ssl.setCiphers(new String[] { "ALPHA", "BRAVO", "CHARLIE" }); Connector connector = this.tomcat.getConnector(); SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); - customizer.customize(WebServerSslBundle.get(ssl)); + customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap()); this.tomcat.start(); SSLHostConfig[] sslHostConfigs = connector.getProtocolHandler().findSslHostConfigs(); assertThat(sslHostConfigs[0].getCiphers()).isEqualTo("ALPHA:BRAVO:CHARLIE"); @@ -91,7 +93,7 @@ class SslConnectorCustomizerTests { ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" }); Connector connector = this.tomcat.getConnector(); SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); - customizer.customize(WebServerSslBundle.get(ssl)); + customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap()); this.tomcat.start(); SSLHostConfig sslHostConfig = connector.getProtocolHandler().findSslHostConfigs()[0]; assertThat(sslHostConfig.getSslProtocol()).isEqualTo("TLS"); @@ -107,7 +109,7 @@ class SslConnectorCustomizerTests { ssl.setCiphers(new String[] { "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "BRAVO" }); Connector connector = this.tomcat.getConnector(); SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, connector, ssl.getClientAuth()); - customizer.customize(WebServerSslBundle.get(ssl)); + customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap()); this.tomcat.start(); SSLHostConfig sslHostConfig = connector.getProtocolHandler().findSslHostConfigs()[0]; assertThat(sslHostConfig.getSslProtocol()).isEqualTo("TLS"); @@ -119,7 +121,7 @@ class SslConnectorCustomizerTests { assertThatIllegalStateException().isThrownBy(() -> { SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, this.tomcat.getConnector(), Ssl.ClientAuth.NONE); - customizer.customize(WebServerSslBundle.get(new Ssl())); + customizer.customize(WebServerSslBundle.get(new Ssl()), Collections.emptyMap()); }).withMessageContaining("SSL is enabled but no trust material is configured"); } @@ -133,7 +135,7 @@ class SslConnectorCustomizerTests { assertThatIllegalStateException().isThrownBy(() -> { SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, this.tomcat.getConnector(), ssl.getClientAuth()); - customizer.customize(WebServerSslBundle.get(ssl)); + customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap()); }).withMessageContaining("must be empty or null for PKCS11 hardware key stores"); } @@ -145,7 +147,8 @@ class SslConnectorCustomizerTests { ssl.setKeyStorePassword("1234"); SslConnectorCustomizer customizer = new SslConnectorCustomizer(this.logger, this.tomcat.getConnector(), ssl.getClientAuth()); - assertThatNoException().isThrownBy(() -> customizer.customize(WebServerSslBundle.get(ssl))); + assertThatNoException() + .isThrownBy(() -> customizer.customize(WebServerSslBundle.get(ssl), Collections.emptyMap())); } } diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/build.gradle new file mode 100644 index 00000000000..df3b0b965ea --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/build.gradle @@ -0,0 +1,86 @@ +plugins { + id "java" + id "org.springframework.boot.conventions" + id "org.springframework.boot.integration-test" +} + +description = "Spring Boot SNI Integration Tests" + +configurations { + app +} + +dependencies { + app project(path: ":spring-boot-project:spring-boot-dependencies", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter-web", configuration: "mavenRepository") + app project(path: ":spring-boot-project:spring-boot-starters:spring-boot-starter", configuration: "mavenRepository") + + intTestImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-parent"))) + intTestImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-web")) + intTestImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support")) + intTestImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) + intTestImplementation("org.testcontainers:junit-jupiter") + intTestImplementation("org.testcontainers:testcontainers") +} + +task syncMavenRepository(type: Sync) { + from configurations.app + into "${buildDir}/int-test-maven-repository" +} + +task syncReactiveServerAppSource(type: org.springframework.boot.build.SyncAppSource) { + sourceDirectory = file("spring-boot-sni-reactive-app") + destinationDirectory = file("${buildDir}/spring-boot-sni-reactive-app") +} + +task buildReactiveServerApps(type: GradleBuild) { + dependsOn syncReactiveServerAppSource, syncMavenRepository + dir = "${buildDir}/spring-boot-sni-reactive-app" + startParameter.buildCacheEnabled = false + tasks = [ + "nettyServerApp", + "tomcatServerApp", + "undertowServerApp" + ] +} + +task syncServletServerAppSource(type: org.springframework.boot.build.SyncAppSource) { + sourceDirectory = file("spring-boot-sni-servlet-app") + destinationDirectory = file("${buildDir}/spring-boot-sni-servlet-app") +} + +task buildServletServerApps(type: GradleBuild) { + dependsOn syncServletServerAppSource, syncMavenRepository + dir = "${buildDir}/spring-boot-sni-servlet-app" + startParameter.buildCacheEnabled = false + tasks = [ + "tomcatServerApp", + "undertowServerApp" + ] +} + +task syncClientAppSource(type: org.springframework.boot.build.SyncAppSource) { + sourceDirectory = file("spring-boot-sni-client-app") + destinationDirectory = file("${buildDir}/spring-boot-sni-client-app") +} + +task buildClientApp(type: GradleBuild) { + dependsOn syncClientAppSource, syncMavenRepository + dir = "${buildDir}/spring-boot-sni-client-app" + startParameter.buildCacheEnabled = false + tasks = ["build"] +} + +intTest { + inputs.files( + "${buildDir}/spring-boot-server-tests-app/build/libs/spring-boot-server-tests-app-netty-reactive.jar", + "${buildDir}/spring-boot-server-tests-app/build/libs/spring-boot-server-tests-app-tomcat-reactive.jar", + "${buildDir}/spring-boot-server-tests-app/build/libs/spring-boot-server-tests-app-tomcat-servlet.jar", + "${buildDir}/spring-boot-server-tests-app/build/libs/spring-boot-server-tests-app-undertow-reactive.jar", + "${buildDir}/spring-boot-server-tests-app/build/libs/spring-boot-server-tests-app-undertow-servlet.jar") + .withPropertyName("applicationArchives") + .withPathSensitivity(PathSensitivity.RELATIVE) + .withNormalizer(ClasspathNormalizer) + dependsOn buildReactiveServerApps, buildServletServerApps, buildClientApp +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/create-certs.sh b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/create-certs.sh new file mode 100755 index 00000000000..19262c31382 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/create-certs.sh @@ -0,0 +1,72 @@ +#!/bin/bash + +create_ssl_config() { + cat > openssl.cnf <<_END_ + subjectAltName = @alt_names + [alt_names] + DNS.1 = example.com + DNS.2 = localhost + [ server_cert ] + keyUsage = digitalSignature, keyEncipherment + nsCertType = server + [ client_cert ] + keyUsage = digitalSignature, keyEncipherment + nsCertType = client +_END_ + +} + +generate_ca_cert() { + local location=$1 + + mkdir -p ${location} + + openssl genrsa -out ${location}/test-ca.key 4096 + openssl req -key ${location}/test-ca.key -out ${location}/test-ca.crt \ + -x509 -new -nodes -sha256 -days 365 \ + -subj "/O=Spring Boot Test/CN=Certificate Authority" \ + -addext "subjectAltName=DNS:hello.example.com,DNS:hello-alt.example.com" +} + +generate_cert() { + local location=$1 + local caLocation=$2 + local hostname=$3 + + local keyfile=${location}/test-${hostname}-server.key + local certfile=${location}/test-${hostname}-server.crt + + mkdir -p ${location} + + openssl genrsa -out ${keyfile} 2048 + openssl req -key ${keyfile} \ + -new -sha256 \ + -subj "/O=Spring Boot Test/CN=${hostname}.example.com" \ + -addext "subjectAltName=DNS:${hostname}.example.com" | \ + openssl x509 -req -out ${certfile} \ + -CA ${caLocation}/test-ca.crt -CAkey ${caLocation}/test-ca.key -CAserial ${caLocation}/test-ca.txt -CAcreateserial \ + -sha256 -days 365 \ + -extfile openssl.cnf \ + -extensions server_cert +} + +if ! command -v openssl &> /dev/null; then + echo "openssl is required" + exit +fi + +mkdir -p certs + +create_ssl_config +generate_ca_cert certs/ca +generate_cert certs/default certs/ca hello +generate_cert certs/alt certs/ca hello-alt + +rm -f openssl.cnf +rm -f certs/ca/test-ca.key certs/ca/test-ca.txt + +cp -r certs/* spring-boot-sni-reactive-app/src/main/resources +cp -r certs/* spring-boot-sni-servlet-app/src/main/resources +cp -r certs/ca/* spring-boot-sni-client-app/src/main/resources/ca + +rm -rf certs diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/build.gradle new file mode 100644 index 00000000000..3d8ee7040c6 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/build.gradle @@ -0,0 +1,17 @@ +plugins { + id "java" + id "org.springframework.boot" +} + +apply plugin: "io.spring.dependency-management" + +repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/settings.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/settings.gradle new file mode 100644 index 00000000000..687ab25fbfd --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.springframework.boot") { + useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}" + } + } + } +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/java/org/springframework/boot/sni/client/SniClientApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/java/org/springframework/boot/sni/client/SniClientApplication.java new file mode 100644 index 00000000000..b9a1bfab0d3 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/java/org/springframework/boot/sni/client/SniClientApplication.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2024 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.Arrays; + +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.web.client.RestClientSsl; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.web.client.RestClient; +import org.springframework.boot.ssl.SslBundles; + +@SpringBootApplication +public class SniClientApplication { + + public static void main(String[] args) { + new SpringApplicationBuilder(SniClientApplication.class) + .web(WebApplicationType.NONE).run(args); + } + + @Bean + public RestClient restClient(RestClient.Builder restClientBuilder, RestClientSsl ssl) { + return restClientBuilder.apply(ssl.fromBundle("server")).build(); + } + + @Bean + public CommandLineRunner commandLineRunner(RestClient client) { + return ((args) -> { + for (String hostname : args) { + callServer(client, hostname); + callActuator(client, hostname); + } + }); + } + + private static void callServer(RestClient client, String hostname) { + String url = "https://" + hostname + ":8443/"; + System.out.println(">>>>>> Calling server at '" + url + "'"); + try { + ResponseEntity response = client.get().uri(url).retrieve().toEntity(String.class); + System.out.println(">>>>>> Server response status code is '" + response.getStatusCode() + "'"); + System.out.println(">>>>>> Server response body is '" + response + "'"); + } catch (Exception ex) { + System.out.println(">>>>>> Exception thrown calling server at '" + url + "': " + ex.getMessage()); + ex.printStackTrace(); + } + } + + private static void callActuator(RestClient client, String hostname) { + String url = "https://" + hostname + ":8444/actuator/health"; + System.out.println(">>>>>> Calling server actuator at '" + url + "'"); + try { + ResponseEntity response = client.get().uri(url).retrieve().toEntity(String.class); + System.out.println(">>>>>> Server actuator response status code is '" + response.getStatusCode() + "'"); + System.out.println(">>>>>> Server actuator response body is '" + response + "'"); + } catch (Exception ex) { + System.out.println(">>>>>> Exception thrown calling server actuator at '" + url + "': " + ex.getMessage()); + ex.printStackTrace(); + } + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/resources/application.yml b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/resources/application.yml new file mode 100644 index 00000000000..2a3d95a7999 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/resources/application.yml @@ -0,0 +1,7 @@ +spring: + ssl: + bundle: + pem: + server: + truststore: + certificate: "classpath:ca/test-ca.crt" diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/resources/ca/test-ca.crt b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/resources/ca/test-ca.crt new file mode 100644 index 00000000000..b6ec29bca77 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-client-app/src/main/resources/ca/test-ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFjjCCA3agAwIBAgIUE7GfX9Wcx8X4/jDHboW5TUSvHMUwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDIyNjIwMTIxMFoXDTI1MDIyNTIwMTIxMFow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNh +dGUgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkQq1 +UWWdc1BuD9sxI4XOrxY9JVITfuejj5hhWg9sjrvov24QjQoTtcOheNzck5MUFcS9 +9RGB4EJWQy17hA4c7XP3KPG9e9xIrZC11ANVUlFqJSSvv/z46KjnvUDgf0PuQKOG +RTWwezNzl0CcJHuLwdJ3yVbtcNwuUh3ououvmmNVtj2dVij2QDYmfPeIwIaAmEhG +t8Oe/dwII/vnVD7iDJ8IApxj8Etk5XZ3DOGTc0ky9h1suY+wQg6amgNEkvPFJQFm +Ej5G920dq2ox/QD/DCMRxmUaqFs1ozqy3ZWDCB7IYYJ6yzE51GMc8JcsLR2vArqs +UJGPAKobdUSkbO0aqQ36+sUc44PGVHtbslJuGyBKVXbJuImhemdGQsLaXeix2XfE +jhFFHi/zsTRtkeiSPkFOdcYFx2j9ts7usdNHTHgNPC11QdFij5+aqmSv/UJw8vRT +kosswUTPK0K5JRs/bx0KWPWyi34m/pCnzsFXALfxdfE9OIPemcmyFsHMQnCOevNu +clf88GNF0w3HpU6MTL1bN3MHc7X/l90HV552GfUY0m4qravRgU3RzzTj9g6GydRz +xFflEi34ELpzQ4FMHdxLAyMtb8mMqGGO8JF9j8vaIhFSn75KEl3qNn1wWhLqBTFY +ptvE2HCY1gz4GIq1AkCjVmgHIPSt4SrjNsSiDjUCAwEAAaOBiTCBhjAdBgNVHQ4E +FgQUxxEN3g2hCxU56J4iHbVmOqVax9UwHwYDVR0jBBgwFoAUxxEN3g2hCxU56J4i +HbVmOqVax9UwDwYDVR0TAQH/BAUwAwEB/zAzBgNVHREELDAqghFoZWxsby5leGFt +cGxlLmNvbYIVaGVsbG8tYWx0LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IC +AQAWowMnIv6eUYLMsJ2tVNy6L2+Wj2Hw/mLb1JlaMRdhiHpHIzNFi+Z7Loq+PXfb +R+He2SVbDieAaI5Uj1KiuH/j6DGNnZgoFKeu6qquuxvnURQATZhcRTq0Hpcuj/df +mICXqPHWNSd1UhR/qwjiW5osqlxnDOEg5LIzxZ1cK62YLC/em3lwzX76dXMJyRA0 +mR32QrqZPeJAg/v5+L5WFJAj9BybcjRLKMzqYCCsVNYrYS4XI7Ms68R5Dyd/iQnY +UTZChIjr8Yq5vgSK9+NPDh5naEJAlMtn9FQJPFeNW7T2y41uQf525+tXFeF73zWO +NktUISrTcG1VmRP3UzWO9taoKDN/vp8iWJ5xNgTNO81NvlZXyjJ3snByVrsTECPv ++MiQbv70aFZ5GE3VOLZVFQYyusRcPGvn46vkk/3wmuUySx/YwY0keg5BEW4JI8Jk +Ia3PtzQDSu888Z4vIXjOCMPHnFk6BAs4Xy2S2q8+XUgrdhPaLl2Yi0IHljUq2Q1f +4bQDJqiIVb3eIYoN/VrcD7v2uy5yKPZdiOLltZFMkb7mcKutYdLOGqAkDNsXaxr1 +zHByIlKXk4XWiPekrnh5tNqt1K0rPfvAmYBVTcGQXFcl8PBR6WEdvh0MldvYGpRV +4jRtDYoYTtRRmVOKUQ9DLTWwv1CIDPe/OFsesQQlodiZhw== +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/build.gradle new file mode 100644 index 00000000000..e38367a66b3 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/build.gradle @@ -0,0 +1,55 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + id "java" + id "org.springframework.boot" version "3.3.0-SNAPSHOT" +} + +apply plugin: "io.spring.dependency-management" + +repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + +configurations { + app { + extendsFrom(configurations.runtimeClasspath) + } + netty { + extendsFrom(app) + } + tomcat { + extendsFrom(app) + } + undertow { + extendsFrom(app) + } +} + +dependencies { + compileOnly("org.springframework:spring-webflux") + + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-actuator") + + app(files(sourceSets.main.output)) + app("org.springframework:spring-webflux") { + exclude group: "spring-boot-project", module: "spring-boot-starter-reactor-netty" + } + netty("org.springframework.boot:spring-boot-starter-webflux") + tomcat("org.springframework.boot:spring-boot-starter-tomcat") + undertow("org.springframework.boot:spring-boot-starter-undertow") +} + +["netty", "tomcat", "undertow"].each { webServer -> + def configurer = { task -> + task.mainClass = "org.springframework.boot.sni.server.SniServerApplication" + task.classpath = configurations.getByName(webServer) + task.archiveClassifier = webServer + task.targetJavaVersion = project.getTargetCompatibility() + } + tasks.register("${webServer}ServerApp", BootJar, configurer) +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/settings.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/settings.gradle new file mode 100644 index 00000000000..06d9554ad0d --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.springframework.boot") { + useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}" + } + } + } +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/HelloController.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/HelloController.java new file mode 100644 index 00000000000..8b3976f129a --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/HelloController.java @@ -0,0 +1,31 @@ +/* + * Copyright 2012-2024 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 org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @GetMapping + public String hello(ServerHttpRequest request) { + return "Hello from " + request.getURI(); + } + +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java new file mode 100644 index 00000000000..5f3cd504ba0 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2024 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.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; + +@SpringBootApplication +public class SniServerApplication { + + public static void main(String[] args) { + SpringApplication.run(SniServerApplication.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/alt/test-hello-alt-server.crt b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/alt/test-hello-alt-server.crt new file mode 100644 index 00000000000..27b08406679 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/alt/test-hello-alt-server.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEZjCCAk6gAwIBAgIUOLEdvpEkuAYUivOxM2sfYLdaHL4wDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDIyNjIwMTIxMFoXDTI1MDIyNTIwMTIxMFow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVaGVsbG8tYWx0 +LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Hde +3zmOM539oWpp1KNNmGSSqZaYed3qAbrZK8fk4TjN9U+2Yv6glo4XDCRkY61PNmQF +l2+pwnPvscu6CIf+S8DzXa3Y38u0e14RdhfdeJyluul2vJME16S7psCmajqWHAAT +Y/sl1Yjt/5AsEaqjGIDGXDyUzEGrtxZUZmptLpl736Y262JZOmoPBQRhEHudQss6 +utUqYbqeCO/rSIUoIAFtQfm4vJqDq0Jy+VAL9Emzn5SGjR7BZF266Xs0xsGTsBJM +rWCat6j2kJEsYo6IsC0JrFFdv6S8Orp5gBV06gHBlhjIiuvpCi9n9vxN19S+xJgj +YFJORxRnbNchkAwZeQIDAQABo2IwYDALBgNVHQ8EBAMCBaAwEQYJYIZIAYb4QgEB +BAQDAgZAMB0GA1UdDgQWBBTrYnvysRCkQ0NwIw+GCkEFQga7FTAfBgNVHSMEGDAW +gBTHEQ3eDaELFTnoniIdtWY6pVrH1TANBgkqhkiG9w0BAQsFAAOCAgEAT8L/5jqb +64MkeLJQbyXjXpHIaP9dr4NHBeTjXPx8FhuI9RG9ywNzuSlH0DeCQbcdPuMAmrDa +oWoaYzHpuG2saHcCeBdE0HtVCofvKYVQK7vUsT+tKqNbkGzt0cRdwmciDUh6O+Ub +7iOSd7wIjeobEoOxBB+Rk6mRpXRUENAPKMiAuDvpRDFaFyGedzxIeaRp5Gi+LEBh +/2cRZkWcrcWuKNAIszvdZXIl8gUwPU4KVkqX/pALLK5Ax87uniEjl77BQSeuC4++ +QYSnj76VRoyIqt9ZsjVG6VCOwze6M9nVmTR9mgADFsZFZPiqp73q8mOCEpbPfovi +rV8PFZzWZao4s4ulSA3GFlRxaBHe2AcR2pJSchG14OGjW8KWbsOEqLqy+f88ECEP +o9jjc6fq0QeznTZlRIw47UT2ykS1qV6xiKN67iMylCla8ogwK3HvF8pCME7LK99q +qslcLxj2DJQDHTqtDL2fOlxO0RGGuGg4jMfdBwHMjd30nHff5K5UZQARCFKTdbTZ +V5OOdJbDRcicXBQE7sbaTqztjjGKiRxCdvgcV760KJH+gYkzMikqC/xUIv7ac4qX +Q1FHQl+tsxZmM2nP7EapMYZqLle7go8Govze6r0pFEF6MT47ST7m0/ZBWKxm0CaU +uWpyhqoylaIqbR9rojPDSPDvslZvI4KGY8M= +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/alt/test-hello-alt-server.key b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/alt/test-hello-alt-server.key new file mode 100644 index 00000000000..8b67b8b6fa0 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/alt/test-hello-alt-server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUd17fOY4znf2h +amnUo02YZJKplph53eoButkrx+ThOM31T7Zi/qCWjhcMJGRjrU82ZAWXb6nCc++x +y7oIh/5LwPNdrdjfy7R7XhF2F914nKW66Xa8kwTXpLumwKZqOpYcABNj+yXViO3/ +kCwRqqMYgMZcPJTMQau3FlRmam0umXvfpjbrYlk6ag8FBGEQe51Cyzq61Sphup4I +7+tIhSggAW1B+bi8moOrQnL5UAv0SbOflIaNHsFkXbrpezTGwZOwEkytYJq3qPaQ +kSxijoiwLQmsUV2/pLw6unmAFXTqAcGWGMiK6+kKL2f2/E3X1L7EmCNgUk5HFGds +1yGQDBl5AgMBAAECggEAA/oNVzWck8Vr7zmEAZbiPO1PpsegKFJ0WX6bJ1ahJ2te +Gi6ucJaTgD3omzGTL3UZpnWjenzR5fHa6q5a17iz6cxoFw4fS7u5r8AdqOLezQMh +lv4HsD/ljKO+ChZRBxam+J8yaGIAXfQngEESkhdqNhXdoJxXAfu4sGnv6nrTewEb +sNTgzBceCotwVPBBnN+Ggx2LGp3FWnKqDeaLJM6+vBMDpJYm4BSU21H46bUu9dMm +yM+msZeBZMzAWBVOKoHJxoEE0I6EpwTIPuuq2oqjzQCzzAsKTTFhb51kwMj6k1o4 +Zc11IlDqjNCdB61R+7dqE4LoZWOa/cu26m+lMaZcgQKBgQD4q8zoKLSZ2jlpt1qi +URhupUOyf2BcP8fjygItlMGi3BHY8Rq6DLvoxUMFk0IJAKTolzoVDGxqbrc0Im6k +jed8VQ7SX8soN0aoEyKulv7jCXCRAzoZ2a0oh9cwNLmoEzIoYYSvPSe47HJb1B+T +aN7qQ6h+k0Q0XObgUK7D6WzyswKBgQDaumiM44gECZ7LEW+c16MIOIJWqyJyQsd+ +Bgnn2uHyT9B5ITExsws1oco7GabqZLvC+IpoVsbdS73wnuneLJ40ykqTkwKNLAzB +eUabqiLqLZZ2ACajA86uND7B4hfCeW1srcqU5hqL2/cY4CVJPkBO4mZ6//n8ruZs +OfQhRrbpIwKBgQCPKUlEdvrSgGIBTL/vJsTsHlUFFHQDZ+zKZWgvma6I9i2IOfZr +Gh2serSFJywjRq2qAjY8G/TmqWrrps8QCWo1mDp6PxAUzQ3ugWW8Ic4II00dD0CJ +1VntNZdbd19TNgnwWYQr5wdRXT7RQyQSl5OORvlgNaRUiQ+aIJkczOweJQKBgQCj +yjtIZYoRG/MhNalS1ddr7IUNyZE95uvkXzlDuhDAlywRyN1BzkVyn/kEUK1BkLVZ +xyw9/d1lEbbmXNncWaUO+vzljYy3kmjq6JoLL1h97C1jp7FHGS7IHK9yGJCaPLvI +SkwNPFJcsRdUNWU2d7tIVxlOuijFI2PBX5SE5qNJ6QKBgBex5fpTZ2y4Zn4mcF8s +qjWTAJ8L5xD84PWhZAgbxrawiS+z7S0vMcfxF154JTP89C0EtnMeygE9jFJjvE9E +TAmM2UlIp7p8BKlr15GZjEBn3DqtwV49zo1pLcTjujBCDl+tfR7EVD0VrNb5fely +iM4vJ89vAebkd7G7ro1nbBF6 +-----END PRIVATE KEY----- diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/application.yml b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/application.yml new file mode 100644 index 00000000000..540336014ef --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/application.yml @@ -0,0 +1,42 @@ +spring: + ssl: + bundle: + pem: + default: + keystore: + certificate: "classpath:default/test-hello-server.crt" + private-key: "classpath:default/test-hello-server.key" + truststore: + certificate: "classpath:ca/test-ca.crt" + alt: + keystore: + certificate: "classpath:alt/test-hello-alt-server.crt" + private-key: "classpath:alt/test-hello-alt-server.key" + truststore: + certificate: "classpath:ca/test-ca.crt" + +server: + port: 8443 + ssl: + bundle: "default" + server-name-bundles: + - server-name: "hello.example.com" + bundle: "default" + - server-name: "hello-alt.example.com" + bundle: "alt" + +management: + server: + port: 8444 + ssl: + bundle: "default" + server-name-bundles: + - server-name: "hello.example.com" + bundle: "default" + - server-name: "hello-alt.example.com" + bundle: "alt" + endpoints: + web: + exposure: + include: + - "*" diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/ca/test-ca.crt b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/ca/test-ca.crt new file mode 100644 index 00000000000..b6ec29bca77 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/ca/test-ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFjjCCA3agAwIBAgIUE7GfX9Wcx8X4/jDHboW5TUSvHMUwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDIyNjIwMTIxMFoXDTI1MDIyNTIwMTIxMFow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNh +dGUgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkQq1 +UWWdc1BuD9sxI4XOrxY9JVITfuejj5hhWg9sjrvov24QjQoTtcOheNzck5MUFcS9 +9RGB4EJWQy17hA4c7XP3KPG9e9xIrZC11ANVUlFqJSSvv/z46KjnvUDgf0PuQKOG +RTWwezNzl0CcJHuLwdJ3yVbtcNwuUh3ououvmmNVtj2dVij2QDYmfPeIwIaAmEhG +t8Oe/dwII/vnVD7iDJ8IApxj8Etk5XZ3DOGTc0ky9h1suY+wQg6amgNEkvPFJQFm +Ej5G920dq2ox/QD/DCMRxmUaqFs1ozqy3ZWDCB7IYYJ6yzE51GMc8JcsLR2vArqs +UJGPAKobdUSkbO0aqQ36+sUc44PGVHtbslJuGyBKVXbJuImhemdGQsLaXeix2XfE +jhFFHi/zsTRtkeiSPkFOdcYFx2j9ts7usdNHTHgNPC11QdFij5+aqmSv/UJw8vRT +kosswUTPK0K5JRs/bx0KWPWyi34m/pCnzsFXALfxdfE9OIPemcmyFsHMQnCOevNu +clf88GNF0w3HpU6MTL1bN3MHc7X/l90HV552GfUY0m4qravRgU3RzzTj9g6GydRz +xFflEi34ELpzQ4FMHdxLAyMtb8mMqGGO8JF9j8vaIhFSn75KEl3qNn1wWhLqBTFY +ptvE2HCY1gz4GIq1AkCjVmgHIPSt4SrjNsSiDjUCAwEAAaOBiTCBhjAdBgNVHQ4E +FgQUxxEN3g2hCxU56J4iHbVmOqVax9UwHwYDVR0jBBgwFoAUxxEN3g2hCxU56J4i +HbVmOqVax9UwDwYDVR0TAQH/BAUwAwEB/zAzBgNVHREELDAqghFoZWxsby5leGFt +cGxlLmNvbYIVaGVsbG8tYWx0LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IC +AQAWowMnIv6eUYLMsJ2tVNy6L2+Wj2Hw/mLb1JlaMRdhiHpHIzNFi+Z7Loq+PXfb +R+He2SVbDieAaI5Uj1KiuH/j6DGNnZgoFKeu6qquuxvnURQATZhcRTq0Hpcuj/df +mICXqPHWNSd1UhR/qwjiW5osqlxnDOEg5LIzxZ1cK62YLC/em3lwzX76dXMJyRA0 +mR32QrqZPeJAg/v5+L5WFJAj9BybcjRLKMzqYCCsVNYrYS4XI7Ms68R5Dyd/iQnY +UTZChIjr8Yq5vgSK9+NPDh5naEJAlMtn9FQJPFeNW7T2y41uQf525+tXFeF73zWO +NktUISrTcG1VmRP3UzWO9taoKDN/vp8iWJ5xNgTNO81NvlZXyjJ3snByVrsTECPv ++MiQbv70aFZ5GE3VOLZVFQYyusRcPGvn46vkk/3wmuUySx/YwY0keg5BEW4JI8Jk +Ia3PtzQDSu888Z4vIXjOCMPHnFk6BAs4Xy2S2q8+XUgrdhPaLl2Yi0IHljUq2Q1f +4bQDJqiIVb3eIYoN/VrcD7v2uy5yKPZdiOLltZFMkb7mcKutYdLOGqAkDNsXaxr1 +zHByIlKXk4XWiPekrnh5tNqt1K0rPfvAmYBVTcGQXFcl8PBR6WEdvh0MldvYGpRV +4jRtDYoYTtRRmVOKUQ9DLTWwv1CIDPe/OFsesQQlodiZhw== +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/default/test-hello-server.crt b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/default/test-hello-server.crt new file mode 100644 index 00000000000..31185387c15 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/default/test-hello-server.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEYjCCAkqgAwIBAgIUOLEdvpEkuAYUivOxM2sfYLdaHL0wDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDIyNjIwMTIxMFoXDTI1MDIyNTIwMTIxMFow +NzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEaMBgGA1UEAwwRaGVsbG8uZXhh +bXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+mTU1h68x +R6T4cmRFhzw7Uop3V4gLY//8n4I+6qrLjZx4ZCIA/QHZ/80DzSFJlE5P1TP1k7Wc ++MCI5KYjMzAZoR9mBfH6MUArDvPU7eZdxT3QQUMHfpAuLhPhwxsgLlogl5vq74t0 +WV+eipRJIEi7lqjNk1EuO3Nli5Wp6/tRv6v33stUwpWIWnSuxu6r+gS5xp1mpRgD +KBi950ZSUJx+0QcwOs52n9iZTruCxunEDVggj/xUsbdBBCT30bmqS681d7TZTj1R +6Y9PlMiJ4Do5SSs48hZic8ZVsc6TWTVntxUWZotd5NCTk7R4CwL/XdpphuL8mnA0 +eblUXyTkc4xPAgMBAAGjYjBgMAsGA1UdDwQEAwIFoDARBglghkgBhvhCAQEEBAMC +BkAwHQYDVR0OBBYEFArqIg3CQz0XayzQ3mO9Ikl1bmsgMB8GA1UdIwQYMBaAFMcR +Dd4NoQsVOeieIh21ZjqlWsfVMA0GCSqGSIb3DQEBCwUAA4ICAQBJY1n6gOglZGzJ +rVDni0RYWB7xYoBEwQG0UhK6ThZe7bqtnaYA5LL8l7jjtIwSE0fVtkGHe0OaBIRQ +Ntn1cxu15JRdwIUyn0S+ypq3IGPFX5vKaNL1wd7RL227EjtXOnH7ByQ182aNrkp9 +p2nwMiOKhFbK1qsz4zohdcdQq7sYQUAKABneGumk0c6UkmnYKHGkzK0pxrEU2qBN +xc9/NqmBlWyefvcdjAXWB7DBH+KScc7tatkZsOvwlGrWMxyLPDR1WzMZXbHwxPt6 +bHE5fdOPnkel1DJMjjn2/dphTBWOZBrFWuIeEWCzR1sKWgxRUzIzgA3hXKC29p5f +Y1BaXtzrFFD/mP9p7h8VYDPUSuNzDO9iqPf60qL+WN3cbkabJtrhBqzWSbPlzMOH +5ysn7RbVsvwD+OjmXuNhlHq65uf3ftAxCFCsiaFvfaRV9PYdYZP5JAHLZVPFhRhO +qg0KkPKTFrOwBw1w+mDf2ZQ/d/9Hd9PnknutmkBPwaqb0dRZcCUFLy/gPMrLkQse +P3naqOpHGFPLbLIKbQ0vyTT/6I0/lglZJ2ijk52a2gbz2BMjL+5b+A6MP2GGy166 +HxMKRbSQ3FgGGVuJd1ggUX8/juuP1Ykw58yS0OmfT2ryHLxfqF0Pr7uswNnPyGjZ +K/2fVlQBLbIfMjtmuoFvO01llElZHw== +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/default/test-hello-server.key b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/default/test-hello-server.key new file mode 100644 index 00000000000..bf4f2bfad2b --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-reactive-app/src/main/resources/default/test-hello-server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC+mTU1h68xR6T4 +cmRFhzw7Uop3V4gLY//8n4I+6qrLjZx4ZCIA/QHZ/80DzSFJlE5P1TP1k7Wc+MCI +5KYjMzAZoR9mBfH6MUArDvPU7eZdxT3QQUMHfpAuLhPhwxsgLlogl5vq74t0WV+e +ipRJIEi7lqjNk1EuO3Nli5Wp6/tRv6v33stUwpWIWnSuxu6r+gS5xp1mpRgDKBi9 +50ZSUJx+0QcwOs52n9iZTruCxunEDVggj/xUsbdBBCT30bmqS681d7TZTj1R6Y9P +lMiJ4Do5SSs48hZic8ZVsc6TWTVntxUWZotd5NCTk7R4CwL/XdpphuL8mnA0eblU +XyTkc4xPAgMBAAECggEAJ/SBGYQknz2IIUcFqyei4kK24Ta5v72KW8BqctsJy9sX +WouPL0ramQMNTMczO7P5yLWGi2wYDdx9rBTWmRlxc2X56Y7Ef7DUZVJgnhnzCWRA +RYhwz0DiY7PoGhMm/BOLdDqkBleKEe1sZJVjaYL5jE2UfGfuBDWVRsvAp5rfF+8r +gp5ewvaZUeFC09/aN+1k84wxA0njAmjiYDSId5Ymn0AxHDeoESmysMfr48p39/XH +5YiPne3OlrkEFlN1NYvPGf09ifyQYH91HuTrzKTX+jhvYM7E+LzSTrbcwlMUKDem +5CnVAkAJwnMhQDAOwAn52vPEqcru9d7jq+nkf+LQ8QKBgQD50mr9ApGHLN5uZ1Zk +1BRkTzAL4+yDYq0V5TiMaN7WNGTmIEUjEvnmaegxw37lBmmGhn6EPzSN/ZszgQIj +EQohxfU8lQ8ftBdnhmWTSs2Z57fHXItbvvKVxVRHw9oX+ZVj3akqKD9K6UAqWJlJ ++L4H+yHMIByS0ICnZcxhnXpSHwKBgQDDT9sRM2SCCULvpVTwqye7b7gW5Dlcv300 +X5CS1av7id1QMFcU3nohp7KzTkxRbeuGL2QF7wVa5NBMh7+GevnRrRtEImfcaA5n +zhDQDfOUg0/5csOCvL0WTIkHEpTEiqd2SVaaGzHvy09mLCDJxDpc9LuTZiDP9/JL +vzIcb0Nf0QKBgHnk0nEbFLjZCrrhzwSpej2raa0Ti+5bckqxqlLQRJJNxEGI01MW +yjpDyJinY74Jz+lkrEyIrnLtoBGUS9+iS8hI16y0qkl0zMqlh+BDamhC6Kfsns6o +L6MmQkY16K80B1FP8V9xfdhmUPmYe0rdhJNOVKJNtMNp2qxS/lNOzEVPAoGACmps +zVsHRiQGTM9tWzRVdxp7H8VmBbs0iyF5jUsV0+FDSy54xmUi8D6IOiW3zjPldo96 +bxKTH4jKTvqCTUKrpfHsXVLUZR2rfv+vR9kmn0ntbuke4g78qn7EY/sqsdyPF7DL +jIZcwGQARPufeAMd9a0bf73XjB+17TIyEvAgELECgYBUyPSNDHl0S02Lmc9SJ24S +TH58t04sRf4cN7ZQdAgPutmnMVSFC6HCrxlAZ94vsG2wEsp9bwrt0bVj15WQFkWj +6aN3uwLNTB0MDkyC9R9DS9Rr2NUMRCJbCXtR4V2E8ueFkEltshTjrccgZPx2fpl3 +2/tjshevKDJFL+y3pShqKg== +-----END PRIVATE KEY----- diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/build.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/build.gradle new file mode 100644 index 00000000000..8a7548629ea --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/build.gradle @@ -0,0 +1,52 @@ +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + id "java" + id "org.springframework.boot" version "3.3.0-SNAPSHOT" +} + +apply plugin: "io.spring.dependency-management" + +repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } +} + +configurations { + app { + extendsFrom(configurations.runtimeClasspath) + } + tomcat { + extendsFrom(app) + } + undertow { + extendsFrom(app) + } +} + +dependencies { + compileOnly("jakarta.servlet:jakarta.servlet-api:6.0.0") + implementation("org.springframework.boot:spring-boot-starter-web") { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' + } + implementation("org.springframework.boot:spring-boot-starter-actuator") + + app(files(sourceSets.main.output)) + app('org.springframework.boot:spring-boot-starter-web') { + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat' + } + tomcat("org.springframework.boot:spring-boot-starter-tomcat") + undertow("org.springframework.boot:spring-boot-starter-undertow") +} + +["tomcat", "undertow"].each { webServer -> + def configurer = { task -> + task.mainClass = "org.springframework.boot.sni.server.SniServerApplication" + task.classpath = configurations.getByName(webServer) + task.archiveClassifier = webServer + task.targetJavaVersion = project.getTargetCompatibility() + } + tasks.register("${webServer}ServerApp", BootJar, configurer) +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/settings.gradle b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/settings.gradle new file mode 100644 index 00000000000..06d9554ad0d --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/settings.gradle @@ -0,0 +1,15 @@ +pluginManagement { + repositories { + maven { url "file:${rootDir}/../int-test-maven-repository"} + mavenCentral() + maven { url "https://repo.spring.io/snapshot" } + maven { url "https://repo.spring.io/milestone" } + } + resolutionStrategy { + eachPlugin { + if (requested.id.id == "org.springframework.boot") { + useModule "org.springframework.boot:spring-boot-gradle-plugin:${requested.version}" + } + } + } +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/HelloController.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/HelloController.java new file mode 100644 index 00000000000..f45ad885a21 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/HelloController.java @@ -0,0 +1,32 @@ +/* + * Copyright 2012-2024 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 jakarta.servlet.http.HttpServletRequest; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HelloController { + + @GetMapping + public String hello(HttpServletRequest request) { + return "Hello from " + request.getRequestURL(); + } + +} \ No newline at end of file diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java new file mode 100644 index 00000000000..5f3cd504ba0 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/java/org/springframework/boot/sni/server/SniServerApplication.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2024 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.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; + +@SpringBootApplication +public class SniServerApplication { + + public static void main(String[] args) { + SpringApplication.run(SniServerApplication.class, args); + } + +} diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/alt/test-hello-alt-server.crt b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/alt/test-hello-alt-server.crt new file mode 100644 index 00000000000..27b08406679 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/alt/test-hello-alt-server.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEZjCCAk6gAwIBAgIUOLEdvpEkuAYUivOxM2sfYLdaHL4wDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDIyNjIwMTIxMFoXDTI1MDIyNTIwMTIxMFow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVaGVsbG8tYWx0 +LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1Hde +3zmOM539oWpp1KNNmGSSqZaYed3qAbrZK8fk4TjN9U+2Yv6glo4XDCRkY61PNmQF +l2+pwnPvscu6CIf+S8DzXa3Y38u0e14RdhfdeJyluul2vJME16S7psCmajqWHAAT +Y/sl1Yjt/5AsEaqjGIDGXDyUzEGrtxZUZmptLpl736Y262JZOmoPBQRhEHudQss6 +utUqYbqeCO/rSIUoIAFtQfm4vJqDq0Jy+VAL9Emzn5SGjR7BZF266Xs0xsGTsBJM +rWCat6j2kJEsYo6IsC0JrFFdv6S8Orp5gBV06gHBlhjIiuvpCi9n9vxN19S+xJgj +YFJORxRnbNchkAwZeQIDAQABo2IwYDALBgNVHQ8EBAMCBaAwEQYJYIZIAYb4QgEB +BAQDAgZAMB0GA1UdDgQWBBTrYnvysRCkQ0NwIw+GCkEFQga7FTAfBgNVHSMEGDAW +gBTHEQ3eDaELFTnoniIdtWY6pVrH1TANBgkqhkiG9w0BAQsFAAOCAgEAT8L/5jqb +64MkeLJQbyXjXpHIaP9dr4NHBeTjXPx8FhuI9RG9ywNzuSlH0DeCQbcdPuMAmrDa +oWoaYzHpuG2saHcCeBdE0HtVCofvKYVQK7vUsT+tKqNbkGzt0cRdwmciDUh6O+Ub +7iOSd7wIjeobEoOxBB+Rk6mRpXRUENAPKMiAuDvpRDFaFyGedzxIeaRp5Gi+LEBh +/2cRZkWcrcWuKNAIszvdZXIl8gUwPU4KVkqX/pALLK5Ax87uniEjl77BQSeuC4++ +QYSnj76VRoyIqt9ZsjVG6VCOwze6M9nVmTR9mgADFsZFZPiqp73q8mOCEpbPfovi +rV8PFZzWZao4s4ulSA3GFlRxaBHe2AcR2pJSchG14OGjW8KWbsOEqLqy+f88ECEP +o9jjc6fq0QeznTZlRIw47UT2ykS1qV6xiKN67iMylCla8ogwK3HvF8pCME7LK99q +qslcLxj2DJQDHTqtDL2fOlxO0RGGuGg4jMfdBwHMjd30nHff5K5UZQARCFKTdbTZ +V5OOdJbDRcicXBQE7sbaTqztjjGKiRxCdvgcV760KJH+gYkzMikqC/xUIv7ac4qX +Q1FHQl+tsxZmM2nP7EapMYZqLle7go8Govze6r0pFEF6MT47ST7m0/ZBWKxm0CaU +uWpyhqoylaIqbR9rojPDSPDvslZvI4KGY8M= +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/alt/test-hello-alt-server.key b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/alt/test-hello-alt-server.key new file mode 100644 index 00000000000..8b67b8b6fa0 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/alt/test-hello-alt-server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDUd17fOY4znf2h +amnUo02YZJKplph53eoButkrx+ThOM31T7Zi/qCWjhcMJGRjrU82ZAWXb6nCc++x +y7oIh/5LwPNdrdjfy7R7XhF2F914nKW66Xa8kwTXpLumwKZqOpYcABNj+yXViO3/ +kCwRqqMYgMZcPJTMQau3FlRmam0umXvfpjbrYlk6ag8FBGEQe51Cyzq61Sphup4I +7+tIhSggAW1B+bi8moOrQnL5UAv0SbOflIaNHsFkXbrpezTGwZOwEkytYJq3qPaQ +kSxijoiwLQmsUV2/pLw6unmAFXTqAcGWGMiK6+kKL2f2/E3X1L7EmCNgUk5HFGds +1yGQDBl5AgMBAAECggEAA/oNVzWck8Vr7zmEAZbiPO1PpsegKFJ0WX6bJ1ahJ2te +Gi6ucJaTgD3omzGTL3UZpnWjenzR5fHa6q5a17iz6cxoFw4fS7u5r8AdqOLezQMh +lv4HsD/ljKO+ChZRBxam+J8yaGIAXfQngEESkhdqNhXdoJxXAfu4sGnv6nrTewEb +sNTgzBceCotwVPBBnN+Ggx2LGp3FWnKqDeaLJM6+vBMDpJYm4BSU21H46bUu9dMm +yM+msZeBZMzAWBVOKoHJxoEE0I6EpwTIPuuq2oqjzQCzzAsKTTFhb51kwMj6k1o4 +Zc11IlDqjNCdB61R+7dqE4LoZWOa/cu26m+lMaZcgQKBgQD4q8zoKLSZ2jlpt1qi +URhupUOyf2BcP8fjygItlMGi3BHY8Rq6DLvoxUMFk0IJAKTolzoVDGxqbrc0Im6k +jed8VQ7SX8soN0aoEyKulv7jCXCRAzoZ2a0oh9cwNLmoEzIoYYSvPSe47HJb1B+T +aN7qQ6h+k0Q0XObgUK7D6WzyswKBgQDaumiM44gECZ7LEW+c16MIOIJWqyJyQsd+ +Bgnn2uHyT9B5ITExsws1oco7GabqZLvC+IpoVsbdS73wnuneLJ40ykqTkwKNLAzB +eUabqiLqLZZ2ACajA86uND7B4hfCeW1srcqU5hqL2/cY4CVJPkBO4mZ6//n8ruZs +OfQhRrbpIwKBgQCPKUlEdvrSgGIBTL/vJsTsHlUFFHQDZ+zKZWgvma6I9i2IOfZr +Gh2serSFJywjRq2qAjY8G/TmqWrrps8QCWo1mDp6PxAUzQ3ugWW8Ic4II00dD0CJ +1VntNZdbd19TNgnwWYQr5wdRXT7RQyQSl5OORvlgNaRUiQ+aIJkczOweJQKBgQCj +yjtIZYoRG/MhNalS1ddr7IUNyZE95uvkXzlDuhDAlywRyN1BzkVyn/kEUK1BkLVZ +xyw9/d1lEbbmXNncWaUO+vzljYy3kmjq6JoLL1h97C1jp7FHGS7IHK9yGJCaPLvI +SkwNPFJcsRdUNWU2d7tIVxlOuijFI2PBX5SE5qNJ6QKBgBex5fpTZ2y4Zn4mcF8s +qjWTAJ8L5xD84PWhZAgbxrawiS+z7S0vMcfxF154JTP89C0EtnMeygE9jFJjvE9E +TAmM2UlIp7p8BKlr15GZjEBn3DqtwV49zo1pLcTjujBCDl+tfR7EVD0VrNb5fely +iM4vJ89vAebkd7G7ro1nbBF6 +-----END PRIVATE KEY----- diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/application.yml b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/application.yml new file mode 100644 index 00000000000..540336014ef --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/application.yml @@ -0,0 +1,42 @@ +spring: + ssl: + bundle: + pem: + default: + keystore: + certificate: "classpath:default/test-hello-server.crt" + private-key: "classpath:default/test-hello-server.key" + truststore: + certificate: "classpath:ca/test-ca.crt" + alt: + keystore: + certificate: "classpath:alt/test-hello-alt-server.crt" + private-key: "classpath:alt/test-hello-alt-server.key" + truststore: + certificate: "classpath:ca/test-ca.crt" + +server: + port: 8443 + ssl: + bundle: "default" + server-name-bundles: + - server-name: "hello.example.com" + bundle: "default" + - server-name: "hello-alt.example.com" + bundle: "alt" + +management: + server: + port: 8444 + ssl: + bundle: "default" + server-name-bundles: + - server-name: "hello.example.com" + bundle: "default" + - server-name: "hello-alt.example.com" + bundle: "alt" + endpoints: + web: + exposure: + include: + - "*" diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/ca/test-ca.crt b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/ca/test-ca.crt new file mode 100644 index 00000000000..b6ec29bca77 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/ca/test-ca.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFjjCCA3agAwIBAgIUE7GfX9Wcx8X4/jDHboW5TUSvHMUwDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDIyNjIwMTIxMFoXDTI1MDIyNTIwMTIxMFow +OzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlmaWNh +dGUgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkQq1 +UWWdc1BuD9sxI4XOrxY9JVITfuejj5hhWg9sjrvov24QjQoTtcOheNzck5MUFcS9 +9RGB4EJWQy17hA4c7XP3KPG9e9xIrZC11ANVUlFqJSSvv/z46KjnvUDgf0PuQKOG +RTWwezNzl0CcJHuLwdJ3yVbtcNwuUh3ououvmmNVtj2dVij2QDYmfPeIwIaAmEhG +t8Oe/dwII/vnVD7iDJ8IApxj8Etk5XZ3DOGTc0ky9h1suY+wQg6amgNEkvPFJQFm +Ej5G920dq2ox/QD/DCMRxmUaqFs1ozqy3ZWDCB7IYYJ6yzE51GMc8JcsLR2vArqs +UJGPAKobdUSkbO0aqQ36+sUc44PGVHtbslJuGyBKVXbJuImhemdGQsLaXeix2XfE +jhFFHi/zsTRtkeiSPkFOdcYFx2j9ts7usdNHTHgNPC11QdFij5+aqmSv/UJw8vRT +kosswUTPK0K5JRs/bx0KWPWyi34m/pCnzsFXALfxdfE9OIPemcmyFsHMQnCOevNu +clf88GNF0w3HpU6MTL1bN3MHc7X/l90HV552GfUY0m4qravRgU3RzzTj9g6GydRz +xFflEi34ELpzQ4FMHdxLAyMtb8mMqGGO8JF9j8vaIhFSn75KEl3qNn1wWhLqBTFY +ptvE2HCY1gz4GIq1AkCjVmgHIPSt4SrjNsSiDjUCAwEAAaOBiTCBhjAdBgNVHQ4E +FgQUxxEN3g2hCxU56J4iHbVmOqVax9UwHwYDVR0jBBgwFoAUxxEN3g2hCxU56J4i +HbVmOqVax9UwDwYDVR0TAQH/BAUwAwEB/zAzBgNVHREELDAqghFoZWxsby5leGFt +cGxlLmNvbYIVaGVsbG8tYWx0LmV4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IC +AQAWowMnIv6eUYLMsJ2tVNy6L2+Wj2Hw/mLb1JlaMRdhiHpHIzNFi+Z7Loq+PXfb +R+He2SVbDieAaI5Uj1KiuH/j6DGNnZgoFKeu6qquuxvnURQATZhcRTq0Hpcuj/df +mICXqPHWNSd1UhR/qwjiW5osqlxnDOEg5LIzxZ1cK62YLC/em3lwzX76dXMJyRA0 +mR32QrqZPeJAg/v5+L5WFJAj9BybcjRLKMzqYCCsVNYrYS4XI7Ms68R5Dyd/iQnY +UTZChIjr8Yq5vgSK9+NPDh5naEJAlMtn9FQJPFeNW7T2y41uQf525+tXFeF73zWO +NktUISrTcG1VmRP3UzWO9taoKDN/vp8iWJ5xNgTNO81NvlZXyjJ3snByVrsTECPv ++MiQbv70aFZ5GE3VOLZVFQYyusRcPGvn46vkk/3wmuUySx/YwY0keg5BEW4JI8Jk +Ia3PtzQDSu888Z4vIXjOCMPHnFk6BAs4Xy2S2q8+XUgrdhPaLl2Yi0IHljUq2Q1f +4bQDJqiIVb3eIYoN/VrcD7v2uy5yKPZdiOLltZFMkb7mcKutYdLOGqAkDNsXaxr1 +zHByIlKXk4XWiPekrnh5tNqt1K0rPfvAmYBVTcGQXFcl8PBR6WEdvh0MldvYGpRV +4jRtDYoYTtRRmVOKUQ9DLTWwv1CIDPe/OFsesQQlodiZhw== +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/default/test-hello-server.crt b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/default/test-hello-server.crt new file mode 100644 index 00000000000..31185387c15 --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/default/test-hello-server.crt @@ -0,0 +1,26 @@ +-----BEGIN CERTIFICATE----- +MIIEYjCCAkqgAwIBAgIUOLEdvpEkuAYUivOxM2sfYLdaHL0wDQYJKoZIhvcNAQEL +BQAwOzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEeMBwGA1UEAwwVQ2VydGlm +aWNhdGUgQXV0aG9yaXR5MB4XDTI0MDIyNjIwMTIxMFoXDTI1MDIyNTIwMTIxMFow +NzEZMBcGA1UECgwQU3ByaW5nIEJvb3QgVGVzdDEaMBgGA1UEAwwRaGVsbG8uZXhh +bXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+mTU1h68x +R6T4cmRFhzw7Uop3V4gLY//8n4I+6qrLjZx4ZCIA/QHZ/80DzSFJlE5P1TP1k7Wc ++MCI5KYjMzAZoR9mBfH6MUArDvPU7eZdxT3QQUMHfpAuLhPhwxsgLlogl5vq74t0 +WV+eipRJIEi7lqjNk1EuO3Nli5Wp6/tRv6v33stUwpWIWnSuxu6r+gS5xp1mpRgD +KBi950ZSUJx+0QcwOs52n9iZTruCxunEDVggj/xUsbdBBCT30bmqS681d7TZTj1R +6Y9PlMiJ4Do5SSs48hZic8ZVsc6TWTVntxUWZotd5NCTk7R4CwL/XdpphuL8mnA0 +eblUXyTkc4xPAgMBAAGjYjBgMAsGA1UdDwQEAwIFoDARBglghkgBhvhCAQEEBAMC +BkAwHQYDVR0OBBYEFArqIg3CQz0XayzQ3mO9Ikl1bmsgMB8GA1UdIwQYMBaAFMcR +Dd4NoQsVOeieIh21ZjqlWsfVMA0GCSqGSIb3DQEBCwUAA4ICAQBJY1n6gOglZGzJ +rVDni0RYWB7xYoBEwQG0UhK6ThZe7bqtnaYA5LL8l7jjtIwSE0fVtkGHe0OaBIRQ +Ntn1cxu15JRdwIUyn0S+ypq3IGPFX5vKaNL1wd7RL227EjtXOnH7ByQ182aNrkp9 +p2nwMiOKhFbK1qsz4zohdcdQq7sYQUAKABneGumk0c6UkmnYKHGkzK0pxrEU2qBN +xc9/NqmBlWyefvcdjAXWB7DBH+KScc7tatkZsOvwlGrWMxyLPDR1WzMZXbHwxPt6 +bHE5fdOPnkel1DJMjjn2/dphTBWOZBrFWuIeEWCzR1sKWgxRUzIzgA3hXKC29p5f +Y1BaXtzrFFD/mP9p7h8VYDPUSuNzDO9iqPf60qL+WN3cbkabJtrhBqzWSbPlzMOH +5ysn7RbVsvwD+OjmXuNhlHq65uf3ftAxCFCsiaFvfaRV9PYdYZP5JAHLZVPFhRhO +qg0KkPKTFrOwBw1w+mDf2ZQ/d/9Hd9PnknutmkBPwaqb0dRZcCUFLy/gPMrLkQse +P3naqOpHGFPLbLIKbQ0vyTT/6I0/lglZJ2ijk52a2gbz2BMjL+5b+A6MP2GGy166 +HxMKRbSQ3FgGGVuJd1ggUX8/juuP1Ykw58yS0OmfT2ryHLxfqF0Pr7uswNnPyGjZ +K/2fVlQBLbIfMjtmuoFvO01llElZHw== +-----END CERTIFICATE----- diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/default/test-hello-server.key b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/default/test-hello-server.key new file mode 100644 index 00000000000..bf4f2bfad2b --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/spring-boot-sni-servlet-app/src/main/resources/default/test-hello-server.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC+mTU1h68xR6T4 +cmRFhzw7Uop3V4gLY//8n4I+6qrLjZx4ZCIA/QHZ/80DzSFJlE5P1TP1k7Wc+MCI +5KYjMzAZoR9mBfH6MUArDvPU7eZdxT3QQUMHfpAuLhPhwxsgLlogl5vq74t0WV+e +ipRJIEi7lqjNk1EuO3Nli5Wp6/tRv6v33stUwpWIWnSuxu6r+gS5xp1mpRgDKBi9 +50ZSUJx+0QcwOs52n9iZTruCxunEDVggj/xUsbdBBCT30bmqS681d7TZTj1R6Y9P +lMiJ4Do5SSs48hZic8ZVsc6TWTVntxUWZotd5NCTk7R4CwL/XdpphuL8mnA0eblU +XyTkc4xPAgMBAAECggEAJ/SBGYQknz2IIUcFqyei4kK24Ta5v72KW8BqctsJy9sX +WouPL0ramQMNTMczO7P5yLWGi2wYDdx9rBTWmRlxc2X56Y7Ef7DUZVJgnhnzCWRA +RYhwz0DiY7PoGhMm/BOLdDqkBleKEe1sZJVjaYL5jE2UfGfuBDWVRsvAp5rfF+8r +gp5ewvaZUeFC09/aN+1k84wxA0njAmjiYDSId5Ymn0AxHDeoESmysMfr48p39/XH +5YiPne3OlrkEFlN1NYvPGf09ifyQYH91HuTrzKTX+jhvYM7E+LzSTrbcwlMUKDem +5CnVAkAJwnMhQDAOwAn52vPEqcru9d7jq+nkf+LQ8QKBgQD50mr9ApGHLN5uZ1Zk +1BRkTzAL4+yDYq0V5TiMaN7WNGTmIEUjEvnmaegxw37lBmmGhn6EPzSN/ZszgQIj +EQohxfU8lQ8ftBdnhmWTSs2Z57fHXItbvvKVxVRHw9oX+ZVj3akqKD9K6UAqWJlJ ++L4H+yHMIByS0ICnZcxhnXpSHwKBgQDDT9sRM2SCCULvpVTwqye7b7gW5Dlcv300 +X5CS1av7id1QMFcU3nohp7KzTkxRbeuGL2QF7wVa5NBMh7+GevnRrRtEImfcaA5n +zhDQDfOUg0/5csOCvL0WTIkHEpTEiqd2SVaaGzHvy09mLCDJxDpc9LuTZiDP9/JL +vzIcb0Nf0QKBgHnk0nEbFLjZCrrhzwSpej2raa0Ti+5bckqxqlLQRJJNxEGI01MW +yjpDyJinY74Jz+lkrEyIrnLtoBGUS9+iS8hI16y0qkl0zMqlh+BDamhC6Kfsns6o +L6MmQkY16K80B1FP8V9xfdhmUPmYe0rdhJNOVKJNtMNp2qxS/lNOzEVPAoGACmps +zVsHRiQGTM9tWzRVdxp7H8VmBbs0iyF5jUsV0+FDSy54xmUi8D6IOiW3zjPldo96 +bxKTH4jKTvqCTUKrpfHsXVLUZR2rfv+vR9kmn0ntbuke4g78qn7EY/sqsdyPF7DL +jIZcwGQARPufeAMd9a0bf73XjB+17TIyEvAgELECgYBUyPSNDHl0S02Lmc9SJ24S +TH58t04sRf4cN7ZQdAgPutmnMVSFC6HCrxlAZ94vsG2wEsp9bwrt0bVj15WQFkWj +6aN3uwLNTB0MDkyC9R9DS9Rr2NUMRCJbCXtR4V2E8ueFkEltshTjrccgZPx2fpl3 +2/tjshevKDJFL+y3pShqKg== +-----END PRIVATE KEY----- diff --git a/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/src/intTest/java/org/springframework/boot/sni/SniIntegrationTests.java b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/src/intTest/java/org/springframework/boot/sni/SniIntegrationTests.java new file mode 100644 index 00000000000..4fe4c177d5b --- /dev/null +++ b/spring-boot-tests/spring-boot-integration-tests/spring-boot-sni-tests/src/intTest/java/org/springframework/boot/sni/SniIntegrationTests.java @@ -0,0 +1,129 @@ +/* + * Copyright 2012-2024 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; + +import java.io.File; +import java.time.Duration; +import java.util.Map; + +import org.awaitility.Awaitility; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.images.builder.ImageFromDockerfile; +import org.testcontainers.junit.jupiter.Testcontainers; + +import org.springframework.util.Assert; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for SSL configuration with SNI. + * + * @author Scott Frederick + */ +@Testcontainers(disabledWithoutDocker = true) +class SniIntegrationTests { + + private static final Map SERVER_START_MESSAGES = Map.ofEntries(Map.entry("netty", "Netty started"), + Map.entry("tomcat", "Tomcat initialized"), Map.entry("undertow", "starting server: Undertow")); + + public static final String PRIMARY_SERVER_NAME = "hello.example.com"; + + public static final String ALT_SERVER_NAME = "hello-alt.example.com"; + + private static final Integer SERVER_PORT = 8443; + + private static final Network SHARED_NETWORK = Network.newNetwork(); + + @ParameterizedTest + @CsvSource({ "reactive,netty", "reactive,tomcat", "servlet,tomcat", "reactive,undertow", "servlet,undertow" }) + void home(String webStack, String server) { + try (ApplicationContainer serverContainer = new ServerApplicationContainer(webStack, server)) { + serverContainer.start(); + Awaitility.await().atMost(Duration.ofSeconds(60)).until(serverContainer::isRunning); + String serverLogs = serverContainer.getLogs(); + assertThat(serverLogs).contains(SERVER_START_MESSAGES.get(server)); + try (ApplicationContainer clientContainer = new ClientApplicationContainer()) { + clientContainer.start(); + Awaitility.await().atMost(Duration.ofSeconds(60)).until(() -> !clientContainer.isRunning()); + String clientLogs = clientContainer.getLogs(); + assertServerCalledWithName(clientLogs, PRIMARY_SERVER_NAME); + assertServerCalledWithName(clientLogs, ALT_SERVER_NAME); + clientContainer.stop(); + } + serverContainer.stop(); + } + } + + private void assertServerCalledWithName(String clientLogs, String serverName) { + assertThat(clientLogs).contains("Calling server at 'https://" + serverName + ":8443/'") + .contains("Hello from https://" + serverName + ":8443/"); + assertThat(clientLogs).contains("Calling server actuator at 'https://" + serverName + ":8444/actuator/health'") + .contains("{\"status\":\"UP\"}"); + } + + static final class ClientApplicationContainer extends ApplicationContainer { + + ClientApplicationContainer() { + super("spring-boot-sni-client-app", "", PRIMARY_SERVER_NAME, ALT_SERVER_NAME); + } + + } + + static final class ServerApplicationContainer extends ApplicationContainer { + + ServerApplicationContainer(String webStack, String server) { + super("spring-boot-sni-" + webStack + "-app", "-" + server); + withNetworkAliases(PRIMARY_SERVER_NAME, ALT_SERVER_NAME); + } + + } + + static class ApplicationContainer extends GenericContainer { + + protected ApplicationContainer(String appName, String fileSuffix, String... entryPointArgs) { + super(new ImageFromDockerfile().withFileFromFile("spring-boot.jar", findJarFile(appName, fileSuffix)) + .withDockerfileFromBuilder((builder) -> builder.from("eclipse-temurin:17-jre-jammy") + .add("spring-boot.jar", "/spring-boot.jar") + .entryPoint(buildEntryPoint(entryPointArgs)))); + withExposedPorts(SERVER_PORT); + withStartupTimeout(Duration.ofMinutes(2)); + withStartupAttempts(3); + withNetwork(SHARED_NETWORK); + withNetworkMode(SHARED_NETWORK.getId()); + } + + private static File findJarFile(String appName, String fileSuffix) { + String path = String.format("build/%1$s/build/libs/%1$s%2$s.jar", appName, fileSuffix); + File jar = new File(path); + Assert.state(jar.isFile(), () -> "Could not find " + path); + return jar; + } + + private static String buildEntryPoint(String... args) { + StringBuilder builder = new StringBuilder().append("java").append(" -jar").append(" /spring-boot.jar"); + for (String arg : args) { + builder.append(" ").append(arg); + } + return builder.toString(); + } + + } + +}