Create spring-boot-web-server module

This commit is contained in:
Andy Wilkinson
2025-05-01 13:40:49 +01:00
committed by Phillip Webb
parent 0cf76bf43b
commit 96bee8e034
202 changed files with 374 additions and 271 deletions

View File

@@ -0,0 +1,822 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.server.reactive;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.time.Duration;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import javax.net.ssl.KeyManagerFactory;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.awaitility.Awaitility;
import org.eclipse.jetty.client.ContentResponse;
import org.eclipse.jetty.client.StringRequestContent;
import org.eclipse.jetty.http2.client.HTTP2Client;
import org.eclipse.jetty.http2.client.transport.HttpClientTransportOverHTTP2;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import reactor.netty.NettyPipeline;
import reactor.netty.http.Http11SslContextSpec;
import reactor.netty.http.client.HttpClient;
import reactor.netty.tcp.SslProvider.GenericSslContextSpec;
import reactor.test.StepVerifier;
import org.springframework.boot.testsupport.classpath.resources.ResourcePath;
import org.springframework.boot.testsupport.classpath.resources.WithPackageResources;
import org.springframework.boot.web.server.Compression;
import org.springframework.boot.web.server.GracefulShutdownResult;
import org.springframework.boot.web.server.Http2;
import org.springframework.boot.web.server.Shutdown;
import org.springframework.boot.web.server.Ssl;
import org.springframework.boot.web.server.WebServer;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.unit.DataSize;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientRequestException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Base for testing classes that extends {@link AbstractReactiveWebServerFactory}.
*
* @author Brian Clozel
* @author Scott Frederick
*/
public abstract class AbstractReactiveWebServerFactoryTests {
protected WebServer webServer;
@AfterEach
void tearDown() {
if (this.webServer != null) {
try {
this.webServer.stop();
try {
this.webServer.destroy();
}
catch (Exception ex) {
// Ignore
}
}
catch (Exception ex) {
// Ignore
}
}
}
protected abstract ConfigurableReactiveWebServerFactory getFactory();
@Test
void specificPort() throws Exception {
ConfigurableReactiveWebServerFactory factory = getFactory();
int specificPort = doWithRetry(() -> {
factory.setPort(0);
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
return this.webServer.getPort();
});
Mono<String> result = getWebClient(this.webServer.getPort()).build()
.post()
.uri("/test")
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue("Hello World"))
.retrieve()
.bodyToMono(String.class);
assertThat(result.block(Duration.ofSeconds(30))).isEqualTo("Hello World");
assertThat(this.webServer.getPort()).isEqualTo(specificPort);
}
@Test
protected void restartAfterStop() throws Exception {
ConfigurableReactiveWebServerFactory factory = getFactory();
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
int port = this.webServer.getPort();
assertThat(getResponse(port, "/test")).isEqualTo("Hello World");
this.webServer.stop();
assertThatException().isThrownBy(() -> getResponse(port, "/test"));
this.webServer.start();
assertThat(getResponse(this.webServer.getPort(), "/test")).isEqualTo("Hello World");
}
private String getResponse(int port, String uri) {
WebClient webClient = getWebClient(port).build();
Mono<String> result = webClient.post()
.uri(uri)
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue("Hello World"))
.retrieve()
.bodyToMono(String.class);
return result.block(Duration.ofSeconds(30));
}
@Test
void portIsMinusOneWhenConnectionIsClosed() {
ConfigurableReactiveWebServerFactory factory = getFactory();
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
assertThat(this.webServer.getPort()).isGreaterThan(0);
this.webServer.destroy();
assertThat(this.webServer.getPort()).isEqualTo(-1);
}
@Test
@WithPackageResources("test.jks")
void basicSslFromClassPath() {
testBasicSslWithKeyStore("classpath:test.jks", "password");
}
@Test
@WithPackageResources("test.jks")
void basicSslFromFileSystem(@ResourcePath("test.jks") String keyStore) {
testBasicSslWithKeyStore(keyStore, "password");
}
protected final void testBasicSslWithKeyStore(String keyStore, String keyPassword) {
ConfigurableReactiveWebServerFactory factory = getFactory();
Ssl ssl = new Ssl();
ssl.setKeyStore(keyStore);
ssl.setKeyPassword(keyPassword);
ssl.setKeyStorePassword("secret");
factory.setSsl(ssl);
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
ReactorClientHttpConnector connector = buildTrustAllSslConnector();
WebClient client = WebClient.builder()
.baseUrl("https://localhost:" + this.webServer.getPort())
.clientConnector(connector)
.build();
Mono<String> result = client.post()
.uri("/test")
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue("Hello World"))
.retrieve()
.bodyToMono(String.class);
assertThat(result.block(Duration.ofSeconds(30))).isEqualTo("Hello World");
}
@Test
@WithPackageResources("test.jks")
void sslWithValidAlias() {
String keyStore = "classpath:test.jks";
String keyPassword = "password";
ConfigurableReactiveWebServerFactory factory = getFactory();
Ssl ssl = new Ssl();
ssl.setKeyStore(keyStore);
ssl.setKeyStorePassword("secret");
ssl.setKeyPassword(keyPassword);
ssl.setKeyAlias("test-alias");
factory.setSsl(ssl);
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
ReactorClientHttpConnector connector = buildTrustAllSslConnector();
WebClient client = WebClient.builder()
.baseUrl("https://localhost:" + this.webServer.getPort())
.clientConnector(connector)
.build();
Mono<String> result = client.post()
.uri("/test")
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue("Hello World"))
.retrieve()
.bodyToMono(String.class);
StepVerifier.create(result).expectNext("Hello World").expectComplete().verify(Duration.ofSeconds(30));
}
@Test
@WithPackageResources("test.jks")
void sslWithInvalidAliasFailsDuringStartup() {
String keyStore = "classpath:test.jks";
String keyPassword = "password";
ConfigurableReactiveWebServerFactory factory = getFactory();
Ssl ssl = new Ssl();
ssl.setKeyStore(keyStore);
ssl.setKeyPassword(keyPassword);
ssl.setKeyAlias("test-alias-404");
factory.setSsl(ssl);
assertThatSslWithInvalidAliasCallFails(() -> factory.getWebServer(new EchoHandler()).start());
}
protected void assertThatSslWithInvalidAliasCallFails(ThrowingCallable call) {
assertThatException().isThrownBy(call)
.withStackTraceContaining("Keystore does not contain alias 'test-alias-404'");
}
protected ReactorClientHttpConnector buildTrustAllSslConnector() {
GenericSslContextSpec<?> sslContextSpec = Http11SslContextSpec.forClient()
.configure((builder) -> builder.sslProvider(SslProvider.JDK)
.trustManager(InsecureTrustManagerFactory.INSTANCE));
HttpClient client = HttpClient.create().wiretap(true).secure((spec) -> spec.sslContext(sslContextSpec));
return new ReactorClientHttpConnector(client);
}
@Test
@WithPackageResources("test.jks")
void sslWantsClientAuthenticationSucceedsWithClientCertificate() throws Exception {
Ssl ssl = new Ssl();
ssl.setClientAuth(Ssl.ClientAuth.WANT);
ssl.setKeyStore("classpath:test.jks");
ssl.setKeyPassword("password");
ssl.setKeyStorePassword("secret");
ssl.setTrustStore("classpath:test.jks");
testClientAuthSuccess(ssl, buildTrustAllSslWithClientKeyConnector("test.jks", "password"));
}
@Test
@WithPackageResources("test.jks")
void sslWantsClientAuthenticationSucceedsWithoutClientCertificate() {
Ssl ssl = new Ssl();
ssl.setClientAuth(Ssl.ClientAuth.WANT);
ssl.setKeyStore("classpath:test.jks");
ssl.setKeyPassword("password");
ssl.setTrustStore("classpath:test.jks");
ssl.setKeyStorePassword("secret");
testClientAuthSuccess(ssl, buildTrustAllSslConnector());
}
protected ReactorClientHttpConnector buildTrustAllSslWithClientKeyConnector(String keyStore,
String keyStorePassword) throws Exception {
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream stream = new ClassPathResource(keyStore).getInputStream()) {
clientKeyStore.load(stream, "secret".toCharArray());
}
KeyManagerFactory clientKeyManagerFactory = KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
clientKeyManagerFactory.init(clientKeyStore, keyStorePassword.toCharArray());
GenericSslContextSpec<?> sslContextSpec = Http11SslContextSpec.forClient()
.configure((builder) -> builder.sslProvider(SslProvider.JDK)
.trustManager(InsecureTrustManagerFactory.INSTANCE)
.keyManager(clientKeyManagerFactory));
HttpClient client = HttpClient.create().wiretap(true).secure((spec) -> spec.sslContext(sslContextSpec));
return new ReactorClientHttpConnector(client);
}
protected void testClientAuthSuccess(Ssl sslConfiguration, ReactorClientHttpConnector clientConnector) {
ConfigurableReactiveWebServerFactory factory = getFactory();
factory.setSsl(sslConfiguration);
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
WebClient client = WebClient.builder()
.baseUrl("https://localhost:" + this.webServer.getPort())
.clientConnector(clientConnector)
.build();
Mono<String> result = client.post()
.uri("/test")
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue("Hello World"))
.retrieve()
.bodyToMono(String.class);
assertThat(result.block(Duration.ofSeconds(30))).isEqualTo("Hello World");
}
@Test
@WithPackageResources("test.jks")
void sslNeedsClientAuthenticationSucceedsWithClientCertificate() throws Exception {
Ssl ssl = new Ssl();
ssl.setClientAuth(Ssl.ClientAuth.NEED);
ssl.setKeyStore("classpath:test.jks");
ssl.setKeyStorePassword("secret");
ssl.setKeyPassword("password");
ssl.setTrustStore("classpath:test.jks");
testClientAuthSuccess(ssl, buildTrustAllSslWithClientKeyConnector("test.jks", "password"));
}
@Test
@WithPackageResources("test.jks")
void sslNeedsClientAuthenticationFailsWithoutClientCertificate() {
Ssl ssl = new Ssl();
ssl.setClientAuth(Ssl.ClientAuth.NEED);
ssl.setKeyStore("classpath:test.jks");
ssl.setKeyStorePassword("secret");
ssl.setKeyPassword("password");
ssl.setTrustStore("classpath:test.jks");
testClientAuthFailure(ssl, buildTrustAllSslConnector());
}
@Test
@WithPackageResources({ "test-cert.pem", "test-key.pem", "test.p12" })
void sslWithPemCertificates() throws Exception {
Ssl ssl = new Ssl();
ssl.setClientAuth(Ssl.ClientAuth.NEED);
ssl.setCertificate("classpath:test-cert.pem");
ssl.setCertificatePrivateKey("classpath:test-key.pem");
ssl.setTrustCertificate("classpath:test-cert.pem");
testClientAuthSuccess(ssl, buildTrustAllSslWithClientKeyConnector("test.p12", "secret"));
}
protected void testClientAuthFailure(Ssl sslConfiguration, ReactorClientHttpConnector clientConnector) {
ConfigurableReactiveWebServerFactory factory = getFactory();
factory.setSsl(sslConfiguration);
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
WebClient client = WebClient.builder()
.baseUrl("https://localhost:" + this.webServer.getPort())
.clientConnector(clientConnector)
.build();
Mono<String> result = client.post()
.uri("/test")
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue("Hello World"))
.retrieve()
.bodyToMono(String.class);
StepVerifier.create(result).expectError(WebClientRequestException.class).verify(Duration.ofSeconds(10));
}
protected WebClient.Builder getWebClient(int port) {
return getWebClient(HttpClient.create().wiretap(true), port);
}
protected WebClient.Builder getWebClient(HttpClient client, int port) {
InetSocketAddress address = new InetSocketAddress(port);
String baseUrl = "http://" + address.getHostString() + ":" + address.getPort();
return WebClient.builder().clientConnector(new ReactorClientHttpConnector(client)).baseUrl(baseUrl);
}
@Test
protected void compressionOfResponseToGetRequest() {
WebClient client = prepareCompressionTest();
ResponseEntity<Void> response = client.get().retrieve().toBodilessEntity().block(Duration.ofSeconds(30));
assertResponseIsCompressed(response);
}
@Test
protected void compressionOfResponseToPostRequest() {
WebClient client = prepareCompressionTest();
ResponseEntity<Void> response = client.post().retrieve().toBodilessEntity().block(Duration.ofSeconds(30));
assertResponseIsCompressed(response);
}
@Test
void noCompressionForSmallResponse() {
Compression compression = new Compression();
compression.setEnabled(true);
compression.setMinResponseSize(DataSize.ofBytes(3001));
WebClient client = prepareCompressionTest(compression);
ResponseEntity<Void> response = client.get().retrieve().toBodilessEntity().block(Duration.ofSeconds(30));
assertResponseIsNotCompressed(response);
}
@Test
void noCompressionForMimeType() {
Compression compression = new Compression();
compression.setEnabled(true);
compression.setMimeTypes(new String[] { "application/json" });
WebClient client = prepareCompressionTest(compression);
ResponseEntity<Void> response = client.get().retrieve().toBodilessEntity().block(Duration.ofSeconds(30));
assertResponseIsNotCompressed(response);
}
@Test
protected void noCompressionForUserAgent() {
Compression compression = new Compression();
compression.setEnabled(true);
compression.setExcludedUserAgents(new String[] { "testUserAgent" });
WebClient client = prepareCompressionTest(compression);
ResponseEntity<Void> response = client.get()
.header("User-Agent", "testUserAgent")
.retrieve()
.toBodilessEntity()
.block(Duration.ofSeconds(30));
assertResponseIsNotCompressed(response);
}
@Test
void noCompressionForResponseWithInvalidContentType() {
Compression compression = new Compression();
compression.setEnabled(true);
compression.setMimeTypes(new String[] { "application/json" });
WebClient client = prepareCompressionTest(compression, "test~plain");
ResponseEntity<Void> response = client.get().retrieve().toBodilessEntity().block(Duration.ofSeconds(30));
assertResponseIsNotCompressed(response);
}
@Test
void whenSslIsEnabledAndNoKeyStoreIsConfiguredThenServerFailsToStart() {
assertThatIllegalStateException().isThrownBy(() -> testBasicSslWithKeyStore(null, null))
.withMessageContaining("SSL is enabled but no trust material is configured");
}
@Test
void whenThereAreNoInFlightRequestsShutDownGracefullyReturnsTrueBeforePeriodElapses() throws Exception {
ConfigurableReactiveWebServerFactory factory = getFactory();
factory.setShutdown(Shutdown.GRACEFUL);
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
AtomicReference<GracefulShutdownResult> result = new AtomicReference<>();
this.webServer.shutDownGracefully(result::set);
Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> GracefulShutdownResult.IDLE == result.get());
}
@Test
void whenARequestRemainsInFlightThenShutDownGracefullyDoesNotInvokeCallbackUntilTheRequestCompletes()
throws Exception {
ConfigurableReactiveWebServerFactory factory = getFactory();
factory.setShutdown(Shutdown.GRACEFUL);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
Mono<ResponseEntity<Void>> request = getWebClient(this.webServer.getPort()).build()
.get()
.retrieve()
.toBodilessEntity();
AtomicReference<ResponseEntity<Void>> responseReference = new AtomicReference<>();
CountDownLatch responseLatch = new CountDownLatch(1);
request.subscribe((response) -> {
responseReference.set(response);
responseLatch.countDown();
});
blockingHandler.awaitQueue();
AtomicReference<GracefulShutdownResult> result = new AtomicReference<>();
this.webServer.shutDownGracefully(result::set);
assertThat(responseReference.get()).isNull();
blockingHandler.completeOne();
assertThat(responseLatch.await(5, TimeUnit.SECONDS)).isTrue();
Awaitility.await().atMost(Duration.ofSeconds(30)).until(() -> GracefulShutdownResult.IDLE == result.get());
}
@Test
void givenAnInflightRequestWhenTheServerIsStoppedThenGracefulShutdownCallbackIsCalledWithRequestsActive()
throws Exception {
ConfigurableReactiveWebServerFactory factory = getFactory();
factory.setShutdown(Shutdown.GRACEFUL);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
Mono<ResponseEntity<Void>> request = getWebClient(this.webServer.getPort()).build()
.get()
.retrieve()
.toBodilessEntity();
AtomicReference<ResponseEntity<Void>> responseReference = new AtomicReference<>();
CountDownLatch responseLatch = new CountDownLatch(1);
request.subscribe((response) -> {
responseReference.set(response);
responseLatch.countDown();
});
blockingHandler.awaitQueue();
AtomicReference<GracefulShutdownResult> result = new AtomicReference<>();
this.webServer.shutDownGracefully(result::set);
assertThat(responseReference.get()).isNull();
try {
this.webServer.stop();
}
catch (Exception ex) {
// Continue
}
System.out.println("Stopped");
Awaitility.await()
.atMost(Duration.ofSeconds(5))
.until(() -> GracefulShutdownResult.REQUESTS_ACTIVE == result.get());
blockingHandler.completeOne();
}
@Test
void whenARequestIsActiveAfterGracefulShutdownEndsThenStopWillComplete() throws InterruptedException {
ConfigurableReactiveWebServerFactory factory = getFactory();
factory.setShutdown(Shutdown.GRACEFUL);
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
Mono<ResponseEntity<Void>> request = getWebClient(this.webServer.getPort()).build()
.get()
.retrieve()
.toBodilessEntity();
AtomicReference<ResponseEntity<Void>> responseReference = new AtomicReference<>();
CountDownLatch responseLatch = new CountDownLatch(1);
request.subscribe((response) -> {
responseReference.set(response);
responseLatch.countDown();
});
blockingHandler.awaitQueue();
AtomicReference<GracefulShutdownResult> result = new AtomicReference<>();
this.webServer.shutDownGracefully(result::set);
this.webServer.stop();
Awaitility.await()
.atMost(Duration.ofSeconds(30))
.until(() -> GracefulShutdownResult.REQUESTS_ACTIVE == result.get());
blockingHandler.completeOne();
}
@Test
void whenARequestIsActiveThenStopWillComplete() throws InterruptedException {
ConfigurableReactiveWebServerFactory factory = getFactory();
BlockingHandler blockingHandler = new BlockingHandler();
this.webServer = factory.getWebServer(blockingHandler);
this.webServer.start();
Mono<ResponseEntity<Void>> request = getWebClient(this.webServer.getPort()).build()
.get()
.retrieve()
.toBodilessEntity();
AtomicReference<ResponseEntity<Void>> responseReference = new AtomicReference<>();
CountDownLatch responseLatch = new CountDownLatch(1);
request.subscribe((response) -> {
responseReference.set(response);
responseLatch.countDown();
});
blockingHandler.awaitQueue();
try {
this.webServer.stop();
}
catch (Exception ex) {
// Continue
}
blockingHandler.completeOne();
}
@Test
protected void whenHttp2IsEnabledAndSslIsDisabledThenH2cCanBeUsed() throws Exception {
ConfigurableReactiveWebServerFactory factory = getFactory();
Http2 http2 = new Http2();
http2.setEnabled(true);
factory.setHttp2(http2);
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
try (org.eclipse.jetty.client.HttpClient client = new org.eclipse.jetty.client.HttpClient(
new HttpClientTransportOverHTTP2(new HTTP2Client()))) {
client.start();
ContentResponse response = client.POST("http://localhost:" + this.webServer.getPort())
.body(new StringRequestContent("text/plain", "Hello World"))
.send();
assertThat(response.getStatus()).isEqualTo(HttpStatus.OK.value());
assertThat(response.getContentAsString()).isEqualTo("Hello World");
}
}
@Test
protected void whenHttp2IsEnabledAndSslIsDisabledThenHttp11CanStillBeUsed() {
ConfigurableReactiveWebServerFactory factory = getFactory();
Http2 http2 = new Http2();
http2.setEnabled(true);
factory.setHttp2(http2);
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
Mono<String> result = getWebClient(this.webServer.getPort()).build()
.post()
.uri("/test")
.contentType(MediaType.TEXT_PLAIN)
.body(BodyInserters.fromValue("Hello World"))
.retrieve()
.bodyToMono(String.class);
assertThat(result.block(Duration.ofSeconds(30))).isEqualTo("Hello World");
}
@Test
void startedLogMessageWithSinglePort() {
ConfigurableReactiveWebServerFactory factory = getFactory();
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
assertThat(startedLogMessage()).matches(
"(Jetty|Netty|Tomcat|Undertow) started on port " + this.webServer.getPort() + " \\(http(/1.1)?\\)");
}
@Test
protected void startedLogMessageWithMultiplePorts() {
ConfigurableReactiveWebServerFactory factory = getFactory();
addConnector(0, factory);
this.webServer = factory.getWebServer(new EchoHandler());
this.webServer.start();
assertThat(startedLogMessage()).matches("(Jetty|Tomcat|Undertow) started on ports " + this.webServer.getPort()
+ " \\(http(/1.1)?\\), [0-9]+ \\(http(/1.1)?\\)");
}
protected WebClient prepareCompressionTest() {
Compression compression = new Compression();
compression.setEnabled(true);
return prepareCompressionTest(compression);
}
protected WebClient prepareCompressionTest(Compression compression) {
return prepareCompressionTest(compression, MediaType.TEXT_PLAIN_VALUE);
}
protected WebClient prepareCompressionTest(Compression compression, String responseContentType) {
ConfigurableReactiveWebServerFactory factory = getFactory();
factory.setCompression(compression);
this.webServer = factory.getWebServer(new CharsHandler(3000, responseContentType));
this.webServer.start();
HttpClient client = HttpClient.create()
.wiretap(true)
.compress(true)
.doOnConnected((connection) -> connection.channel()
.pipeline()
.addBefore(NettyPipeline.HttpDecompressor, "CompressionTest", new CompressionDetectionHandler()));
return getWebClient(client, this.webServer.getPort()).build();
}
protected void assertResponseIsCompressed(ResponseEntity<Void> response) {
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getHeaders().getFirst("X-Test-Compressed")).isEqualTo("true");
}
protected void assertResponseIsNotCompressed(ResponseEntity<Void> response) {
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getHeaders().headerNames()).doesNotContain("X-Test-Compressed");
}
protected void assertForwardHeaderIsUsed(ConfigurableReactiveWebServerFactory factory) {
this.webServer = factory.getWebServer(new XForwardedHandler());
this.webServer.start();
String body = getWebClient(this.webServer.getPort()).build()
.get()
.header("X-Forwarded-Proto", "https")
.retrieve()
.bodyToMono(String.class)
.block(Duration.ofSeconds(30));
assertThat(body).isEqualTo("https");
}
private <T> T doWithRetry(Callable<T> action) throws Exception {
Exception lastFailure = null;
for (int i = 0; i < 10; i++) {
try {
return action.call();
}
catch (Exception ex) {
lastFailure = ex;
}
}
throw new IllegalStateException("Action was not successful in 10 attempts", lastFailure);
}
protected final void doWithBlockedPort(BlockedPortAction action) throws Exception {
ServerSocket serverSocket = new ServerSocket();
try (serverSocket) {
int blockedPort = doWithRetry(() -> {
serverSocket.bind(null);
return serverSocket.getLocalPort();
});
action.run(blockedPort);
}
}
protected abstract String startedLogMessage();
protected abstract void addConnector(int port, ConfigurableReactiveWebServerFactory factory);
public interface BlockedPortAction {
void run(int port);
}
protected static class EchoHandler implements HttpHandler {
public EchoHandler() {
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
response.setStatusCode(HttpStatus.OK);
return response.writeWith(request.getBody());
}
}
protected static class BlockingHandler implements HttpHandler {
private final BlockingQueue<Sinks.Empty<Void>> processors = new ArrayBlockingQueue<>(10);
private volatile boolean blocking = true;
public BlockingHandler() {
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
if (this.blocking) {
Sinks.Empty<Void> completion = Sinks.empty();
this.processors.add(completion);
return completion.asMono().then(Mono.empty());
}
return Mono.empty();
}
public void completeOne() {
try {
Sinks.Empty<Void> processor = this.processors.take();
processor.tryEmitEmpty();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
public void awaitQueue() throws InterruptedException {
while (this.processors.isEmpty()) {
Thread.sleep(100);
}
}
public void stopBlocking() {
this.blocking = false;
this.processors.forEach(Sinks.Empty::tryEmitEmpty);
}
}
static class CompressionDetectionHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
if (msg instanceof HttpResponse response) {
boolean compressed = response.headers().contains(HttpHeaderNames.CONTENT_ENCODING, "gzip", true);
if (compressed) {
response.headers().set("X-Test-Compressed", "true");
}
}
ctx.fireChannelRead(msg);
}
}
static class CharsHandler implements HttpHandler {
private static final DefaultDataBufferFactory factory = new DefaultDataBufferFactory();
private final DataBuffer bytes;
private final String mediaType;
CharsHandler(int contentSize, String mediaType) {
char[] chars = new char[contentSize];
Arrays.fill(chars, 'F');
this.bytes = factory.wrap(new String(chars).getBytes(StandardCharsets.UTF_8));
this.mediaType = mediaType;
}
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
response.setStatusCode(HttpStatus.OK);
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, this.mediaType);
response.getHeaders().setContentLength(this.bytes.readableByteCount());
return response.writeWith(Mono.just(this.bytes));
}
}
static class XForwardedHandler implements HttpHandler {
@Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
String scheme = request.getURI().getScheme();
DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
DataBuffer buffer = bufferFactory.wrap(scheme.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer));
}
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.server.reactive;
import java.util.Map;
import org.springframework.boot.web.server.WebServer;
import org.springframework.http.server.reactive.HttpHandler;
/**
* A mock reactive {@link WebServer}.
*
* @author Brian Clozel
*/
public class MockReactiveWebServer implements WebServer {
private final int port;
private HttpHandler httpHandler;
private Map<String, HttpHandler> httpHandlerMap;
MockReactiveWebServer(HttpHandler httpHandler, int port) {
this.httpHandler = httpHandler;
this.port = port;
}
MockReactiveWebServer(Map<String, HttpHandler> httpHandlerMap, int port) {
this.httpHandlerMap = httpHandlerMap;
this.port = port;
}
public HttpHandler getHttpHandler() {
return this.httpHandler;
}
public Map<String, HttpHandler> getHttpHandlerMap() {
return this.httpHandlerMap;
}
@Override
public void start() {
}
@Override
public void stop() {
}
@Override
public int getPort() {
return this.port;
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.server.reactive;
import org.springframework.boot.web.server.WebServer;
import org.springframework.http.server.reactive.HttpHandler;
import static org.mockito.Mockito.spy;
/**
* Mock {@link ReactiveWebServerFactory}.
*
* @author Brian Clozel
*/
public class MockReactiveWebServerFactory extends AbstractReactiveWebServerFactory {
private MockReactiveWebServer webServer;
@Override
public WebServer getWebServer(HttpHandler httpHandler) {
this.webServer = spy(new MockReactiveWebServer(httpHandler, getPort()));
return this.webServer;
}
public MockReactiveWebServer getWebServer() {
return this.webServer;
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.server.servlet;
import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;
import org.junit.jupiter.api.Test;
import org.springframework.boot.testsupport.classpath.ForkedClassPath;
import org.springframework.boot.testsupport.web.servlet.DirtiesUrlFactories;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.mockito.Mockito.mock;
/**
* Base class for tests for {@link WebServer}s driving {@link ServletContextListener}s
* correctly.
*
* @author Andy Wilkinson
*/
@DirtiesUrlFactories
public abstract class AbstractServletWebServerServletContextListenerTests {
private final Class<?> webServerConfiguration;
protected AbstractServletWebServerServletContextListenerTests(Class<?> webServerConfiguration) {
this.webServerConfiguration = webServerConfiguration;
}
@Test
@ForkedClassPath
void registeredServletContextListenerBeanIsCalled() {
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(
ServletListenerRegistrationBeanConfiguration.class, this.webServerConfiguration);
ServletContextListener servletContextListener = (ServletContextListener) context
.getBean("registration", ServletListenerRegistrationBean.class)
.getListener();
then(servletContextListener).should().contextInitialized(any(ServletContextEvent.class));
context.close();
}
@Test
@ForkedClassPath
void servletContextListenerBeanIsCalled() {
AnnotationConfigServletWebServerApplicationContext context = new AnnotationConfigServletWebServerApplicationContext(
ServletContextListenerBeanConfiguration.class, this.webServerConfiguration);
ServletContextListener servletContextListener = context.getBean("servletContextListener",
ServletContextListener.class);
then(servletContextListener).should().contextInitialized(any(ServletContextEvent.class));
context.close();
}
@Configuration(proxyBeanMethods = false)
static class ServletContextListenerBeanConfiguration {
@Bean
ServletContextListener servletContextListener() {
return mock(ServletContextListener.class);
}
}
@Configuration(proxyBeanMethods = false)
static class ServletListenerRegistrationBeanConfiguration {
@Bean
ServletListenerRegistrationBean<ServletContextListener> registration() {
return new ServletListenerRegistrationBean<>(mock(ServletContextListener.class));
}
}
}

View File

@@ -0,0 +1,222 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.server.servlet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.StreamSupport;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterRegistration;
import jakarta.servlet.Servlet;
import jakarta.servlet.ServletContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRegistration;
import jakarta.servlet.SessionCookieConfig;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.WebServerException;
import org.springframework.mock.web.MockSessionCookieConfig;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
/**
* A mock servlet {@link WebServer}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class MockServletWebServer implements WebServer {
private ServletContext servletContext;
private final Initializer[] initializers;
private final List<RegisteredServlet> registeredServlets = new ArrayList<>();
private final List<RegisteredFilter> registeredFilters = new ArrayList<>();
private final Map<String, FilterRegistration> filterRegistrations = new HashMap<>();
private final Map<String, ServletRegistration> servletRegistrations = new HashMap<>();
private final int port;
MockServletWebServer(ServletContextInitializers initializers, int port) {
this(StreamSupport.stream(initializers.spliterator(), false)
.map((initializer) -> (Initializer) initializer::onStartup)
.toArray(Initializer[]::new), port);
}
MockServletWebServer(Initializer[] initializers, int port) {
this.initializers = initializers;
this.port = port;
initialize();
}
@SuppressWarnings("unchecked")
private void initialize() {
try {
this.servletContext = mock(ServletContext.class);
lenient().doAnswer((invocation) -> {
RegisteredServlet registeredServlet = new RegisteredServlet(invocation.getArgument(1));
MockServletWebServer.this.registeredServlets.add(registeredServlet);
this.servletRegistrations.put(invocation.getArgument(0), registeredServlet.getRegistration());
return registeredServlet.getRegistration();
}).when(this.servletContext).addServlet(anyString(), any(Servlet.class));
lenient().doAnswer((invocation) -> {
RegisteredFilter registeredFilter = new RegisteredFilter(invocation.getArgument(1));
MockServletWebServer.this.registeredFilters.add(registeredFilter);
this.filterRegistrations.put(invocation.getArgument(0), registeredFilter.getRegistration());
return registeredFilter.getRegistration();
}).when(this.servletContext).addFilter(anyString(), any(Filter.class));
final SessionCookieConfig sessionCookieConfig = new MockSessionCookieConfig();
given(this.servletContext.getSessionCookieConfig()).willReturn(sessionCookieConfig);
final Map<String, String> initParameters = new HashMap<>();
lenient().doAnswer((invocation) -> {
initParameters.put(invocation.getArgument(0), invocation.getArgument(1));
return null;
}).when(this.servletContext).setInitParameter(anyString(), anyString());
given(this.servletContext.getInitParameterNames())
.willReturn(Collections.enumeration(initParameters.keySet()));
lenient().doAnswer((invocation) -> initParameters.get(invocation.getArgument(0)))
.when(this.servletContext)
.getInitParameter(anyString());
given(this.servletContext.getAttributeNames()).willReturn(Collections.emptyEnumeration());
lenient().when((Map<String, FilterRegistration>) this.servletContext.getFilterRegistrations())
.thenReturn(this.filterRegistrations);
lenient().when((Map<String, ServletRegistration>) this.servletContext.getServletRegistrations())
.thenReturn(this.servletRegistrations);
for (Initializer initializer : this.initializers) {
initializer.onStartup(this.servletContext);
}
}
catch (ServletException ex) {
throw new RuntimeException(ex);
}
}
@Override
public void start() throws WebServerException {
}
@Override
public void stop() {
this.servletContext = null;
this.registeredServlets.clear();
this.filterRegistrations.clear();
this.registeredFilters.clear();
}
public ServletContext getServletContext() {
return this.servletContext;
}
public Servlet[] getServlets() {
Servlet[] servlets = new Servlet[this.registeredServlets.size()];
Arrays.setAll(servlets, (i) -> this.registeredServlets.get(i).getServlet());
return servlets;
}
public RegisteredServlet getRegisteredServlet(int index) {
return getRegisteredServlets().get(index);
}
public List<RegisteredServlet> getRegisteredServlets() {
return this.registeredServlets;
}
public RegisteredFilter getRegisteredFilters(int index) {
return getRegisteredFilters().get(index);
}
public List<RegisteredFilter> getRegisteredFilters() {
return this.registeredFilters;
}
@Override
public int getPort() {
return this.port;
}
/**
* A registered servlet.
*/
public static class RegisteredServlet {
private final Servlet servlet;
private final ServletRegistration.Dynamic registration;
public RegisteredServlet(Servlet servlet) {
this.servlet = servlet;
this.registration = mock(ServletRegistration.Dynamic.class);
}
public ServletRegistration.Dynamic getRegistration() {
return this.registration;
}
public Servlet getServlet() {
return this.servlet;
}
}
/**
* A registered filter.
*/
public static class RegisteredFilter {
private final Filter filter;
private final FilterRegistration.Dynamic registration;
public RegisteredFilter(Filter filter) {
this.filter = filter;
this.registration = mock(FilterRegistration.Dynamic.class);
}
public FilterRegistration.Dynamic getRegistration() {
return this.registration;
}
public Filter getFilter() {
return this.filter;
}
}
/**
* Initializer (usually implement by adapting {@code ServletContextInitializer}).
*/
@FunctionalInterface
protected interface Initializer {
void onStartup(ServletContext context) throws ServletException;
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.web.server.servlet;
import jakarta.servlet.ServletContext;
import org.springframework.boot.web.server.AbstractConfigurableWebServerFactory;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.servlet.MockServletWebServer.RegisteredFilter;
import org.springframework.boot.web.server.servlet.MockServletWebServer.RegisteredServlet;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import static org.mockito.Mockito.spy;
/**
* Mock {@link ServletWebServerFactory}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class MockServletWebServerFactory extends AbstractConfigurableWebServerFactory
implements ConfigurableServletWebServerFactory {
private final ServletWebServerSettings settings = new ServletWebServerSettings();
private MockServletWebServer webServer;
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
this.webServer = spy(
new MockServletWebServer(ServletContextInitializers.from(this.settings, initializers), getPort()));
return this.webServer;
}
public MockServletWebServer getWebServer() {
return this.webServer;
}
public ServletContext getServletContext() {
return (getWebServer() != null) ? getWebServer().getServletContext() : null;
}
public RegisteredServlet getRegisteredServlet(int index) {
return (getWebServer() != null) ? getWebServer().getRegisteredServlet(index) : null;
}
public RegisteredFilter getRegisteredFilter(int index) {
return (getWebServer() != null) ? getWebServer().getRegisteredFilters(index) : null;
}
@Override
public ServletWebServerSettings getSettings() {
return this.settings;
}
}

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDqzCCApOgAwIBAgIIFMqbpqvipw0wDQYJKoZIhvcNAQELBQAwbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDAgFw0yMzA1MDUxMTI2NThaGA8yMTIzMDQxMTExMjY1OFowbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPwHWxoE3xjRmNdD
+m+e/aFlr5wEGQUdWSDD613OB1w7kqO/audEp3c6HxDB3GPcEL0amJwXgY6CQMYu
sythuZX/EZSc2HdilTBu/5T+mbdWe5JkKThpiA0RYeucQfKuB7zv4ypioa4wiR4D
nPsZXjg95OF8pCzYEssv8wT49v+M3ohWUgfF0FPlMFCSo0YVTuzB1mhDlWKq/jhQ
11WpTmk/dQX+l6ts6bYIcJt4uItG+a68a4FutuSjZdTAE0f5SOYRBpGH96mjLwEP
fW8ZjzvKb9g4R2kiuoPxvCDs1Y/8V2yvKqLyn5Tx9x/DjFmOi0DRK/TgELvNceCb
UDJmhXMCAwEAAaNPME0wHQYDVR0OBBYEFMBIGU1nwix5RS3O5hGLLoMdR1+NMCwG
A1UdEQQlMCOCCWxvY2FsaG9zdIcQAAAAAAAAAAAAAAAAAAAAAYcEfwAAATANBgkq
hkiG9w0BAQsFAAOCAQEAhepfJgTFvqSccsT97XdAZfvB0noQx5NSynRV8NWmeOld
hHP6Fzj6xCxHSYvlUfmX8fVP9EOAuChgcbbuTIVJBu60rnDT21oOOnp8FvNonCV6
gJ89sCL7wZ77dw2RKIeUFjXXEV3QJhx2wCOVmLxnJspDoKFIEVjfLyiPXKxqe/6b
dG8zzWDZ6z+M2JNCtVoOGpljpHqMPCmbDktncv6H3dDTZ83bmLj1nbpOU587gAJ8
fl1PiUDyPRIl2cnOJd+wCHKsyym/FL7yzk0OSEZ81I92LpGd/0b2Ld3m/bpe+C4Z
ILzLXTnC6AhrLcDc9QN/EO+BiCL52n7EplNLtSn1LQ==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,59 @@
Bag Attributes
friendlyName: test-alias
localKeyID: 54 69 6D 65 20 31 36 38 33 32 38 36 31 31 34 30 37 31
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD8B1saBN8Y0ZjX
Q/pvnv2hZa+cBBkFHVkgw+tdzgdcO5Kjv2rnRKd3Oh8Qwdxj3BC9GpicF4GOgkDG
LrMrYbmV/xGUnNh3YpUwbv+U/pm3VnuSZCk4aYgNEWHrnEHyrge87+MqYqGuMIke
A5z7GV44PeThfKQs2BLLL/ME+Pb/jN6IVlIHxdBT5TBQkqNGFU7swdZoQ5Viqv44
UNdVqU5pP3UF/perbOm2CHCbeLiLRvmuvGuBbrbko2XUwBNH+UjmEQaRh/epoy8B
D31vGY87ym/YOEdpIrqD8bwg7NWP/Fdsryqi8p+U8fcfw4xZjotA0Sv04BC7zXHg
m1AyZoVzAgMBAAECggEAfEqiZqANaF+BqXQIb4Dw42ZTJzWsIyYYnPySOGZRoe5t
QJ03uwtULYv34xtANe1DQgd6SMyc46ugBzzjtprQ3ET5Jhn99U6kdcjf+dpf85dO
hOEppP0CkDNI39nleinSfh6uIOqYgt/D143/nqQhn8oCdSOzkbwT9KnWh1bC9T7I
vFjGfElvt1/xl88qYgrWgYLgXaencNGgiv/4/M0FNhiHEGsVC7SCu6kapC/WIQpE
5IdV+HR+tiLoGZhXlhqorY7QC4xKC4wwafVSiFxqDOQAuK+SMD4TCEv0Aop+c+SE
YBigVTmgVeJkjK7IkTEhKkAEFmRF5/5w+bZD9FhTNQKBgQD+4fNG1ChSU8RdizZT
5dPlDyAxpETSCEXFFVGtPPh2j93HDWn7XugNyjn5FylTH507QlabC+5wZqltdIjK
GRB5MIinQ9/nR2fuwGc9s+0BiSEwNOUB1MWm7wWL/JUIiKq6sTi6sJIfsYg79zco
qxl5WE94aoINx9Utq1cdWhwJTQKBgQD9IjPksd4Jprz8zMrGLzR8k1gqHyhv24qY
EJ7jiHKKAP6xllTUYwh1IBSL6w2j5lfZPpIkb4Jlk2KUoX6fN81pWkBC/fTBUSIB
EHM9bL51+yKEYUbGIy/gANuRbHXsWg3sjUsFTNPN4hGTFk3w2xChCyl/f5us8Lo8
Z633SNdpvwKBgQCGyDU9XzNzVZihXtx7wS0sE7OSjKtX5cf/UCbA1V0OVUWR3SYO
J0HPCQFfF0BjFHSwwYPKuaR9C8zMdLNhK5/qdh/NU7czNi9fsZ7moh7SkRFbzJzN
OxbKD9t/CzJEMQEXeF/nWTfsSpUgILqqZtAxuuFLbAcaAnJYlCKdAumQgQKBgQCK
mqjJh68pn7gJwGUjoYNe1xtGbSsqHI9F9ovZ0MPO1v6e5M7sQJHH+Fnnxzv/y8e8
d6tz8e73iX1IHymDKv35uuZHCGF1XOR+qrA/KQUc+vcKf21OXsP/JtkTRs1HLoRD
S5aRf2DWcfvniyYARSNU2xTM8GWgi2ueWbMDHUp+ZwKBgA/swC+K+Jg5DEWm6Sau
e6y+eC6S+SoXEKkI3wf7m9aKoZo0y+jh8Gas6gratlc181pSM8O3vZG0n19b493I
apCFomMLE56zEzvyzfpsNhFhk5MBMCn0LPyzX6MiynRlGyWIj0c99fbHI3pOMufP
WgmVLTZ8uDcSW1MbdUCwFSk5
-----END PRIVATE KEY-----
Bag Attributes
friendlyName: test-alias
localKeyID: 54 69 6D 65 20 31 36 38 33 32 38 36 31 31 34 30 37 31
subject=C = US, ST = California, L = Palo Alto, O = VMware, OU = Spring, CN = localhost
issuer=C = US, ST = California, L = Palo Alto, O = VMware, OU = Spring, CN = localhost
-----BEGIN CERTIFICATE-----
MIIDqzCCApOgAwIBAgIIFMqbpqvipw0wDQYJKoZIhvcNAQELBQAwbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDAgFw0yMzA1MDUxMTI2NThaGA8yMTIzMDQxMTExMjY1OFowbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPwHWxoE3xjRmNdD
+m+e/aFlr5wEGQUdWSDD613OB1w7kqO/audEp3c6HxDB3GPcEL0amJwXgY6CQMYu
sythuZX/EZSc2HdilTBu/5T+mbdWe5JkKThpiA0RYeucQfKuB7zv4ypioa4wiR4D
nPsZXjg95OF8pCzYEssv8wT49v+M3ohWUgfF0FPlMFCSo0YVTuzB1mhDlWKq/jhQ
11WpTmk/dQX+l6ts6bYIcJt4uItG+a68a4FutuSjZdTAE0f5SOYRBpGH96mjLwEP
fW8ZjzvKb9g4R2kiuoPxvCDs1Y/8V2yvKqLyn5Tx9x/DjFmOi0DRK/TgELvNceCb
UDJmhXMCAwEAAaNPME0wHQYDVR0OBBYEFMBIGU1nwix5RS3O5hGLLoMdR1+NMCwG
A1UdEQQlMCOCCWxvY2FsaG9zdIcQAAAAAAAAAAAAAAAAAAAAAYcEfwAAATANBgkq
hkiG9w0BAQsFAAOCAQEAhepfJgTFvqSccsT97XdAZfvB0noQx5NSynRV8NWmeOld
hHP6Fzj6xCxHSYvlUfmX8fVP9EOAuChgcbbuTIVJBu60rnDT21oOOnp8FvNonCV6
gJ89sCL7wZ77dw2RKIeUFjXXEV3QJhx2wCOVmLxnJspDoKFIEVjfLyiPXKxqe/6b
dG8zzWDZ6z+M2JNCtVoOGpljpHqMPCmbDktncv6H3dDTZ83bmLj1nbpOU587gAJ8
fl1PiUDyPRIl2cnOJd+wCHKsyym/FL7yzk0OSEZ81I92LpGd/0b2Ld3m/bpe+C4Z
ILzLXTnC6AhrLcDc9QN/EO+BiCL52n7EplNLtSn1LQ==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,22 @@
-----BEGIN CERTIFICATE-----
MIIDqzCCApOgAwIBAgIIFMqbpqvipw0wDQYJKoZIhvcNAQELBQAwbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDAgFw0yMzA1MDUxMTI2NThaGA8yMTIzMDQxMTExMjY1OFowbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPwHWxoE3xjRmNdD
+m+e/aFlr5wEGQUdWSDD613OB1w7kqO/audEp3c6HxDB3GPcEL0amJwXgY6CQMYu
sythuZX/EZSc2HdilTBu/5T+mbdWe5JkKThpiA0RYeucQfKuB7zv4ypioa4wiR4D
nPsZXjg95OF8pCzYEssv8wT49v+M3ohWUgfF0FPlMFCSo0YVTuzB1mhDlWKq/jhQ
11WpTmk/dQX+l6ts6bYIcJt4uItG+a68a4FutuSjZdTAE0f5SOYRBpGH96mjLwEP
fW8ZjzvKb9g4R2kiuoPxvCDs1Y/8V2yvKqLyn5Tx9x/DjFmOi0DRK/TgELvNceCb
UDJmhXMCAwEAAaNPME0wHQYDVR0OBBYEFMBIGU1nwix5RS3O5hGLLoMdR1+NMCwG
A1UdEQQlMCOCCWxvY2FsaG9zdIcQAAAAAAAAAAAAAAAAAAAAAYcEfwAAATANBgkq
hkiG9w0BAQsFAAOCAQEAhepfJgTFvqSccsT97XdAZfvB0noQx5NSynRV8NWmeOld
hHP6Fzj6xCxHSYvlUfmX8fVP9EOAuChgcbbuTIVJBu60rnDT21oOOnp8FvNonCV6
gJ89sCL7wZ77dw2RKIeUFjXXEV3QJhx2wCOVmLxnJspDoKFIEVjfLyiPXKxqe/6b
dG8zzWDZ6z+M2JNCtVoOGpljpHqMPCmbDktncv6H3dDTZ83bmLj1nbpOU587gAJ8
fl1PiUDyPRIl2cnOJd+wCHKsyym/FL7yzk0OSEZ81I92LpGd/0b2Ld3m/bpe+C4Z
ILzLXTnC6AhrLcDc9QN/EO+BiCL52n7EplNLtSn1LQ==
-----END CERTIFICATE-----

View File

@@ -0,0 +1,59 @@
Bag Attributes
friendlyName: test-alias
localKeyID: 54 69 6D 65 20 31 36 38 33 32 38 36 31 31 34 30 37 31
Key Attributes: <No Attributes>
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQD8B1saBN8Y0ZjX
Q/pvnv2hZa+cBBkFHVkgw+tdzgdcO5Kjv2rnRKd3Oh8Qwdxj3BC9GpicF4GOgkDG
LrMrYbmV/xGUnNh3YpUwbv+U/pm3VnuSZCk4aYgNEWHrnEHyrge87+MqYqGuMIke
A5z7GV44PeThfKQs2BLLL/ME+Pb/jN6IVlIHxdBT5TBQkqNGFU7swdZoQ5Viqv44
UNdVqU5pP3UF/perbOm2CHCbeLiLRvmuvGuBbrbko2XUwBNH+UjmEQaRh/epoy8B
D31vGY87ym/YOEdpIrqD8bwg7NWP/Fdsryqi8p+U8fcfw4xZjotA0Sv04BC7zXHg
m1AyZoVzAgMBAAECggEAfEqiZqANaF+BqXQIb4Dw42ZTJzWsIyYYnPySOGZRoe5t
QJ03uwtULYv34xtANe1DQgd6SMyc46ugBzzjtprQ3ET5Jhn99U6kdcjf+dpf85dO
hOEppP0CkDNI39nleinSfh6uIOqYgt/D143/nqQhn8oCdSOzkbwT9KnWh1bC9T7I
vFjGfElvt1/xl88qYgrWgYLgXaencNGgiv/4/M0FNhiHEGsVC7SCu6kapC/WIQpE
5IdV+HR+tiLoGZhXlhqorY7QC4xKC4wwafVSiFxqDOQAuK+SMD4TCEv0Aop+c+SE
YBigVTmgVeJkjK7IkTEhKkAEFmRF5/5w+bZD9FhTNQKBgQD+4fNG1ChSU8RdizZT
5dPlDyAxpETSCEXFFVGtPPh2j93HDWn7XugNyjn5FylTH507QlabC+5wZqltdIjK
GRB5MIinQ9/nR2fuwGc9s+0BiSEwNOUB1MWm7wWL/JUIiKq6sTi6sJIfsYg79zco
qxl5WE94aoINx9Utq1cdWhwJTQKBgQD9IjPksd4Jprz8zMrGLzR8k1gqHyhv24qY
EJ7jiHKKAP6xllTUYwh1IBSL6w2j5lfZPpIkb4Jlk2KUoX6fN81pWkBC/fTBUSIB
EHM9bL51+yKEYUbGIy/gANuRbHXsWg3sjUsFTNPN4hGTFk3w2xChCyl/f5us8Lo8
Z633SNdpvwKBgQCGyDU9XzNzVZihXtx7wS0sE7OSjKtX5cf/UCbA1V0OVUWR3SYO
J0HPCQFfF0BjFHSwwYPKuaR9C8zMdLNhK5/qdh/NU7czNi9fsZ7moh7SkRFbzJzN
OxbKD9t/CzJEMQEXeF/nWTfsSpUgILqqZtAxuuFLbAcaAnJYlCKdAumQgQKBgQCK
mqjJh68pn7gJwGUjoYNe1xtGbSsqHI9F9ovZ0MPO1v6e5M7sQJHH+Fnnxzv/y8e8
d6tz8e73iX1IHymDKv35uuZHCGF1XOR+qrA/KQUc+vcKf21OXsP/JtkTRs1HLoRD
S5aRf2DWcfvniyYARSNU2xTM8GWgi2ueWbMDHUp+ZwKBgA/swC+K+Jg5DEWm6Sau
e6y+eC6S+SoXEKkI3wf7m9aKoZo0y+jh8Gas6gratlc181pSM8O3vZG0n19b493I
apCFomMLE56zEzvyzfpsNhFhk5MBMCn0LPyzX6MiynRlGyWIj0c99fbHI3pOMufP
WgmVLTZ8uDcSW1MbdUCwFSk5
-----END PRIVATE KEY-----
Bag Attributes
friendlyName: test-alias
localKeyID: 54 69 6D 65 20 31 36 38 33 32 38 36 31 31 34 30 37 31
subject=C = US, ST = California, L = Palo Alto, O = VMware, OU = Spring, CN = localhost
issuer=C = US, ST = California, L = Palo Alto, O = VMware, OU = Spring, CN = localhost
-----BEGIN CERTIFICATE-----
MIIDqzCCApOgAwIBAgIIFMqbpqvipw0wDQYJKoZIhvcNAQELBQAwbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDAgFw0yMzA1MDUxMTI2NThaGA8yMTIzMDQxMTExMjY1OFowbDELMAkGA1UE
BhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVBhbG8gQWx0bzEP
MA0GA1UEChMGVk13YXJlMQ8wDQYDVQQLEwZTcHJpbmcxEjAQBgNVBAMTCWxvY2Fs
aG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPwHWxoE3xjRmNdD
+m+e/aFlr5wEGQUdWSDD613OB1w7kqO/audEp3c6HxDB3GPcEL0amJwXgY6CQMYu
sythuZX/EZSc2HdilTBu/5T+mbdWe5JkKThpiA0RYeucQfKuB7zv4ypioa4wiR4D
nPsZXjg95OF8pCzYEssv8wT49v+M3ohWUgfF0FPlMFCSo0YVTuzB1mhDlWKq/jhQ
11WpTmk/dQX+l6ts6bYIcJt4uItG+a68a4FutuSjZdTAE0f5SOYRBpGH96mjLwEP
fW8ZjzvKb9g4R2kiuoPxvCDs1Y/8V2yvKqLyn5Tx9x/DjFmOi0DRK/TgELvNceCb
UDJmhXMCAwEAAaNPME0wHQYDVR0OBBYEFMBIGU1nwix5RS3O5hGLLoMdR1+NMCwG
A1UdEQQlMCOCCWxvY2FsaG9zdIcQAAAAAAAAAAAAAAAAAAAAAYcEfwAAATANBgkq
hkiG9w0BAQsFAAOCAQEAhepfJgTFvqSccsT97XdAZfvB0noQx5NSynRV8NWmeOld
hHP6Fzj6xCxHSYvlUfmX8fVP9EOAuChgcbbuTIVJBu60rnDT21oOOnp8FvNonCV6
gJ89sCL7wZ77dw2RKIeUFjXXEV3QJhx2wCOVmLxnJspDoKFIEVjfLyiPXKxqe/6b
dG8zzWDZ6z+M2JNCtVoOGpljpHqMPCmbDktncv6H3dDTZ83bmLj1nbpOU587gAJ8
fl1PiUDyPRIl2cnOJd+wCHKsyym/FL7yzk0OSEZ81I92LpGd/0b2Ld3m/bpe+C4Z
ILzLXTnC6AhrLcDc9QN/EO+BiCL52n7EplNLtSn1LQ==
-----END CERTIFICATE-----