diff --git a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java index 1ec6c5c92af21..a73cb0531725a 100644 --- a/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java +++ b/extensions/websockets-next/server/deployment/src/main/java/io/quarkus/websockets/next/deployment/WebSocketServerProcessor.java @@ -68,6 +68,7 @@ import io.quarkus.websockets.next.runtime.JsonTextMessageCodec; import io.quarkus.websockets.next.runtime.WebSocketEndpoint.ExecutionModel; import io.quarkus.websockets.next.runtime.WebSocketEndpointBase; +import io.quarkus.websockets.next.runtime.WebSocketHttpServerOptionsCustomizer; import io.quarkus.websockets.next.runtime.WebSocketServerRecorder; import io.quarkus.websockets.next.runtime.WebSocketSessionContext; import io.smallrye.mutiny.Multi; @@ -200,7 +201,9 @@ public void registerRoutes(WebSocketServerRecorder recorder, HttpRootPathBuildIt @BuildStep AdditionalBeanBuildItem additionalBeans() { return AdditionalBeanBuildItem.builder().setUnremovable() - .addBeanClasses(Codecs.class, JsonTextMessageCodec.class, ConnectionManager.class).build(); + .addBeanClasses(Codecs.class, JsonTextMessageCodec.class, ConnectionManager.class, + WebSocketHttpServerOptionsCustomizer.class) + .build(); } @BuildStep diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolNotAvailableTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolNotAvailableTest.java new file mode 100644 index 0000000000000..9ef02fe878268 --- /dev/null +++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolNotAvailableTest.java @@ -0,0 +1,60 @@ +package io.quarkus.websockets.next.test.subprotocol; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.concurrent.CompletionException; +import java.util.concurrent.atomic.AtomicBoolean; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakeException; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.vertx.core.Vertx; +import io.vertx.core.http.WebSocketConnectOptions; + +public class SubprotocolNotAvailableTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Endpoint.class, WSClient.class); + }); + + @Inject + Vertx vertx; + + @TestHTTPResource("endpoint") + URI endUri; + + @Test + void testConnectionRejected() { + CompletionException e = assertThrows(CompletionException.class, + () -> new WSClient(vertx).connect(new WebSocketConnectOptions().addSubProtocol("oak"), endUri)); + Throwable cause = e.getCause(); + assertTrue(cause instanceof WebSocketClientHandshakeException); + assertFalse(Endpoint.OPEN_CALLED.get()); + } + + @WebSocket(path = "/endpoint") + public static class Endpoint { + + static final AtomicBoolean OPEN_CALLED = new AtomicBoolean(); + + @OnOpen + void open() { + OPEN_CALLED.set(true); + } + + } + +} diff --git a/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolSelectedTest.java b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolSelectedTest.java new file mode 100644 index 0000000000000..a9e52a296e574 --- /dev/null +++ b/extensions/websockets-next/server/deployment/src/test/java/io/quarkus/websockets/next/test/subprotocol/SubprotocolSelectedTest.java @@ -0,0 +1,63 @@ +package io.quarkus.websockets.next.test.subprotocol; + +import static io.quarkus.websockets.next.WebSocketConnection.HandshakeRequest.SEC_WEBSOCKET_PROTOCOL; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.net.URI; +import java.util.concurrent.ExecutionException; + +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.OnOpen; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.WebSocketConnection; +import io.quarkus.websockets.next.test.utils.WSClient; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Vertx; +import io.vertx.core.http.WebSocketConnectOptions; + +public class SubprotocolSelectedTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Endpoint.class, WSClient.class); + }).overrideConfigKey("quarkus.websockets-next.supported-subprotocols", "oak,larch"); + + @Inject + Vertx vertx; + + @TestHTTPResource("endpoint") + URI endUri; + + @Test + void testSubprotocol() throws InterruptedException, ExecutionException { + WSClient client = new WSClient(vertx).connect(new WebSocketConnectOptions().addSubProtocol("oak"), endUri); + assertEquals("ok", client.waitForNextMessage().toString()); + } + + @WebSocket(path = "/endpoint") + public static class Endpoint { + + @Inject + WebSocketConnection connection; + + @OnOpen + Uni open() { + if (connection.handshakeRequest().header(SEC_WEBSOCKET_PROTOCOL) == null) { + return connection.sendText("Sec-WebSocket-Protocol not set: " + connection.handshakeRequest().headers()); + } else if ("oak".equals(connection.subprotocol())) { + return connection.sendText("ok"); + } else { + return connection.sendText("Invalid protocol: " + connection.subprotocol()); + } + } + + } + +} diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java index 384d1aa6bddfc..900e86fce2f2d 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java @@ -91,6 +91,12 @@ default void closeAndAwait() { */ HandshakeRequest handshakeRequest(); + /** + * + * @return the subprotocol selected by the handshake + */ + String subprotocol(); + /** * * @return the time when this connection was created @@ -172,6 +178,31 @@ interface HandshakeRequest { */ String query(); + /** + * See The WebSocket Protocol. + */ + public static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key"; + + /** + * See The WebSocket Protocol. + */ + public static final String SEC_WEBSOCKET_EXTENSIONS = "Sec-WebSocket-Extensions"; + + /** + * See The WebSocket Protocol. + */ + public static final String SEC_WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; + + /** + * See The WebSocket Protocol. + */ + public static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + + /** + * See The WebSocket Protocol. + */ + public static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version"; + } } diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java index 6ef568f6345f7..ff38df72391d6 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/WebSocketsRuntimeConfig.java @@ -1,6 +1,7 @@ package io.quarkus.websockets.next; import java.time.Duration; +import java.util.List; import java.util.Optional; import io.quarkus.runtime.annotations.ConfigPhase; @@ -11,6 +12,13 @@ @ConfigRoot(phase = ConfigPhase.RUN_TIME) public interface WebSocketsRuntimeConfig { + /** + * See The WebSocket Protocol + * + * @return the supported subprotocols + */ + Optional> supportedSubprotocols(); + /** * TODO Not implemented yet. * diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java index 735868dfaac15..29b7d925fc25d 100644 --- a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketConnectionImpl.java @@ -119,6 +119,11 @@ public HandshakeRequest handshakeRequest() { return handshakeRequest; } + @Override + public String subprotocol() { + return webSocket.subProtocol(); + } + @Override public Instant creationTime() { return creationTime; diff --git a/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java new file mode 100644 index 0000000000000..5018b1aee2b35 --- /dev/null +++ b/extensions/websockets-next/server/runtime/src/main/java/io/quarkus/websockets/next/runtime/WebSocketHttpServerOptionsCustomizer.java @@ -0,0 +1,28 @@ +package io.quarkus.websockets.next.runtime; + +import java.util.List; + +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; + +import io.quarkus.vertx.http.HttpServerOptionsCustomizer; +import io.quarkus.websockets.next.WebSocketsRuntimeConfig; +import io.vertx.core.http.HttpServerOptions; + +@Dependent +public class WebSocketHttpServerOptionsCustomizer implements HttpServerOptionsCustomizer { + + @Inject + WebSocketsRuntimeConfig config; + + @Override + public void customizeHttpServer(HttpServerOptions options) { + config.supportedSubprotocols().orElse(List.of()).forEach(options::addWebSocketSubProtocol); + } + + @Override + public void customizeHttpsServer(HttpServerOptions options) { + config.supportedSubprotocols().orElse(List.of()).forEach(options::addWebSocketSubProtocol); + } + +}