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
This commit is contained in:
Scott Frederick 2024-03-28 14:12:20 -05:00
parent d726e523f5
commit ad79c373f8
48 changed files with 1322 additions and 55 deletions

View File

@ -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"

View File

@ -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."

View File

@ -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."

View File

@ -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.

View File

@ -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<CloseableChannel> 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<String, SslBundle> 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<String, SslBundle> serverNameSslBundles) {
super(null, clientAuth, sslBundle, serverNameSslBundles);
this.sslBundle = sslBundle;
}

View File

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

View File

@ -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<HttpProtocol> protocols = new ArrayList<>();
protocols.add(HttpProtocol.HTTP11);

View File

@ -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<String, SslProvider> serverNameSslProviders;
private volatile SslBundle sslBundle;
public SslServerCustomizer(Http2 http2, Ssl.ClientAuth clientAuth, SslBundle sslBundle) {
public SslServerCustomizer(Http2 http2, Ssl.ClientAuth clientAuth, SslBundle sslBundle,
Map<String, SslBundle> 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<String, SslProvider> createServerNameSslProviders(Map<String, SslBundle> serverNameSslBundles) {
Map<String, SslProvider> serverNameSslProviders = new HashMap<>();
serverNameSslBundles
.forEach((hostName, sslBundle) -> serverNameSslProviders.put(hostName, createSslProvider(sslBundle)));
return serverNameSslProviders;
}
private SslProvider createSslProvider(SslBundle sslBundle) {

View File

@ -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<String, SslBundle> 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<String, SslBundle> 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();

View File

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

View File

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

View File

@ -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<String, SslBundle> serverNameSslBundles;
SslBuilderCustomizer(int port, InetAddress address, ClientAuth clientAuth, SslBundle sslBundle,
Map<String, SslBundle> 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";

View File

@ -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<HttpHandlerFactory> httpHandlerFactories = this.delegate.createHttpHandlerFactories(this,
(next) -> new UndertowHttpHandlerAdapter(httpHandler));
return new UndertowWebServer(builder, httpHandlerFactories, getPort() >= 0);

View File

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

View File

@ -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<SslBundle> sslBundleSupplier) {
Builder createBuilder(AbstractConfigurableWebServerFactory factory, Supplier<SslBundle> sslBundleSupplier,
Supplier<Map<String, SslBundle>> 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 {

View File

@ -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<String, SslBundle> 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

View File

@ -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<ServerNameSslBundle> 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<ServerNameSslBundle> getServerNameBundles() {
return this.serverNameBundles;
}
public void setServerNameBundles(List<ServerNameSslBundle> 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.
*/

View File

@ -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;

View File

@ -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<ServerNameSslBundle> 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();

View File

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

View File

@ -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
}

View File

@ -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

View File

@ -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")
}

View File

@ -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}"
}
}
}
}

View File

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

View File

@ -0,0 +1,7 @@
spring:
ssl:
bundle:
pem:
server:
truststore:
certificate: "classpath:ca/test-ca.crt"

View File

@ -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-----

View File

@ -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)
}

View File

@ -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}"
}
}
}
}

View File

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

View File

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

View File

@ -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-----

View File

@ -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-----

View File

@ -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:
- "*"

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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)
}

View File

@ -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}"
}
}
}
}

View File

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

View File

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

View File

@ -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-----

View File

@ -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-----

View File

@ -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:
- "*"

View File

@ -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-----

View File

@ -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-----

View File

@ -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-----

View File

@ -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<String, String> 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<ApplicationContainer> {
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();
}
}
}