Skip to content

Commit

Permalink
Add TLS Registry configuration to WebSockets Next Client
Browse files Browse the repository at this point in the history
Resolves: quarkusio#41004
  • Loading branch information
geoand committed Jun 11, 2024
1 parent 99d63a7 commit 89ceb59
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 31 deletions.
9 changes: 9 additions & 0 deletions extensions/websockets-next/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-tls-registry-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-websockets-next</artifactId>
Expand Down Expand Up @@ -56,6 +60,11 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>me.escoffier.certs</groupId>
<artifactId>certificate-generator-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package io.quarkus.websockets.next.test.client;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.websockets.next.OnClose;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.PathParam;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketClient;
import io.quarkus.websockets.next.WebSocketClientConnection;
import io.quarkus.websockets.next.WebSocketConnector;
import me.escoffier.certs.Format;
import me.escoffier.certs.junit5.Certificate;
import me.escoffier.certs.junit5.Certificates;

@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "mtls-test", password = "secret", formats = {
Format.JKS, Format.PKCS12, Format.PEM }, client = true))
public class MtlsWithP12ClientEndpointTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(ServerEndpoint.class, ClientEndpoint.class)
.addAsResource(new File("target/certs/mtls-test-keystore.p12"), "server-keystore.p12")
.addAsResource(new File("target/certs/mtls-test-server-truststore.p12"), "server-truststore.p12")
.addAsResource(new File("target/certs/mtls-test-client-keystore.p12"), "client-keystore.p12")
.addAsResource(new File("target/certs/mtls-test-client-truststore.p12"), "client-truststore.p12"))

.overrideConfigKey("quarkus.tls.ws-server.key-store.p12.path", "server-keystore.p12")
.overrideConfigKey("quarkus.tls.ws-server.key-store.p12.password", "secret")
.overrideConfigKey("quarkus.tls.ws-server.trust-store.p12.path", "server-truststore.p12")
.overrideConfigKey("quarkus.tls.ws-server.trust-store.p12.password", "secret")
.overrideConfigKey("quarkus.http.tls-configuration-name", "ws-server")

.overrideConfigKey("quarkus.tls.ws-client.key-store.p12.path", "client-keystore.p12")
.overrideConfigKey("quarkus.tls.ws-client.key-store.p12.password", "secret")
.overrideConfigKey("quarkus.tls.ws-client.trust-store.p12.path", "client-truststore.p12")
.overrideConfigKey("quarkus.tls.ws-client.trust-store.p12.password", "secret")
.overrideConfigKey("quarkus.websockets-next.client.tls-configuration-name", "ws-client");

@Inject
WebSocketConnector<ClientEndpoint> connector;

@TestHTTPResource(value = "/", tls = true)
URI uri;

@Test
void testClient() throws InterruptedException {
WebSocketClientConnection connection = connector
.baseUri(uri)
// The value will be encoded automatically
.pathParam("name", "Lu=")
.connectAndAwait();
assertTrue(connection.isSecure());

assertEquals("Lu=", connection.pathParam("name"));
connection.sendTextAndAwait("Hi!");

assertTrue(ClientEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS));
assertEquals("Lu=:Hello Lu=!", ClientEndpoint.MESSAGES.get(0));
assertEquals("Lu=:Hi!", ClientEndpoint.MESSAGES.get(1));

connection.closeAndAwait();
assertTrue(ClientEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
}

@WebSocket(path = "/endpoint/{name}")
public static class ServerEndpoint {

static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1);

@OnOpen
String open(@PathParam String name) {
return "Hello " + name + "!";
}

@OnTextMessage
String echo(String message) {
return message;
}

@OnClose
void close() {
CLOSED_LATCH.countDown();
}

}

@WebSocketClient(path = "/endpoint/{name}")
public static class ClientEndpoint {

static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(2);

static final List<String> MESSAGES = new CopyOnWriteArrayList<>();

static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1);

@OnTextMessage
void onMessage(@PathParam String name, String message, WebSocketClientConnection connection) {
if (!name.equals(connection.pathParam("name"))) {
throw new IllegalArgumentException();
}
MESSAGES.add(name + ":" + message);
MESSAGE_LATCH.countDown();
}

@OnClose
void close() {
CLOSED_LATCH.countDown();
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package io.quarkus.websockets.next.test.client;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.File;
import java.net.URI;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

import jakarta.inject.Inject;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.quarkus.test.common.http.TestHTTPResource;
import io.quarkus.websockets.next.OnClose;
import io.quarkus.websockets.next.OnOpen;
import io.quarkus.websockets.next.OnTextMessage;
import io.quarkus.websockets.next.PathParam;
import io.quarkus.websockets.next.WebSocket;
import io.quarkus.websockets.next.WebSocketClient;
import io.quarkus.websockets.next.WebSocketClientConnection;
import io.quarkus.websockets.next.WebSocketConnector;
import me.escoffier.certs.Format;
import me.escoffier.certs.junit5.Certificate;
import me.escoffier.certs.junit5.Certificates;

@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "ssl-test", password = "secret", formats = {
Format.JKS, Format.PKCS12, Format.PEM }))
public class TlsClientEndpointTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(ServerEndpoint.class, ClientEndpoint.class)
.addAsResource(new File("target/certs/ssl-test-keystore.jks"), "keystore.jks")
.addAsResource(new File("target/certs/ssl-test-truststore.jks"), "truststore.jks"))
.overrideConfigKey("quarkus.tls.key-store.jks.path", "keystore.jks")
.overrideConfigKey("quarkus.tls.key-store.jks.password", "secret")
.overrideConfigKey("quarkus.tls.ws-client.trust-store.jks.path", "truststore.jks")
.overrideConfigKey("quarkus.tls.ws-client.trust-store.jks.password", "secret")
.overrideConfigKey("quarkus.websockets-next.client.tls-configuration-name", "ws-client");

@Inject
WebSocketConnector<ClientEndpoint> connector;

@TestHTTPResource(value = "/", tls = true)
URI uri;

@Test
void testClient() throws InterruptedException {
WebSocketClientConnection connection = connector
.baseUri(uri)
// The value will be encoded automatically
.pathParam("name", "Lu=")
.connectAndAwait();
assertTrue(connection.isSecure());

assertEquals("Lu=", connection.pathParam("name"));
connection.sendTextAndAwait("Hi!");

assertTrue(ClientEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS));
assertEquals("Lu=:Hello Lu=!", ClientEndpoint.MESSAGES.get(0));
assertEquals("Lu=:Hi!", ClientEndpoint.MESSAGES.get(1));

connection.closeAndAwait();
assertTrue(ClientEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
}

@WebSocket(path = "/endpoint/{name}")
public static class ServerEndpoint {

static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1);

@OnOpen
String open(@PathParam String name) {
return "Hello " + name + "!";
}

@OnTextMessage
String echo(String message) {
return message;
}

@OnClose
void close() {
CLOSED_LATCH.countDown();
}

}

@WebSocketClient(path = "/endpoint/{name}")
public static class ClientEndpoint {

static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(2);

static final List<String> MESSAGES = new CopyOnWriteArrayList<>();

static final CountDownLatch CLOSED_LATCH = new CountDownLatch(1);

@OnTextMessage
void onMessage(@PathParam String name, String message, WebSocketClientConnection connection) {
if (!name.equals(connection.pathParam("name"))) {
throw new IllegalArgumentException();
}
MESSAGES.add(name + ":" + message);
MESSAGE_LATCH.countDown();
}

@OnClose
void close() {
CLOSED_LATCH.countDown();
}

}
}
4 changes: 4 additions & 0 deletions extensions/websockets-next/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-tls-registry</artifactId>
</dependency>
<!-- Quarkus Security API -->
<dependency>
<groupId>io.quarkus.security</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,14 @@ public interface WebSocketsClientRuntimeConfig {
@WithDefault("close")
UnhandledFailureStrategy unhandledFailureStrategy();

/**
* The name of the TLS configuration to use.
* <p>
* If a name is configured, it uses the configuration from {@code quarkus.tls.<name>.*}
* If a name is configured, but no TLS configuration is found with that name then an error will be thrown.
* <p>
* The default TLS configuration is <strong>not</strong> used by default.
*/
Optional<String> tlsConfigurationName();

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import org.jboss.logging.Logger;

import io.quarkus.tls.TlsConfigurationRegistry;
import io.quarkus.virtual.threads.VirtualThreadsRecorder;
import io.quarkus.websockets.next.BasicWebSocketConnector;
import io.quarkus.websockets.next.CloseReason;
Expand All @@ -27,7 +28,6 @@
import io.vertx.core.Vertx;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.WebSocketClient;
import io.vertx.core.http.WebSocketClientOptions;
import io.vertx.core.http.WebSocketConnectOptions;

@Typed(BasicWebSocketConnector.class)
Expand All @@ -54,8 +54,8 @@ public class BasicWebSocketConnectorImpl extends WebSocketConnectorBase<BasicWeb
private BiConsumer<WebSocketClientConnection, Throwable> errorHandler;

BasicWebSocketConnectorImpl(Vertx vertx, Codecs codecs, ClientConnectionManager connectionManager,
WebSocketsClientRuntimeConfig config) {
super(vertx, codecs, connectionManager, config);
WebSocketsClientRuntimeConfig config, TlsConfigurationRegistry tlsConfigurationRegistry) {
super(vertx, codecs, connectionManager, config, tlsConfigurationRegistry);
}

@Override
Expand Down Expand Up @@ -115,18 +115,7 @@ public Uni<WebSocketClientConnection> connect() {
// Currently we create a new client for each connection
// The client is closed when the connection is closed
// TODO would it make sense to share clients?
WebSocketClientOptions clientOptions = new WebSocketClientOptions();
if (config.offerPerMessageCompression()) {
clientOptions.setTryUsePerMessageCompression(true);
if (config.compressionLevel().isPresent()) {
clientOptions.setCompressionLevel(config.compressionLevel().getAsInt());
}
}
if (config.maxMessageSize().isPresent()) {
clientOptions.setMaxMessageSize(config.maxMessageSize().getAsInt());
}

WebSocketClient client = vertx.createWebSocketClient();
WebSocketClient client = vertx.createWebSocketClient(populateClientOptions());

WebSocketConnectOptions connectOptions = new WebSocketConnectOptions()
.setSsl(baseUri.getScheme().equals("https"))
Expand Down
Loading

0 comments on commit 89ceb59

Please sign in to comment.