From a286816395be230d5c400a01d1cbe005b0264011 Mon Sep 17 00:00:00 2001 From: Julien Viet Date: Thu, 28 May 2026 20:44:27 +0200 Subject: [PATCH] Support HTTP/2 / HTTP/3 extended connect. Motivation: RFC9220 and RFC8441 define an extended connect method for bootstrapping WebSocket and other protocols for HTTP/2 and HTTP/3. Changes: Implement ENABLE_CONNECT_PROTOCOL setting for HTTP/2 and HTTP/3 Implement the :protocol pseudo header passing for the CONNECT method: this provides support for setting or obtaining the connect protocol value of the pseudo header. --- .../io/vertx/core/http/Http2Settings.java | 2 +- .../io/vertx/core/http/Http3Settings.java | 2 +- .../io/vertx/core/http/HttpClientRequest.java | 14 ++++ .../java/io/vertx/core/http/HttpHeaders.java | 6 ++ .../io/vertx/core/http/HttpServerRequest.java | 5 ++ .../java/io/vertx/core/http/HttpSettings.java | 17 +++++ .../core/http/impl/HttpClientRequestImpl.java | 14 +++- .../impl/HttpClientRequestPushPromise.java | 10 +++ .../vertx/core/http/impl/HttpRequestHead.java | 4 +- .../core/http/impl/HttpServerRequestImpl.java | 7 ++ .../io/vertx/core/http/impl/HttpUtils.java | 13 +++- .../http/impl/headers/HttpRequestHeaders.java | 24 ++++++- .../impl/http1/Http1ClientConnection.java | 2 +- .../http/impl/http1/Http1ServerRequest.java | 5 ++ .../impl/http2/DefaultHttp2ClientStream.java | 7 +- .../impl/http2/DefaultHttp2ServerStream.java | 1 + .../http/impl/http3/Http3ClientStream.java | 6 ++ .../core/http/impl/http3/Http3Connection.java | 21 ++++-- .../http/impl/http3/Http3ServerStream.java | 1 + .../http/impl/tcp/TcpHttpClientTransport.java | 2 +- .../http/HttpServerRequestWrapper.java | 5 ++ .../io/vertx/tests/http/Http2ClientTest.java | 37 +++++++++++ .../io/vertx/tests/http/Http2ServerTest.java | 64 +++++++++++++++++-- .../java/io/vertx/tests/http/HttpTest.java | 2 + .../tests/http/http3/Http3ClientTest.java | 33 +++++++++- .../tests/http/http3/Http3ServerTest.java | 31 ++++++++- 26 files changed, 312 insertions(+), 23 deletions(-) diff --git a/vertx-core/src/main/java/io/vertx/core/http/Http2Settings.java b/vertx-core/src/main/java/io/vertx/core/http/Http2Settings.java index b4f5ded16c1..95ff6131119 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/Http2Settings.java +++ b/vertx-core/src/main/java/io/vertx/core/http/Http2Settings.java @@ -161,7 +161,7 @@ public final class Http2Settings extends HttpSettings { * Default constructor */ public Http2Settings() { - super(7); + super(9); extraSettings = DEFAULT_EXTRA_SETTINGS; } diff --git a/vertx-core/src/main/java/io/vertx/core/http/Http3Settings.java b/vertx-core/src/main/java/io/vertx/core/http/Http3Settings.java index 54d4f10e008..26c74627e9e 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/Http3Settings.java +++ b/vertx-core/src/main/java/io/vertx/core/http/Http3Settings.java @@ -65,7 +65,7 @@ public class Http3Settings extends HttpSettings { } public Http3Settings() { - super(8); + super(9); } public Http3Settings(Http3Settings other) { diff --git a/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java b/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java index 4387a9a6795..5d900e5ae18 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java +++ b/vertx-core/src/main/java/io/vertx/core/http/HttpClientRequest.java @@ -174,6 +174,20 @@ public interface HttpClientRequest extends WriteStream { @CacheReturn MultiMap headers(); + /** + * @return the pseudo header {@code :protocol} to use for extended connect + */ + String connectProtocol(); + + /** + * Set the {@code :protocol} pseudo header used for extended connect. + * + * @param protocol the pseudo header value + * @return a reference to this, so the API can be used fluently + */ + @Fluent + HttpClientRequest connectProtocol(String protocol); + /** * Put an HTTP header * diff --git a/vertx-core/src/main/java/io/vertx/core/http/HttpHeaders.java b/vertx-core/src/main/java/io/vertx/core/http/HttpHeaders.java index bd3acd5ac4f..c06aa5edf9f 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/HttpHeaders.java +++ b/vertx-core/src/main/java/io/vertx/core/http/HttpHeaders.java @@ -491,6 +491,12 @@ public interface HttpHeaders { @GenIgnore(GenIgnore.PERMITTED_TYPE) CharSequence PSEUDO_METHOD = Http2Headers.PseudoHeaderName.METHOD.value(); + /** + * HTTP/2 {@code :protocol} pseudo header + */ + @GenIgnore(GenIgnore.PERMITTED_TYPE) + CharSequence PSEUDO_PROTOCOL = Http2Headers.PseudoHeaderName.PROTOCOL.value(); + /** * Create an optimized {@link CharSequence} which can be used as header name or value. * This should be used if you expect to use it multiple times liked for example adding the same header name or value diff --git a/vertx-core/src/main/java/io/vertx/core/http/HttpServerRequest.java b/vertx-core/src/main/java/io/vertx/core/http/HttpServerRequest.java index 8a76560ec0a..fdb057d1d00 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/HttpServerRequest.java +++ b/vertx-core/src/main/java/io/vertx/core/http/HttpServerRequest.java @@ -137,6 +137,11 @@ default boolean isSSL() { @Nullable HostAndPort authority(boolean real); + /** + * @return the {@code :protocol} pseudo header used by extended connect + */ + String connectProtocol(); + /** * @return the total number of bytes read for the body of the request. */ diff --git a/vertx-core/src/main/java/io/vertx/core/http/HttpSettings.java b/vertx-core/src/main/java/io/vertx/core/http/HttpSettings.java index 08f496e7edd..0fa9bc263ff 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/HttpSettings.java +++ b/vertx-core/src/main/java/io/vertx/core/http/HttpSettings.java @@ -10,9 +10,13 @@ */ package io.vertx.core.http; +import io.netty.handler.codec.http2.Http2CodecUtil; import io.vertx.codegen.annotations.DataObject; +import io.vertx.core.impl.Arguments; import java.util.Arrays; +import java.util.Set; +import java.util.function.LongConsumer; /** * Base container for HTTP settings. @@ -22,6 +26,19 @@ @DataObject public abstract class HttpSettings { + /** + * HTTP {@code ENABLE_CONNECT_PROTOCOL} setting + */ + public static final HttpSetting ENABLE_CONNECT_PROTOCOL; + + static { + LongConsumer enableConnectProtocolValidator = value -> Arguments.require(value == 0L || value == 1L, + "enableConnectProtocol must be >= " + Http2CodecUtil.MIN_HEADER_LIST_SIZE); + ENABLE_CONNECT_PROTOCOL = new HttpSetting<>(0x08, "ENABLE_CONNECT_PROTOCOL", false, + val -> val ? 1L : 0L, val -> val == 1L, enableConnectProtocolValidator, Set.of(HttpVersion.HTTP_2, HttpVersion.HTTP_3)); + + } + private long presence; // Could be first element of values private final long[] values; diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java index e4e0481e77b..b22c13700eb 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestImpl.java @@ -53,6 +53,7 @@ public class HttpClientRequestImpl extends HttpClientRequestBase implements Http private int maxRedirects; private int numberOfRedirections; private final MultiMap headers; + private String connectProtocol; private boolean trailersSent; private boolean headersSent; private StreamPriority priority; @@ -186,6 +187,17 @@ public MultiMap headers() { return headers; } + @Override + public String connectProtocol() { + return connectProtocol; + } + + @Override + public HttpClientRequestImpl connectProtocol(String protocol) { + this.connectProtocol = protocol; + return this; + } + @Override public synchronized HttpClientRequest putHeader(String name, String value) { checkEnded(); @@ -533,7 +545,7 @@ private Future doWrite(Buffer buff, boolean end, boolean connect) { if (uri.isEmpty()) { uri = "/"; } - HttpRequestHead head = new HttpRequestHead(ssl ? "https" : "http", method, uri, headers, authority(), absoluteURI(), traceOperation); + HttpRequestHead head = new HttpRequestHead(ssl ? "https" : "http", method, uri, connectProtocol, headers, authority(), absoluteURI(), traceOperation); future = stream.writeHead(head, chunked, buff, writeEnd, priority, connect); } else { if (buff == null && !end) { diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java index 16a27c41e4e..ad79b8f3e01 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpClientRequestPushPromise.java @@ -65,6 +65,16 @@ public MultiMap headers() { return headers; } + @Override + public String connectProtocol() { + return null; + } + + @Override + public HttpClientRequest connectProtocol(String protocol) { + throw new IllegalStateException(); + } + @Override public Future write(Buffer data) { throw new IllegalStateException(); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpRequestHead.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpRequestHead.java index d06cb37a9a4..e46fc9a9b46 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpRequestHead.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpRequestHead.java @@ -22,15 +22,17 @@ public class HttpRequestHead { public final String scheme; public final HttpMethod method; public final String uri; + public final String protocol; public final MultiMap headers; public final HostAndPort authority; public final String absoluteURI; public final String traceOperation; - public HttpRequestHead(String scheme, HttpMethod method, String uri, MultiMap headers, HostAndPort authority, String absoluteURI, String traceOperation) { + public HttpRequestHead(String scheme, HttpMethod method, String uri, String protocol, MultiMap headers, HostAndPort authority, String absoluteURI, String traceOperation) { this.scheme = scheme; this.method = method; this.uri = uri; + this.protocol = protocol; this.headers = headers; this.authority = authority; this.absoluteURI = absoluteURI; diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpServerRequestImpl.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpServerRequestImpl.java index 617352f40c3..0fa21132a2e 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpServerRequestImpl.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpServerRequestImpl.java @@ -57,6 +57,7 @@ public class HttpServerRequestImpl extends HttpServerRequestBase { private String uri; private MultiMap headersMap; private HostAndPort authority; + private String protocol; private HostAndPort realAuthority; private String absoluteURI; private MultiMap attributes; @@ -112,6 +113,7 @@ public void handleHeaders(HttpRequestHead headers) { method = headers.method(); realAuthority = headers.authority; authority = headers.authority; + protocol = headers.protocol; if (authority == null) { String hostHeader = headers.headers.get(HttpHeaders.HOST); if (hostHeader != null) { @@ -359,6 +361,11 @@ public String scheme() { return real ? realAuthority : authority; } + @Override + public String connectProtocol() { + return protocol; + } + @Override public long bytesRead() { return stream.bytesRead(); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java index d75ba9c21f7..7acca89d802 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/HttpUtils.java @@ -27,6 +27,7 @@ import io.vertx.core.file.OpenOptions; import io.vertx.core.http.HttpClosedException; import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpSettings; import io.vertx.core.http.StreamPriority; import io.vertx.core.internal.ContextInternal; import io.vertx.core.internal.VertxInternal; @@ -379,6 +380,12 @@ private static void fromVertxInitialSettings(boolean server, io.vertx.core.http. if (vertxSettings.getMaxHeaderListSize() != DEFAULT_MAX_HEADER_LIST_SIZE) { nettySettings.maxHeaderListSize(vertxSettings.getMaxHeaderListSize()); } + if (vertxSettings.getMaxHeaderListSize() != DEFAULT_MAX_HEADER_LIST_SIZE) { + nettySettings.maxHeaderListSize(vertxSettings.getMaxHeaderListSize()); + } + if (vertxSettings.get(HttpSettings.ENABLE_CONNECT_PROTOCOL) == Boolean.TRUE) { + nettySettings.put((char)0x08, (Long)1L); + } Map extraSettings = vertxSettings.getExtraSettings(); if (extraSettings != null) { extraSettings.forEach((code, setting) -> { @@ -429,8 +436,12 @@ public static io.vertx.core.http.Http2Settings toVertxSettings(Http2Settings set if (headerTableSize != null) { converted.setHeaderTableSize(headerTableSize); } + Long enableConnectProtocol = settings.get((char)0x08); + if (enableConnectProtocol != null && enableConnectProtocol == 1L) { + converted.set(HttpSettings.ENABLE_CONNECT_PROTOCOL, true); + } settings.forEach((key, value) -> { - if (key > 6) { + if (key > 6 && key != 8) { converted.set(key, value); } }); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/headers/HttpRequestHeaders.java b/vertx-core/src/main/java/io/vertx/core/http/impl/headers/HttpRequestHeaders.java index 5f6bd7e9bef..746bd310ddf 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/headers/HttpRequestHeaders.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/headers/HttpRequestHeaders.java @@ -108,6 +108,7 @@ public String value(HttpRequestHeaders req, int index) { private HostAndPort authority; private String uri; private String scheme; + private String protocol; private String trace; public HttpRequestHeaders(Headers headers) { @@ -167,6 +168,15 @@ public HttpHeaders trace(String trace) { return this; } + public String protocol() { + return protocol; + } + + public HttpHeaders protocol(String protocol) { + this.protocol = protocol; + return this; + } + public boolean validate() { CharSequence methodHeader = headers.get(io.vertx.core.http.HttpHeaders.PSEUDO_METHOD); if (methodHeader == null) { @@ -197,7 +207,15 @@ public boolean validate() { authorityPresence = authority; } - if (method == HttpMethod.CONNECT) { + CharSequence protocolHeader = headers.get(io.vertx.core.http.HttpHeaders.PSEUDO_PROTOCOL); + String protocol; + if (method == HttpMethod.CONNECT && protocolHeader != null) { + protocol = protocolHeader.toString(); + } else { + protocol = null; + } + + if (method == HttpMethod.CONNECT && protocol == null) { if (scheme != null || uri != null || authorityPresence == null) { return false; } @@ -225,6 +243,7 @@ public boolean validate() { this.uri = uri; this.authority = authority; this.scheme = scheme; + this.protocol = protocol; return true; } @@ -254,6 +273,9 @@ public void prepare() { if (scheme != null) { headers.set(io.vertx.core.http.HttpHeaders.PSEUDO_SCHEME, scheme); } + if (method == HttpMethod.CONNECT && protocol != null) { + headers.set(io.vertx.core.http.HttpHeaders.PSEUDO_PROTOCOL, protocol); + } } @Override diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ClientConnection.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ClientConnection.java index 6d1100889dd..83637d6ebe1 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ClientConnection.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ClientConnection.java @@ -1106,7 +1106,7 @@ public synchronized void toWebSocket( if (webSocketMetrics != null) { ws.setMetric(webSocketMetrics.connected(new ObservableRequest(new HttpRequestHead( - ssl ? "https" : "http", HttpMethod.GET, requestURI, headers, authority, "/", null + ssl ? "https" : "http", HttpMethod.GET, requestURI, null, headers, authority, "/", null )))); } ws.pause(); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ServerRequest.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ServerRequest.java index 83f9184d36a..f417fbd0ba0 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ServerRequest.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http1/Http1ServerRequest.java @@ -282,6 +282,11 @@ public HostAndPort authority(boolean real) { return real ? null : authority(); } + @Override + public String connectProtocol() { + return null; + } + @Override public long bytesRead() { synchronized (conn) { diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http2/DefaultHttp2ClientStream.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http2/DefaultHttp2ClientStream.java index 6c84b39f2d0..8ab35fdeace 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/http2/DefaultHttp2ClientStream.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http2/DefaultHttp2ClientStream.java @@ -156,6 +156,11 @@ public void write() { throw new IllegalArgumentException("Missing :authority / host header"); } headers.authority(request.authority); + if (request.protocol != null) { + headers.protocol(request.protocol); + headers.path(request.uri); + headers.scheme(connection.isSsl() ? "https" : "http"); + } // don't end stream for CONNECT e = false; } else { @@ -217,7 +222,7 @@ public void onPush(Http2ClientStream pushStream, int promisedStreamId, HttpHeade } public void onPush(DefaultHttp2ClientStream pushStream, int promisedStreamId, HttpRequestHeaders headers, boolean writable) { - HttpClientPush push = new HttpClientPush(new HttpRequestHead(headers.scheme(), headers.method(), headers.path(), headers, headers.authority(), null, null), pushStream); + HttpClientPush push = new HttpClientPush(new HttpRequestHead(headers.scheme(), headers.method(), headers.path(), null, headers, headers.authority(), null, null), pushStream); pushStream.init(promisedStreamId, writable); if (pushStream.observable != null) { pushStream.observable.observePush(headers); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http2/DefaultHttp2ServerStream.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http2/DefaultHttp2ServerStream.java index 54ed55c170c..44d431894b7 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/http2/DefaultHttp2ServerStream.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http2/DefaultHttp2ServerStream.java @@ -152,6 +152,7 @@ void handleHeader(HttpHeaders map) { requestHeaders.scheme(), requestHeaders.method(), requestHeaders.path(), + requestHeaders.protocol(), map, requestHeaders.authority(), null, diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3ClientStream.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3ClientStream.java index f2ad9759a3f..8e7d2713abf 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3ClientStream.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3ClientStream.java @@ -131,6 +131,12 @@ public Future writeHead(HttpRequestHead request, boolean chunked, Buffer c if (request.method != HttpMethod.CONNECT) { headers.path(request.uri); headers.scheme("https"); + } else { + if (request.protocol != null) { + headers.path(request.uri); + headers.scheme("https"); + headers.protocol(request.protocol); + } } headers.prepare(); return writeHeaders(headers, chunk, end); diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3Connection.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3Connection.java index 49d9b13321b..5d0c02ebc09 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3Connection.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3Connection.java @@ -68,15 +68,19 @@ public Http3Connection(QuicConnectionInternal connection, Http3Settings localSet io.netty.handler.codec.http3.Http3Settings nettyLocalSettings() { io.netty.handler.codec.http3.Http3Settings nSettings = new io.netty.handler.codec.http3.Http3Settings(); - Long val; - if ((val = localSettings.get(Http3Settings.MAX_FIELD_SECTION_SIZE)) != null) { - nSettings.maxFieldSectionSize(val); + Long lval; + if ((lval = localSettings.get(Http3Settings.MAX_FIELD_SECTION_SIZE)) != null) { + nSettings.maxFieldSectionSize(lval); } - if ((val = localSettings.get(Http3Settings.QPACK_BLOCKED_STREAMS)) != null) { - nSettings.qpackBlockedStreams(val); + if ((lval = localSettings.get(Http3Settings.QPACK_BLOCKED_STREAMS)) != null) { + nSettings.qpackBlockedStreams(lval); } - if ((val = localSettings.get(Http3Settings.QPACK_MAX_TABLE_CAPACITY)) != null) { - nSettings.qpackMaxTableCapacity(val); + if ((lval = localSettings.get(Http3Settings.QPACK_MAX_TABLE_CAPACITY)) != null) { + nSettings.qpackMaxTableCapacity(lval); + } + Boolean bval; + if ((bval = localSettings.get(HttpSettings.ENABLE_CONNECT_PROTOCOL)) != null) { + nSettings.enableConnectProtocol(bval); } return nSettings; } @@ -250,6 +254,9 @@ private void handleSettings(io.netty.handler.codec.http3.Http3Settings nSettings case HTTP3_SETTINGS_MAX_FIELD_SECTION_SIZE: vSetting = Http3Settings.MAX_FIELD_SECTION_SIZE; break; + case HTTP3_SETTINGS_ENABLE_CONNECT_PROTOCOL: + vSetting = HttpSettings.ENABLE_CONNECT_PROTOCOL; + break; default: continue; } diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3ServerStream.java b/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3ServerStream.java index 36bfc91596d..2e39bb022bc 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3ServerStream.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/http3/Http3ServerStream.java @@ -62,6 +62,7 @@ protected boolean handleHead(HttpHeaders headers) { requestHeaders.scheme(), requestHeaders.method(), requestHeaders.path(), + requestHeaders.protocol(), requestHeaders, requestHeaders.authority(), null, diff --git a/vertx-core/src/main/java/io/vertx/core/http/impl/tcp/TcpHttpClientTransport.java b/vertx-core/src/main/java/io/vertx/core/http/impl/tcp/TcpHttpClientTransport.java index 8ac09b26cd4..2fb15c84e7b 100644 --- a/vertx-core/src/main/java/io/vertx/core/http/impl/tcp/TcpHttpClientTransport.java +++ b/vertx-core/src/main/java/io/vertx/core/http/impl/tcp/TcpHttpClientTransport.java @@ -356,7 +356,7 @@ private void http1xConnected(HttpVersion version, future.tryComplete(unwrap); }); stream.exceptionHandler(future::tryFail); - HttpRequestHead request = new HttpRequestHead("http", OPTIONS, "/", HttpHeaders.headers(), HostAndPort.authority(server.host(), server.port()), + HttpRequestHead request = new HttpRequestHead("http", OPTIONS, "/", null, HttpHeaders.headers(), HostAndPort.authority(server.host(), server.port()), "http://" + server + "/", null); stream.writeHead(request, false, null, true, null, false); } else { diff --git a/vertx-core/src/main/java/io/vertx/core/internal/http/HttpServerRequestWrapper.java b/vertx-core/src/main/java/io/vertx/core/internal/http/HttpServerRequestWrapper.java index c2f223610f3..f1509ad56a2 100644 --- a/vertx-core/src/main/java/io/vertx/core/internal/http/HttpServerRequestWrapper.java +++ b/vertx-core/src/main/java/io/vertx/core/internal/http/HttpServerRequestWrapper.java @@ -105,6 +105,11 @@ public HostAndPort authority() { return delegate.authority(real); } + @Override + public String connectProtocol() { + return delegate.connectProtocol(); + } + @Override public boolean isValidAuthority() { return delegate.isValidAuthority(); diff --git a/vertx-core/src/test/java/io/vertx/tests/http/Http2ClientTest.java b/vertx-core/src/test/java/io/vertx/tests/http/Http2ClientTest.java index bb7a54f0c85..a6450be6cdd 100644 --- a/vertx-core/src/test/java/io/vertx/tests/http/Http2ClientTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/http/Http2ClientTest.java @@ -190,6 +190,19 @@ public void testServerSettings() throws Exception { await(); } + @Test + public void testEnableConnectProtocolSetting() throws Exception { + io.vertx.core.http.Http2Settings settings = new io.vertx.core.http.Http2Settings(); + settings.set(io.vertx.core.http.Http2Settings.ENABLE_CONNECT_PROTOCOL, true); + server = vertx.createHttpServer(new HttpServerOptions(serverOptions).setInitialSettings(settings)); + server.requestHandler(req -> { + }); + startServer(); + HttpConnection connection = client.connect(requestOptions).await(); + HttpSettings receivedSettings = connection.remoteSettings(); + Assert.assertTrue(receivedSettings.get(Http2Settings.ENABLE_CONNECT_PROTOCOL)); + } + @Test public void testReduceMaxConcurrentStreams() throws Exception { int initial = 10; @@ -1657,6 +1670,30 @@ public void testServerCloseNetSocket() throws Exception { await(); } + @Test + public void testExtendedConnect() throws Exception { + + HttpServerOptions options = createBaseServerOptions(); + options.getInitialSettings().set(Http2Settings.ENABLE_CONNECT_PROTOCOL, true); + + server.close(); + server = vertx.createHttpServer(options); + server.requestHandler(request -> { + Assert.assertEquals("the-protocol", request.connectProtocol()); + request.response().end(); + }); + + startServer(testAddress); + + client.request(new RequestOptions(requestOptions) + .setMethod(HttpMethod.CONNECT)) + .compose(request -> request.connectProtocol("the-protocol") + .send() + .expecting(HttpResponseExpectation.SC_OK) + .compose(HttpClientResponse::end)) + .await(); + } + @Test public void testwriteHeadersCompletionHandler() throws Exception { AtomicInteger status = new AtomicInteger(); diff --git a/vertx-core/src/test/java/io/vertx/tests/http/Http2ServerTest.java b/vertx-core/src/test/java/io/vertx/tests/http/Http2ServerTest.java index 171d7f90b7e..f775fab1f74 100644 --- a/vertx-core/src/test/java/io/vertx/tests/http/Http2ServerTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/http/Http2ServerTest.java @@ -110,19 +110,19 @@ */ public class Http2ServerTest extends Http2TestBase { - private static Http2Headers headers(String method, String scheme, String path) { + static Http2Headers headers(String method, String scheme, String path) { return new DefaultHttp2Headers().method(method).scheme(scheme).path(path); } - private static Http2Headers GET(String scheme, String path) { + static Http2Headers GET(String scheme, String path) { return headers("GET", scheme, path); } - private static Http2Headers GET(String path) { + static Http2Headers GET(String path) { return headers("GET", "https", path); } - private static Http2Headers POST(String path) { + static Http2Headers POST(String path) { return headers("POST", "https", path); } @@ -446,6 +446,30 @@ public void testClientSettings() throws Exception { await(); } + @Test + public void testEnableConnectProtocolSetting() throws Exception { + io.vertx.core.http.Http2Settings settings = new io.vertx.core.http.Http2Settings(); + settings.set(io.vertx.core.http.Http2Settings.ENABLE_CONNECT_PROTOCOL, true); + server = vertx.createHttpServer(new HttpServerOptions(serverOptions).setInitialSettings(settings)); + server.requestHandler(req -> { + }); + startServer(); + TestClient client = new TestClient(); + client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, connection -> { + connection.decoder.frameListener(new Http2FrameAdapter() { + @Override + public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings newSettings) throws Http2Exception { + vertx.runOnContext(v -> { + Long val = newSettings.get((char) 0x08); + Assert.assertEquals((Long) 1L, val); + testComplete(); + }); + } + }); + }); + await(); + } + @Test public void testGet() throws Exception { String expected = TestUtils.randomAlphaString(1000); @@ -735,6 +759,38 @@ public void testConnect() throws Exception { await(); } + @Test + public void testExtendedConnect() throws Exception { + io.vertx.core.http.Http2Settings settings = new io.vertx.core.http.Http2Settings(); + settings.set(io.vertx.core.http.Http2Settings.ENABLE_CONNECT_PROTOCOL, true); + server = vertx.createHttpServer(new HttpServerOptions(serverOptions).setInitialSettings(settings)); + server.requestHandler(req -> { + Assert.assertEquals("the-protocol", req.connectProtocol()); + req.response().end(); + }); + startServer(); + TestClient client = new TestClient(); + client.connect(DEFAULT_HTTPS_PORT, DEFAULT_HTTPS_HOST, connection -> { + int id = connection.nextStreamId(); + connection.decoder.frameListener(new Http2EventAdapter() { + @Override + public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception { + if (endStream) { + vertx.runOnContext(v -> { + testComplete(); + }); + } + } + }); + Http2Headers headers = headers("CONNECT", "https", "/") + .authority(DEFAULT_HTTPS_HOST_AND_PORT) + .set(":protocol", "the-protocol"); + connection.encoder.writeHeaders(connection.context, id, headers, 0, true, connection.context.newPromise()); + connection.context.flush(); + }); + await(); + } + @Test public void testServerRequestPauseResume() throws Exception { testStreamPauseResume(req -> Future.succeededFuture(req)); diff --git a/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java b/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java index 5e80584c0a4..bdba0f136c5 100644 --- a/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/http/HttpTest.java @@ -3777,6 +3777,8 @@ class MockReq implements HttpClientRequest { public String getURI() { throw new UnsupportedOperationException(); } public HttpClientRequest setURI(String uri) { throw new UnsupportedOperationException(); } public String path() { throw new UnsupportedOperationException(); } + public String connectProtocol() { throw new UnsupportedOperationException(); } + public HttpClientRequest connectProtocol(String protocol) { return null; } public String query() { throw new UnsupportedOperationException(); } public MultiMap headers() { return headers; } public HttpClientRequest putHeader(String name, String value) { throw new UnsupportedOperationException(); } diff --git a/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3ClientTest.java b/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3ClientTest.java index bdd32efb842..69f289df0d3 100644 --- a/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3ClientTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3ClientTest.java @@ -401,7 +401,8 @@ public void testSettings() throws Exception { .setInitialSettings(new Http3Settings() .setMaxFieldSectionSize(1024) .setQPackMaxTableCapacity(1024) - .setQPackBlockedStreams(1024))); + .setQPackBlockedStreams(1024) + .set(HttpSettings.ENABLE_CONNECT_PROTOCOL, true))); server.close(); server = vertx.createHttpServer(serverConfig, serverSslOptions); @@ -443,6 +444,7 @@ public void testSettings() throws Exception { Assert.assertEquals(1024L, (long)settings.get(io.vertx.core.http.Http3Settings.MAX_FIELD_SECTION_SIZE)); Assert.assertEquals(1024L, (long)settings.get(io.vertx.core.http.Http3Settings.QPACK_BLOCKED_STREAMS)); Assert.assertEquals(1024L, (long)settings.get(io.vertx.core.http.Http3Settings.QPACK_MAX_TABLE_CAPACITY)); + Assert.assertEquals(true, settings.get(HttpSettings.ENABLE_CONNECT_PROTOCOL)); } // @Test @@ -519,4 +521,33 @@ public void testMaxActiveStreams() throws Exception { Assert.assertTrue(e instanceof TimeoutException); } } + + @Test + public void testExtendedConnect() { + + server.close(); + Http3ServerConfig cfg = new Http3ServerConfig() + .setInitialSettings(new io.vertx.core.http.Http3Settings() + .set(HttpSettings.ENABLE_CONNECT_PROTOCOL, true)); + server = vertx.createHttpServer(new HttpServerConfig(serverOptions).setHttp3Config(cfg), serverSslOptions); + + server.requestHandler(req -> { + Assert.assertEquals("the-protocol", req.connectProtocol()); + req.response().end(); + }); + server.listen(8443, "localhost").await(); + + HttpClientConnection connection = client.connect(new HttpConnectOptions() + .setHost("localhost") + .setPort(8443)).await(); + + connection.request(HttpMethod.CONNECT, 8443, "localhost", "/") + .expecting(request -> request.version() == HttpVersion.HTTP_3) + .compose(request -> request + .connectProtocol("the-protocol") + .send() + .expecting(HttpResponseExpectation.SC_OK) + .compose(HttpClientResponse::end)) + .await(); + } } diff --git a/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3ServerTest.java b/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3ServerTest.java index fa1a3f01a2b..65f600a9e74 100644 --- a/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3ServerTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/http/http3/Http3ServerTest.java @@ -411,7 +411,8 @@ public void testSettings() throws Exception { .setInitialSettings(new io.vertx.core.http.Http3Settings() .setMaxFieldSectionSize(1024) .setQPackBlockedStreams(1024) - .setQPackMaxTableCapacity(1024))); + .setQPackMaxTableCapacity(1024) + .set(HttpSettings.ENABLE_CONNECT_PROTOCOL, true))); server = vertx.createHttpServer(config, sslOptions()); server.connectionHandler(connection -> { @@ -443,7 +444,8 @@ public void testSettings() throws Exception { Http3Settings settings = connection.remoteSettings(); Assert.assertEquals(1024L, (long)settings.maxFieldSectionSize()); Assert.assertEquals(1024L, (long)settings.qpackMaxTableCapacity()); - Assert.assertEquals(1024L, (long)settings.qpackBlockedStreams()); + Assert.assertEquals(1024L, (long)settings.qpackMaxTableCapacity()); + Assert.assertEquals(true, settings.connectProtocolEnabled()); } @Test @@ -467,6 +469,31 @@ public void testConnect() throws Exception { await(); } + @Test + public void testExtendedConnect() throws Exception { + server.close(); + Http3ServerConfig cfg = new Http3ServerConfig() + .setInitialSettings(new io.vertx.core.http.Http3Settings() + .set(HttpSettings.ENABLE_CONNECT_PROTOCOL, true)); + server = vertx.createHttpServer(serverConfig().setHttp3Config(cfg), sslOptions()); + server.requestHandler(req -> { + Assert.assertEquals("the-protocol", req.connectProtocol()); + testComplete(); + }); + + server.listen(8443, "localhost").await(); + + Http3TestClient.Client.Connection connection = client.connect(new InetSocketAddress(NetUtil.LOCALHOST4, 8443)); + Http3TestClient.Client.Stream stream = connection.stream(); + stream.write(new DefaultHttp3Headers() + .method("CONNECT") + .authority("whatever.com") + .scheme("https") + .path("/") + .protocol("the-protocol"), true, false); + await(); + } + @Test public void testServerSharing() throws Exception { VertxInternal vxi = (VertxInternal) vertx;