diff --git a/ci/smoke-tests.yml b/ci/smoke-tests.yml index d1cf0b76..967eed87 100644 --- a/ci/smoke-tests.yml +++ b/ci/smoke-tests.yml @@ -50,6 +50,7 @@ smoke_tests: - validation - webclient - webflux-netty + - webflux-netty-tls - webmvc-tomcat - webmvc-tomcat-tls - websocket diff --git a/settings.gradle b/settings.gradle index 853d8d7e..a7bda155 100644 --- a/settings.gradle +++ b/settings.gradle @@ -78,6 +78,7 @@ include "transactional-event-listener" include "validation" include "webclient" include "webflux-netty" +include "webflux-netty-tls" include "webmvc-tomcat" include "webmvc-tomcat-tls" include "websocket" diff --git a/webflux-netty-tls/README.adoc b/webflux-netty-tls/README.adoc new file mode 100644 index 00000000..d06ee260 --- /dev/null +++ b/webflux-netty-tls/README.adoc @@ -0,0 +1 @@ +Tests if WebFlux with Netty with TLS is working diff --git a/webflux-netty-tls/build.gradle b/webflux-netty-tls/build.gradle new file mode 100644 index 00000000..ed96db28 --- /dev/null +++ b/webflux-netty-tls/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' + id 'org.springframework.boot' + id 'org.springframework.aot.smoke-test' + id 'org.graalvm.buildtools.native' + id 'org.jetbrains.kotlin.jvm' +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-webflux") + implementation(project(":aot-smoke-test-third-party-hints")) + + testImplementation("org.springframework.boot:spring-boot-starter-test") + + aotTestImplementation(project(":aot-smoke-test-support")) + aotTestImplementation("io.projectreactor.netty:reactor-netty") +} + +aotSmokeTest { + webApplication = true +} diff --git a/webflux-netty-tls/src/aotTest/java/com/example/webflux/tls/WebFluxNettyTlsApplicationAotTests.java b/webflux-netty-tls/src/aotTest/java/com/example/webflux/tls/WebFluxNettyTlsApplicationAotTests.java new file mode 100644 index 00000000..3e4ff126 --- /dev/null +++ b/webflux-netty-tls/src/aotTest/java/com/example/webflux/tls/WebFluxNettyTlsApplicationAotTests.java @@ -0,0 +1,86 @@ +package com.example.webflux.tls; + +import java.io.InputStream; +import java.net.URI; +import java.security.KeyStore; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import javax.net.ssl.TrustManagerFactory; + +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import org.junit.jupiter.api.Test; +import reactor.netty.http.client.HttpClient; + +import org.springframework.aot.smoketest.support.junit.AotSmokeTest; +import org.springframework.aot.smoketest.support.junit.ApplicationUrl; +import org.springframework.aot.smoketest.support.junit.ApplicationUrl.Scheme; +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; +import org.springframework.web.reactive.socket.client.WebSocketClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@AotSmokeTest +class WebFluxNettyTlsApplicationAotTests { + + @Test + void stringResponseBody(@ApplicationUrl(scheme = Scheme.HTTPS) URI applicationUrl) throws Exception { + WebTestClient client = buildWebClient(applicationUrl); + + client.get().exchange().expectStatus().isOk().expectBody().consumeWith( + (result) -> assertThat(new String(result.getResponseBodyContent())).isEqualTo("Hello World TLS")); + } + + @Test + void websocket(@ApplicationUrl(scheme = Scheme.SECURE_WEBSOCKET) URI applicationUrl) throws Exception { + WebSocketClient client = new ReactorNettyWebSocketClient(buildHttpClient()); + + // We can't use StepVerifier here, as it isn't designed to be used in a reactive + // pipeline + AtomicReference> messages = new AtomicReference<>(); + client.execute(URI.create(applicationUrl.resolve("/ws/count").toString()), session -> session.receive() + .map(WebSocketMessage::getPayloadAsText).collectList().doOnNext(messages::set).then()) + .block(Duration.ofSeconds(10)); + assertThat(messages.get()).isNotNull().containsExactly("1", "2", "3", "4", "5", "6", "7", "8", "9", "10"); + } + + /** + * Builds a web client for the running application. This web client is configured to + * trust the TLS certificate of the server. + * @param applicationUrl the URL to the application + * @return web client + * @throws Exception if something went wrong + */ + private static WebTestClient buildWebClient(URI applicationUrl) throws Exception { + ClientHttpConnector connector = new ReactorClientHttpConnector(buildHttpClient()); + return WebTestClient.bindToServer(connector).baseUrl(applicationUrl.toString()).build(); + } + + /** + * Builds a http client for the running application. This http client is configured to + * trust the TLS certificate of the server. + * @return http client + * @throws Exception if something went wrong + */ + private static HttpClient buildHttpClient() throws Exception { + KeyStore trustStore = KeyStore.getInstance("jks"); + try (InputStream stream = WebFluxNettyTlsApplicationAotTests.class.getResourceAsStream("/truststore.jks")) { + assertThat(stream).as("Trust store on test classpath").isNotNull(); + trustStore.load(stream, null); + } + + TrustManagerFactory trustManagerFactory = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init(trustStore); + + SslContext sslContext = SslContextBuilder.forClient().trustManager(trustManagerFactory).build(); + return HttpClient.create().secure((s) -> s.sslContext(sslContext)); + } + +} diff --git a/webflux-netty-tls/src/aotTest/resources/truststore.jks b/webflux-netty-tls/src/aotTest/resources/truststore.jks new file mode 100644 index 00000000..06318674 Binary files /dev/null and b/webflux-netty-tls/src/aotTest/resources/truststore.jks differ diff --git a/webflux-netty-tls/src/main/java/com/example/webflux/tls/TestController.java b/webflux-netty-tls/src/main/java/com/example/webflux/tls/TestController.java new file mode 100644 index 00000000..cf1ab743 --- /dev/null +++ b/webflux-netty-tls/src/main/java/com/example/webflux/tls/TestController.java @@ -0,0 +1,17 @@ +package com.example.webflux.tls; + +import reactor.core.publisher.Mono; + +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TestController { + + @GetMapping(value = "/", produces = MediaType.TEXT_PLAIN_VALUE) + public Mono greet() { + return Mono.just("Hello World TLS"); + } + +} diff --git a/webflux-netty-tls/src/main/java/com/example/webflux/tls/WebFluxNettyTlsApplication.java b/webflux-netty-tls/src/main/java/com/example/webflux/tls/WebFluxNettyTlsApplication.java new file mode 100644 index 00000000..157573f1 --- /dev/null +++ b/webflux-netty-tls/src/main/java/com/example/webflux/tls/WebFluxNettyTlsApplication.java @@ -0,0 +1,29 @@ +package com.example.webflux.tls; + +import com.example.webflux.tls.WebFluxNettyTlsApplication.KeyStoreRuntimeHints; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.smoketest.thirdpartyhints.NettyRuntimeHints; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ImportRuntimeHints; + +@SpringBootApplication +@ImportRuntimeHints({ NettyRuntimeHints.class, KeyStoreRuntimeHints.class }) +public class WebFluxNettyTlsApplication { + + public static void main(String[] args) { + SpringApplication.run(WebFluxNettyTlsApplication.class, args); + } + + static class KeyStoreRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + hints.resources().registerPattern("keystore.jks"); + } + + } + +} diff --git a/webflux-netty-tls/src/main/java/com/example/webflux/tls/WebsocketConfig.java b/webflux-netty-tls/src/main/java/com/example/webflux/tls/WebsocketConfig.java new file mode 100644 index 00000000..28bdd1ff --- /dev/null +++ b/webflux-netty-tls/src/main/java/com/example/webflux/tls/WebsocketConfig.java @@ -0,0 +1,33 @@ +package com.example.webflux.tls; + +import java.time.Duration; +import java.util.Map; + +import reactor.core.publisher.Flux; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.WebSocketMessage; + +@Configuration(proxyBeanMethods = false) +class WebsocketConfig { + + @Bean + WebSocketHandler webSocketHandler() { + return webSocketSession -> { + Flux stream = Flux.just(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + .map(element -> webSocketSession.textMessage(Integer.toString(element))) + .delayElements(Duration.ofMillis(100)); + return webSocketSession.send(stream); + }; + } + + @Bean + public SimpleUrlHandlerMapping body(WebSocketHandler webSocketHandler) { + Map handlers = Map.of("/ws/count", webSocketHandler); + return new SimpleUrlHandlerMapping(handlers, 10); + } + +} diff --git a/webflux-netty-tls/src/main/resources/application.properties b/webflux-netty-tls/src/main/resources/application.properties new file mode 100644 index 00000000..b2dbf04e --- /dev/null +++ b/webflux-netty-tls/src/main/resources/application.properties @@ -0,0 +1,4 @@ +server.ssl.key-store=classpath:keystore.jks +server.ssl.key-store-password=secret +server.ssl.key-alias=localhost +server.ssl.key-password=secret diff --git a/webflux-netty-tls/src/main/resources/keystore.jks b/webflux-netty-tls/src/main/resources/keystore.jks new file mode 100644 index 00000000..cad0ed8d Binary files /dev/null and b/webflux-netty-tls/src/main/resources/keystore.jks differ diff --git a/webflux-netty-tls/src/test/java/com/example/webflux/tls/WebFluxNettyTlsApplicationTests.java b/webflux-netty-tls/src/test/java/com/example/webflux/tls/WebFluxNettyTlsApplicationTests.java new file mode 100644 index 00000000..94b63570 --- /dev/null +++ b/webflux-netty-tls/src/test/java/com/example/webflux/tls/WebFluxNettyTlsApplicationTests.java @@ -0,0 +1,14 @@ +package com.example.webflux.tls; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class WebFluxNettyTlsApplicationTests { + + @Test + void contextLoads() { + } + +}