Skip to content

Commit

Permalink
Decouple the handshake part ServerWebSocket API.
Browse files Browse the repository at this point in the history
Motivation:

The server WebSocket API can control handshake implicitly (e.g. sending a message) or explicitly (accept or any WebSocket interaction). This result in a more complex implementation than it should be for such API.

Changes:

Extract the handshake API of the ServerWebSocket API in a new ServerWebSocketHandshake API for which an handler can be set when WebSocket handshake needs to be controlled.
  • Loading branch information
vietj committed Oct 10, 2024
1 parent e7e2b88 commit 34c459a
Show file tree
Hide file tree
Showing 18 changed files with 391 additions and 506 deletions.
11 changes: 5 additions & 6 deletions vertx-core/src/main/asciidoc/http.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1989,14 +1989,13 @@ When a WebSocket connection is made to the server, the handler will be called, p
{@link examples.HTTPExamples#example51}
----

You can choose to reject the WebSocket by calling {@link io.vertx.core.http.ServerWebSocket#reject()}.
===== Server WebSocket handshake

[source,$lang]
----
{@link examples.HTTPExamples#example52}
----
By default, the server accepts any inbound WebSocket.

You can set a WebSocket handshake handler to control the outcome of a WebSocket handshake, i.e. accept or reject an incoming WebSocket.

You can perform an asynchronous handshake by calling {@link io.vertx.core.http.ServerWebSocket#setHandshake} with a `Future`:
You can choose to reject the WebSocket by calling {@link io.vertx.core.http.ServerWebSocketHandshake#accept()} or {@link io.vertx.core.http.ServerWebSocketHandshake#reject()}.

[source,$lang]
----
Expand Down
29 changes: 10 additions & 19 deletions vertx-core/src/main/java/examples/HTTPExamples.java
Original file line number Diff line number Diff line change
Expand Up @@ -1049,29 +1049,20 @@ public void example51(HttpServer server) {
});
}

public void example52(HttpServer server) {

server.webSocketHandler(webSocket -> {
if (webSocket.path().equals("/myapi")) {
webSocket.reject();
} else {
// Do something
}
});
}

public void exampleAsynchronousHandshake(HttpServer server) {
server.webSocketHandler(webSocket -> {
Promise<Integer> promise = Promise.promise();
webSocket.setHandshake(promise.future());
authenticate(webSocket.headers(), ar -> {
server.webSocketHandshakeHandler(handshake -> {
authenticate(handshake.headers(), ar -> {
if (ar.succeeded()) {
// Terminate the handshake with the status code 101 (Switching Protocol)
// Reject the handshake with 401 (Unauthorized)
promise.complete(ar.result() ? 101 : 401);
if (ar.result()) {
// Terminate the handshake with the status code 101 (Switching Protocol)
handshake.accept();
} else {
// Reject the handshake with 401 (Unauthorized)
handshake.reject(401);
}
} else {
// Will send a 500 error
promise.fail(ar.cause());
handshake.reject(500);
}
});
});
Expand Down
13 changes: 12 additions & 1 deletion vertx-core/src/main/java/io/vertx/core/http/HttpServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import io.vertx.codegen.annotations.Fluent;
import io.vertx.codegen.annotations.VertxGen;
import io.vertx.core.metrics.Measured;
import io.vertx.core.net.NetSocket;
import io.vertx.core.net.ServerSSLOptions;
import io.vertx.core.net.SocketAddress;
import io.vertx.core.net.TrafficShapingOptions;
Expand Down Expand Up @@ -78,6 +77,18 @@ public interface HttpServer extends Measured {
@Fluent
HttpServer connectionHandler(Handler<HttpConnection> handler);

/**
* Set a handler for WebSocket handshake.
*
* <p>When an inbound HTTP request presents a WebSocket upgrade, this handler is called first. The handler
* can chose to {@link ServerWebSocketHandshake#accept()} or {@link ServerWebSocketHandshake#reject()} the request.</p>
*
* <p>Setting no handler, implicitly accepts any HTTP request connection presenting an upgrade header and upgrades it
* to a WebSocket.</p>
*/
@Fluent
HttpServer webSocketHandshakeHandler(Handler<ServerWebSocketHandshake> handler);

/**
* Set an exception handler called for socket errors happening before the HTTP connection
* is established, e.g during the TLS handshake.
Expand Down
55 changes: 0 additions & 55 deletions vertx-core/src/main/java/io/vertx/core/http/ServerWebSocket.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,61 +93,6 @@ default ServerWebSocket resume() {
@Nullable
String query();

/**
* Accept the WebSocket and terminate the WebSocket handshake.
* <p/>
* This method should be called from the WebSocket handler to explicitly accept the WebSocket and
* terminate the WebSocket handshake.
*
* @throws IllegalStateException when the WebSocket handshake is already set
*/
void accept();

/**
* Reject the WebSocket.
* <p>
* Calling this method from the WebSocket handler when it is first passed to you gives you the opportunity to reject
* the WebSocket, which will cause the WebSocket handshake to fail by returning
* a {@literal 502} response code.
* <p>
* You might use this method, if for example you only want to accept WebSockets with a particular path.
*
* @throws IllegalStateException when the WebSocket handshake is already set
*/
default void reject() {
// SC_BAD_GATEWAY
reject(502);
}

/**
* Like {@link #reject()} but with a {@code status}.
*/
void reject(int status);

/**
* Set an asynchronous result for the handshake, upon completion of the specified {@code future}, the
* WebSocket will either be
*
* <ul>
* <li>accepted when the {@code future} succeeds with the HTTP {@literal 101} status code</li>
* <li>rejected when the {@code future} is succeeds with an HTTP status code different than {@literal 101}</li>
* <li>rejected when the {@code future} fails with the HTTP status code {@code 500}</li>
* </ul>
*
* The provided future might be completed by the WebSocket itself, e.g calling the {@link #close()} method
* will try to accept the handshake and close the WebSocket afterward. Thus it is advised to try to complete
* the {@code future} with {@link Promise#tryComplete} or {@link Promise#tryFail}.
* <p>
* This method should be called from the WebSocket handler to explicitly set an asynchronous handshake.
* <p>
* Calling this method will override the {@code future} completion handler.
*
* @param future the future to complete with
* @return a future notified when the handshake has completed
* @throws IllegalStateException when the WebSocket has already an asynchronous result
*/
Future<Integer> setHandshake(Future<Integer> future);

/**
* {@inheritDoc}
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* Copyright (c) 2011-2024 Contributors to the Eclipse Foundation
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
*
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
*/
package io.vertx.core.http;

import io.vertx.codegen.annotations.CacheReturn;
import io.vertx.codegen.annotations.GenIgnore;
import io.vertx.codegen.annotations.Nullable;
import io.vertx.codegen.annotations.VertxGen;
import io.vertx.core.Future;
import io.vertx.core.MultiMap;
import io.vertx.core.net.HostAndPort;
import io.vertx.core.net.SocketAddress;

import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import java.security.cert.Certificate;
import java.util.List;

/**
* A server WebSocket handshake, allows to control acceptance or rejection of a WebSocket.
*
* @author <a href="mailto:julien@julienviet.com">Julien Viet</a>
*/
@VertxGen
public interface ServerWebSocketHandshake {

/**
* Returns the HTTP headers.
*
* @return the headers
*/
MultiMap headers();

/**
* @return the WebSocket handshake scheme
*/
@Nullable
String scheme();

/**
* @return the WebSocket handshake authority
*/
@Nullable
HostAndPort authority();

/*
* @return the WebSocket handshake URI. This is a relative URI.
*/
String uri();

/**
* @return the WebSocket handshake path.
*/
String path();

/**
* @return the WebSocket handshake query string.
*/
@Nullable
String query();

/**
* Accept the WebSocket and terminate the WebSocket handshake.
* <p/>
* This method should be called from the WebSocket handler to explicitly accept the WebSocket and
* terminate the WebSocket handshake.
*
* @throws IllegalStateException when the WebSocket handshake is already set
*/
Future<ServerWebSocket> accept();

/**
* Reject the WebSocket.
* <p>
* Calling this method from the WebSocket handler when it is first passed to you gives you the opportunity to reject
* the WebSocket, which will cause the WebSocket handshake to fail by returning
* a {@literal 502} response code.
* <p>
* You might use this method, if for example you only want to accept WebSockets with a particular path.
*
* @throws IllegalStateException when the WebSocket handshake is already set
*/
default Future<Void> reject() {
// SC_BAD_GATEWAY
return reject(502);
}

/**
* Like {@link #reject()} but with a {@code status}.
*/
Future<Void> reject(int status);

/**
* @return the remote address for this connection, possibly {@code null} (e.g a server bound on a domain socket).
* If {@code useProxyProtocol} is set to {@code true}, the address returned will be of the actual connecting client.
*/
@CacheReturn
SocketAddress remoteAddress();

/**
* @return the local address for this connection, possibly {@code null} (e.g a server bound on a domain socket)
* If {@code useProxyProtocol} is set to {@code true}, the address returned will be of the proxy.
*/
@CacheReturn
SocketAddress localAddress();

/**
* @return true if this {@link io.vertx.core.http.HttpConnection} is encrypted via SSL/TLS.
*/
boolean isSsl();

/**
* @return SSLSession associated with the underlying socket. Returns null if connection is
* not SSL.
* @see javax.net.ssl.SSLSession
*/
@GenIgnore(GenIgnore.PERMITTED_TYPE)
SSLSession sslSession();

/**
* @return an ordered list of the peer certificates. Returns null if connection is
* not SSL.
* @throws javax.net.ssl.SSLPeerUnverifiedException SSL peer's identity has not been verified.
* @see SSLSession#getPeerCertificates() ()
* @see #sslSession()
*/
@GenIgnore()
List<Certificate> peerCertificates() throws SSLPeerUnverifiedException;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.vertx.core.http;

public interface WebSocketConnection {

void accept();

void reject();

}
Original file line number Diff line number Diff line change
Expand Up @@ -994,8 +994,8 @@ synchronized void toWebSocket(
}
if (future.isSuccess()) {

VertxHandler<WebSocketConnection> handler = VertxHandler.create(ctx -> {
WebSocketConnection conn = new WebSocketConnection(context, ctx, false, TimeUnit.SECONDS.toMillis(options.getClosingTimeout()), client.metrics());
VertxHandler<WebSocketConnectionImpl> handler = VertxHandler.create(ctx -> {
WebSocketConnectionImpl conn = new WebSocketConnectionImpl(context, ctx, false, TimeUnit.SECONDS.toMillis(options.getClosingTimeout()), client.metrics());
WebSocketImpl webSocket = new WebSocketImpl(
context,
conn,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@
import io.vertx.core.Handler;
import io.vertx.core.Promise;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.ServerWebSocketHandshake;
import io.vertx.core.internal.buffer.BufferInternal;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.ServerWebSocket;
import io.vertx.core.internal.ContextInternal;
import io.vertx.core.internal.PromiseInternal;
import io.vertx.core.net.NetSocket;
Expand Down Expand Up @@ -280,7 +280,7 @@ String serverOrigin() {
return serverOrigin;
}

void createWebSocket(Http1xServerRequest request, PromiseInternal<ServerWebSocket> promise) {
void createWebSocket(Http1xServerRequest request, PromiseInternal<ServerWebSocketHandshake> promise) {
context.execute(() -> {
if (request != responseInProgress) {
promise.fail("Invalid request");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -386,25 +386,22 @@ public String getFormAttribute(String attributeName) {

@Override
public Future<ServerWebSocket> toWebSocket() {
return webSocket().map(ws -> {
ws.accept();
return ws;
});
return webSocket().compose(handshake -> handshake.accept());
}

/**
* @return a future of the un-accepted WebSocket
*/
Future<ServerWebSocket> webSocket() {
PromiseInternal<ServerWebSocket> promise = context.promise();
Future<ServerWebSocketHandshake> webSocket() {
PromiseInternal<ServerWebSocketHandshake> promise = context.promise();
webSocket(promise);
return promise.future();
}

/**
* Handle the request when a WebSocket upgrade header is present.
*/
private void webSocket(PromiseInternal<ServerWebSocket> promise) {
private void webSocket(PromiseInternal<ServerWebSocketHandshake> promise) {
BufferInternal body = BufferInternal.buffer();
boolean[] failed = new boolean[1];
handler(buff -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.vertx.core.Handler;
import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.http.ServerWebSocket;
import io.vertx.core.http.ServerWebSocketHandshake;

import static io.vertx.core.http.HttpHeaders.UPGRADE;
import static io.vertx.core.http.HttpHeaders.WEBSOCKET;
Expand All @@ -37,17 +38,20 @@ public Http1xServerRequestHandler(HttpServerConnectionHandler handlers) {

@Override
public void handle(HttpServerRequest req) {
Handler<ServerWebSocket> wsHandler = handlers.wsHandler;
Handler<ServerWebSocket> wsHandler = handlers.webSocketHandler;
Handler<ServerWebSocketHandshake> wsHandshakeHandler = handlers.webSocketHandshakeHandler;
Handler<HttpServerRequest> reqHandler = handlers.requestHandler;
if (wsHandler != null ) {
if (req.headers().contains(UPGRADE, WEBSOCKET, true) && handlers.server.wsAccept()) {
// Missing upgrade header + null request handler will be handled when creating the handshake by sending a 400 error
// handle((Http1xServerRequest) req, wsHandler);
((Http1xServerRequest)req).webSocket().onComplete(ar -> {
if (ar.succeeded()) {
ServerWebSocketHandshaker ws = (ServerWebSocketHandshaker) ar.result();
wsHandler.handle(ws);
ws.tryAccept();
ServerWebSocketHandshake handshake = ar.result();
if (wsHandshakeHandler != null) {
wsHandshakeHandler.handle(handshake);
} else {
handshake.accept().onSuccess(wsHandler);
}
} else {
// ????
}
Expand Down
Loading

0 comments on commit 34c459a

Please sign in to comment.