From 0e209bc292c5b9386031f30389c84f05ab5b29a7 Mon Sep 17 00:00:00 2001 From: Violeta Georgieva Date: Sat, 13 Apr 2024 10:36:12 +0300 Subject: [PATCH] Initial HTTP/3 server support --- docs/modules/ROOT/pages/http-client.adoc | 2 +- docs/modules/ROOT/pages/http-server.adoc | 30 ++- .../netty/transport/ServerTransport.java | 13 +- .../netty/transport/TransportConfig.java | 4 +- reactor-netty-examples/build.gradle | 1 + .../http/server/http3/Application.java | 52 +++++ .../reactor/netty/http/Http3SettingsSpec.java | 38 +++- .../http/server/Http3ChannelInitializer.java | 120 +++++++++++ .../reactor/netty/http/server/Http3Codec.java | 111 ++++++++++ .../Http3StreamBridgeServerHandler.java | 203 ++++++++++++++++++ .../reactor/netty/http/server/HttpServer.java | 5 + .../netty/http/server/HttpServerConfig.java | 72 ++++++- .../reactor-netty-http/reflect-config.json | 21 ++ 13 files changed, 655 insertions(+), 17 deletions(-) create mode 100644 reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/http3/Application.java create mode 100644 reactor-netty-http/src/main/java/reactor/netty/http/server/Http3ChannelInitializer.java create mode 100644 reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java create mode 100644 reactor-netty-http/src/main/java/reactor/netty/http/server/Http3StreamBridgeServerHandler.java diff --git a/docs/modules/ROOT/pages/http-client.adoc b/docs/modules/ROOT/pages/http-client.adoc index 3e108cc0a4..7bca982e9a 100644 --- a/docs/modules/ROOT/pages/http-client.adoc +++ b/docs/modules/ROOT/pages/http-client.adoc @@ -333,7 +333,7 @@ include::{examples-dir}/http2/H2CApplication.java[lines=18..41] {http-source-link}/reactor/netty/http/HttpProtocol.java [%unbreakable] ---- -include::{sourcedir}/reactor/netty/http/HttpProtocol.java[lines=24..52] +include::{sourcedir}/reactor/netty/http/HttpProtocol.java[lines=24..58] ---- include::partial$proxy.adoc[] diff --git a/docs/modules/ROOT/pages/http-server.adoc b/docs/modules/ROOT/pages/http-server.adoc index 25cde1ee7b..97582c80bd 100644 --- a/docs/modules/ROOT/pages/http-server.adoc +++ b/docs/modules/ROOT/pages/http-server.adoc @@ -501,7 +501,35 @@ hello {http-source-link}/reactor/netty/http/HttpProtocol.java ---- -include::{sourcedir}/reactor/netty/http/HttpProtocol.java[lines=24..52] +include::{sourcedir}/reactor/netty/http/HttpProtocol.java[lines=24..58] +---- + +[[HTTP3]] +== HTTP/3 + +By default, the `HTTP` server supports `HTTP/1.1`. If you need `HTTP/3`, you can get it through configuration. +In addition to the protocol configuration, you need to add dependency to `io.netty.incubator:netty-incubator-codec-http3`. + +The following listing presents a simple `HTTP3` example: + +{examples-link}/http3/Application.java +---- +include::{examples-dir}/http3/Application.java[lines=18..52] +---- +<1> Configures the server to support only `HTTP/3` +<2> Configures `SSL` +<3> Configures `HTTP/3` settings + +The application should now behave as follows: + +[source,bash] +---- +$ curl --http3 https://localhost:8080 -i +HTTP/3 200 +server: reactor-netty +content-length: 5 + +hello ---- [[metrics]] diff --git a/reactor-netty-core/src/main/java/reactor/netty/transport/ServerTransport.java b/reactor-netty-core/src/main/java/reactor/netty/transport/ServerTransport.java index 11538b4c70..0ac817859f 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/transport/ServerTransport.java +++ b/reactor-netty-core/src/main/java/reactor/netty/transport/ServerTransport.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2024 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,6 +38,7 @@ import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; import io.netty.channel.ServerChannel; +import io.netty.channel.socket.DatagramChannel; import io.netty.channel.unix.DomainSocketAddress; import io.netty.handler.codec.DecoderException; import io.netty.util.AttributeKey; @@ -110,9 +111,13 @@ public Mono bind() { ConnectionObserver childObs = new ChildObserver(config.defaultChildObserver().then(config.childObserver())); - Acceptor acceptor = new Acceptor(config.childEventLoopGroup(), config.channelInitializer(childObs, null, true), - config.childOptions, config.childAttrs, isDomainSocket); - TransportConnector.bind(config, new AcceptorInitializer(acceptor), local, isDomainSocket) + ChannelInitializer channelInitializer = config.channelInitializer(childObs, null, true); + if (!config.channelType(isDomainSocket).equals(DatagramChannel.class)) { + Acceptor acceptor = new Acceptor(config.childEventLoopGroup(), channelInitializer, + config.childOptions, config.childAttrs, isDomainSocket); + channelInitializer = new AcceptorInitializer(acceptor); + } + TransportConnector.bind(config, channelInitializer, local, isDomainSocket) .subscribe(disposableServer); }); diff --git a/reactor-netty-core/src/main/java/reactor/netty/transport/TransportConfig.java b/reactor-netty-core/src/main/java/reactor/netty/transport/TransportConfig.java index f603d09ad2..ac545c4a8b 100644 --- a/reactor-netty-core/src/main/java/reactor/netty/transport/TransportConfig.java +++ b/reactor-netty-core/src/main/java/reactor/netty/transport/TransportConfig.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020-2023 VMware, Inc. or its affiliates, All Rights Reserved. + * Copyright (c) 2020-2024 VMware, Inc. or its affiliates, All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -113,7 +113,7 @@ public final ChannelGroup channelGroup() { * @param onServer channel initializer for the server or for the client * @return the {@link ChannelInitializer} that will be used for initializing the channel pipeline */ - public final ChannelInitializer channelInitializer(ConnectionObserver connectionObserver, + public ChannelInitializer channelInitializer(ConnectionObserver connectionObserver, @Nullable SocketAddress remoteAddress, boolean onServer) { requireNonNull(connectionObserver, "connectionObserver"); return new TransportChannelInitializer(this, connectionObserver, remoteAddress, onServer); diff --git a/reactor-netty-examples/build.gradle b/reactor-netty-examples/build.gradle index 9a527aaeb4..9ee4881670 100644 --- a/reactor-netty-examples/build.gradle +++ b/reactor-netty-examples/build.gradle @@ -31,6 +31,7 @@ dependencies { runtimeOnly "io.netty:netty-tcnative-boringssl-static:$boringSslVersion$os_suffix" // Needed for proxy testing runtimeOnly "io.netty:netty-handler-proxy:$nettyVersion" + runtimeOnly "io.netty.incubator:netty-incubator-codec-http3:$nettyHttp3Version" } description = "Examples for the Reactor Netty library" \ No newline at end of file diff --git a/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/http3/Application.java b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/http3/Application.java new file mode 100644 index 0000000000..faa6c20410 --- /dev/null +++ b/reactor-netty-examples/src/main/java/reactor/netty/examples/documentation/http/server/http3/Application.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2024 VMware, Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reactor.netty.examples.documentation.http.server.http3; + +import reactor.core.publisher.Mono; +import reactor.netty.DisposableServer; +import reactor.netty.http.Http3SslContextSpec; +import reactor.netty.http.HttpProtocol; +import reactor.netty.http.server.HttpServer; + +import java.io.File; +import java.time.Duration; + +public class Application { + + public static void main(String[] args) throws Exception { + File certChainFile = new File("certificate chain file"); + File keyFile = new File("private key file"); + + Http3SslContextSpec serverCtx = Http3SslContextSpec.forServer(keyFile, null, certChainFile); + + DisposableServer server = + HttpServer.create() + .port(8080) + .protocol(HttpProtocol.HTTP3) //<1> + .secure(spec -> spec.sslContext(serverCtx)) //<2> + .idleTimeout(Duration.ofSeconds(5)) + .http3Settings(spec -> spec.maxData(10000000) //<3> + .maxStreamDataBidirectionalLocal(1000000) + .maxStreamDataBidirectionalRemote(1000000) + .maxStreamsBidirectional(100)) + .handle((request, response) -> response.header("server", "reactor-netty") + .sendString(Mono.just("hello"))) + .bindNow(); + + server.onDispose() + .block(); + } +} diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/Http3SettingsSpec.java b/reactor-netty-http/src/main/java/reactor/netty/http/Http3SettingsSpec.java index 092c07cdd0..d0142098ab 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/Http3SettingsSpec.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/Http3SettingsSpec.java @@ -15,6 +15,11 @@ */ package reactor.netty.http; +import io.netty.incubator.codec.quic.QuicTokenHandler; +import reactor.util.annotation.Nullable; + +import java.util.Objects; + /** * A configuration builder to fine tune the HTTP/3 settings. * @@ -78,6 +83,16 @@ public interface Builder { * @return {@code this} */ Builder maxStreamsBidirectional(long maxStreamsBidirectional); + + /** + * Set the {@link QuicTokenHandler} that is used to generate and validate tokens or + * {@code null} if no tokens should be used at all. + * Default to {@code null}. + * + * @param tokenHandler the {@link QuicTokenHandler} to use. + * @return {@code this} + */ + Builder tokenHandler(QuicTokenHandler tokenHandler); } /** @@ -125,6 +140,16 @@ public long maxStreamsBidirectional() { return maxStreamsBidirectional; } + /** + * Return the configured {@link QuicTokenHandler} or null. + * + * @return the configured {@link QuicTokenHandler} or null + */ + @Nullable + public QuicTokenHandler tokenHandler() { + return tokenHandler; + } + @Override public boolean equals(Object o) { if (this == o) { @@ -137,7 +162,8 @@ public boolean equals(Object o) { return maxData == that.maxData && maxStreamDataBidirectionalLocal == that.maxStreamDataBidirectionalLocal && maxStreamDataBidirectionalRemote == that.maxStreamDataBidirectionalRemote && - maxStreamsBidirectional == that.maxStreamsBidirectional; + maxStreamsBidirectional == that.maxStreamsBidirectional && + Objects.equals(tokenHandler, that.tokenHandler); } @Override @@ -147,6 +173,7 @@ public int hashCode() { result = 31 * result + Long.hashCode(maxStreamDataBidirectionalLocal); result = 31 * result + Long.hashCode(maxStreamDataBidirectionalRemote); result = 31 * result + Long.hashCode(maxStreamsBidirectional); + result = 31 * result + Objects.hashCode(tokenHandler); return result; } @@ -154,12 +181,14 @@ public int hashCode() { final long maxStreamDataBidirectionalLocal; final long maxStreamDataBidirectionalRemote; final long maxStreamsBidirectional; + final QuicTokenHandler tokenHandler; Http3SettingsSpec(Build build) { this.maxData = build.maxData; this.maxStreamDataBidirectionalLocal = build.maxStreamDataBidirectionalLocal; this.maxStreamDataBidirectionalRemote = build.maxStreamDataBidirectionalRemote; this.maxStreamsBidirectional = build.maxStreamsBidirectional; + this.tokenHandler = build.tokenHandler; } static final class Build implements Builder { @@ -172,6 +201,7 @@ static final class Build implements Builder { long maxStreamDataBidirectionalLocal = DEFAULT_MAX_STREAM_DATA_BIDIRECTIONAL_LOCAL; long maxStreamDataBidirectionalRemote = DEFAULT_MAX_STREAM_DATA_BIDIRECTIONAL_REMOTE; long maxStreamsBidirectional = DEFAULT_MAX_STREAMS_BIDIRECTIONAL; + QuicTokenHandler tokenHandler; @Override public Http3SettingsSpec build() { @@ -213,5 +243,11 @@ public Builder maxStreamsBidirectional(long maxStreamsBidirectional) { this.maxStreamsBidirectional = maxStreamsBidirectional; return this; } + + @Override + public Builder tokenHandler(QuicTokenHandler tokenHandler) { + this.tokenHandler = tokenHandler; + return this; + } } } diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3ChannelInitializer.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3ChannelInitializer.java new file mode 100644 index 0000000000..73b3aeeaae --- /dev/null +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3ChannelInitializer.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2024 VMware, Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reactor.netty.http.server; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.incubator.codec.quic.QuicServerCodecBuilder; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.util.AttributeKey; +import reactor.netty.http.Http3SettingsSpec; + +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static io.netty.incubator.codec.http3.Http3.newQuicServerCodecBuilder; + +final class Http3ChannelInitializer extends ChannelInitializer { + + final Map, ?> attributes; + final Map, ?> childAttributes; + final Map, ?> childOptions; + final Duration idleTimeout; + final Http3SettingsSpec http3Settings; + final Map, ?> options; + final ChannelInitializer quicChannelInitializer; + final QuicSslContext quicSslContext; + + Http3ChannelInitializer(HttpServerConfig config, ChannelInitializer quicChannelInitializer) { + this.attributes = config.attributes(); + this.childAttributes = config.childAttributes(); + this.childOptions = config.childOptions(); + this.idleTimeout = config.idleTimeout(); + this.http3Settings = config.http3SettingsSpec(); + this.options = config.options(); + this.quicChannelInitializer = quicChannelInitializer; + if (config.sslProvider.getSslContext() instanceof QuicSslContext) { + this.quicSslContext = (QuicSslContext) config.sslProvider.getSslContext(); + } + else { + throw new IllegalArgumentException("The configured SslContext is not QuicSslContext"); + } + } + + @Override + protected void initChannel(Channel channel) { + QuicServerCodecBuilder quicServerCodecBuilder = + newQuicServerCodecBuilder().sslContext(quicSslContext) + .handler(quicChannelInitializer); + + if (http3Settings != null) { + quicServerCodecBuilder.initialMaxData(http3Settings.maxData()) + .initialMaxStreamDataBidirectionalLocal(http3Settings.maxStreamDataBidirectionalLocal()) + .initialMaxStreamDataBidirectionalRemote(http3Settings.maxStreamDataBidirectionalRemote()) + .initialMaxStreamsBidirectional(http3Settings.maxStreamsBidirectional()) + .tokenHandler(http3Settings.tokenHandler()); + } + + if (idleTimeout != null) { + quicServerCodecBuilder.maxIdleTimeout(idleTimeout.toMillis(), TimeUnit.MILLISECONDS); + } + + attributes(quicServerCodecBuilder, attributes); + channelOptions(quicServerCodecBuilder, options); + streamAttributes(quicServerCodecBuilder, childAttributes); + streamChannelOptions(quicServerCodecBuilder, childOptions); + + channel.pipeline().addLast(quicServerCodecBuilder.build()); + + channel.pipeline().remove(this); + } + + @SuppressWarnings("unchecked") + static void attributes(QuicServerCodecBuilder quicServerCodecBuilder, Map, ?> attrs) { + for (Map.Entry, ?> e : attrs.entrySet()) { + quicServerCodecBuilder.attr((AttributeKey) e.getKey(), e.getValue()); + } + } + + @SuppressWarnings({"unchecked", "ReferenceEquality"}) + static void channelOptions(QuicServerCodecBuilder quicServerCodecBuilder, Map, ?> options) { + for (Map.Entry, ?> e : options.entrySet()) { + // ReferenceEquality is deliberate + if (e.getKey() != ChannelOption.SO_REUSEADDR) { + quicServerCodecBuilder.option((ChannelOption) e.getKey(), e.getValue()); + } + } + } + + @SuppressWarnings("unchecked") + static void streamAttributes(QuicServerCodecBuilder quicServerCodecBuilder, Map, ?> attrs) { + for (Map.Entry, ?> e : attrs.entrySet()) { + quicServerCodecBuilder.streamAttr((AttributeKey) e.getKey(), e.getValue()); + } + } + + @SuppressWarnings({"unchecked", "ReferenceEquality"}) + static void streamChannelOptions(QuicServerCodecBuilder quicServerCodecBuilder, Map, ?> options) { + for (Map.Entry, ?> e : options.entrySet()) { + // ReferenceEquality is deliberate + if (e.getKey() != ChannelOption.TCP_NODELAY) { + quicServerCodecBuilder.streamOption((ChannelOption) e.getKey(), e.getValue()); + } + } + } +} \ No newline at end of file diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java new file mode 100644 index 0000000000..acca01730f --- /dev/null +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3Codec.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024 VMware, Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reactor.netty.http.server; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelInitializer; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; +import io.netty.handler.codec.http.cookie.ServerCookieEncoder; +import io.netty.incubator.codec.http3.Http3FrameToHttpObjectCodec; +import io.netty.incubator.codec.http3.Http3ServerConnectionHandler; +import io.netty.incubator.codec.quic.QuicStreamChannel; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; +import reactor.netty.ConnectionObserver; +import reactor.netty.NettyPipeline; +import reactor.netty.channel.ChannelOperations; +import reactor.netty.http.logging.HttpMessageLogFactory; +import reactor.util.annotation.Nullable; + +import java.time.Duration; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; + +final class Http3Codec extends ChannelInitializer { + + final BiPredicate compressPredicate; + final ServerCookieDecoder cookieDecoder; + final ServerCookieEncoder cookieEncoder; + final HttpServerFormDecoderProvider formDecoderProvider; + final BiFunction forwardedHeaderHandler; + final HttpMessageLogFactory httpMessageLogFactory; + final ConnectionObserver listener; + final BiFunction, ? super Connection, ? extends Mono> + mapHandle; + final ChannelOperations.OnSetup opsFactory; + final Duration readTimeout; + final Duration requestTimeout; + final boolean validate; + + Http3Codec( + @Nullable BiPredicate compressPredicate, + ServerCookieDecoder decoder, + ServerCookieEncoder encoder, + HttpServerFormDecoderProvider formDecoderProvider, + @Nullable BiFunction forwardedHeaderHandler, + HttpMessageLogFactory httpMessageLogFactory, + ConnectionObserver listener, + @Nullable BiFunction, ? super Connection, ? extends Mono> mapHandle, + ChannelOperations.OnSetup opsFactory, + @Nullable Duration readTimeout, + @Nullable Duration requestTimeout, + boolean validate) { + this.compressPredicate = compressPredicate; + this.cookieDecoder = decoder; + this.cookieEncoder = encoder; + this.formDecoderProvider = formDecoderProvider; + this.forwardedHeaderHandler = forwardedHeaderHandler; + this.httpMessageLogFactory = httpMessageLogFactory; + this.listener = listener; + this.mapHandle = mapHandle; + this.opsFactory = opsFactory; + this.readTimeout = readTimeout; + this.requestTimeout = requestTimeout; + this.validate = validate; + } + + @Override + protected void initChannel(QuicStreamChannel channel) { + channel.pipeline() + .addLast(new Http3FrameToHttpObjectCodec(true, validate)) + .addLast(NettyPipeline.HttpTrafficHandler, + new Http3StreamBridgeServerHandler(compressPredicate, cookieDecoder, cookieEncoder, formDecoderProvider, + forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, readTimeout, requestTimeout)); + + ChannelOperations.addReactiveBridge(channel, opsFactory, listener); + + channel.pipeline().remove(this); + } + + static ChannelHandler newHttp3ServerConnectionHandler( + @Nullable BiPredicate compressPredicate, + ServerCookieDecoder decoder, + ServerCookieEncoder encoder, + HttpServerFormDecoderProvider formDecoderProvider, + @Nullable BiFunction forwardedHeaderHandler, + HttpMessageLogFactory httpMessageLogFactory, + ConnectionObserver listener, + @Nullable BiFunction, ? super Connection, ? extends Mono> mapHandle, + ChannelOperations.OnSetup opsFactory, + @Nullable Duration readTimeout, + @Nullable Duration requestTimeout, + boolean validate) { + return new Http3ServerConnectionHandler( + new Http3Codec(compressPredicate, decoder, encoder, formDecoderProvider, forwardedHeaderHandler, + httpMessageLogFactory, listener, mapHandle, opsFactory, readTimeout, requestTimeout, validate)); + } +} \ No newline at end of file diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3StreamBridgeServerHandler.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3StreamBridgeServerHandler.java new file mode 100644 index 0000000000..0f96f00f3e --- /dev/null +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/Http3StreamBridgeServerHandler.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2024 VMware, Inc. or its affiliates, All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package reactor.netty.http.server; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.DecoderResult; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.HttpResponse; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.LastHttpContent; +import io.netty.handler.codec.http.cookie.ServerCookieDecoder; +import io.netty.handler.codec.http.cookie.ServerCookieEncoder; +import io.netty.incubator.codec.quic.QuicStreamChannel; +import io.netty.util.ReferenceCountUtil; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; +import reactor.netty.ConnectionObserver; +import reactor.netty.ReactorNetty; +import reactor.netty.http.logging.HttpMessageArgProviderFactory; +import reactor.netty.http.logging.HttpMessageLogFactory; +import reactor.util.annotation.Nullable; + +import java.net.SocketAddress; +import java.time.Duration; +import java.time.ZonedDateTime; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; + +import static reactor.netty.ReactorNetty.format; + +final class Http3StreamBridgeServerHandler extends ChannelDuplexHandler implements ChannelFutureListener { + final BiPredicate compress; + final ServerCookieDecoder cookieDecoder; + final ServerCookieEncoder cookieEncoder; + final HttpServerFormDecoderProvider formDecoderProvider; + final BiFunction forwardedHeaderHandler; + final HttpMessageLogFactory httpMessageLogFactory; + final ConnectionObserver listener; + final BiFunction, ? super Connection, ? extends Mono> + mapHandle; + final Duration readTimeout; + final Duration requestTimeout; + + SocketAddress remoteAddress; + + /** + * Flag to indicate if a request is not yet fully responded. + */ + boolean pendingResponse; + + Http3StreamBridgeServerHandler( + @Nullable BiPredicate compress, + ServerCookieDecoder decoder, + ServerCookieEncoder encoder, + HttpServerFormDecoderProvider formDecoderProvider, + @Nullable BiFunction forwardedHeaderHandler, + HttpMessageLogFactory httpMessageLogFactory, + ConnectionObserver listener, + @Nullable BiFunction, ? super Connection, ? extends Mono> mapHandle, + @Nullable Duration readTimeout, + @Nullable Duration requestTimeout) { + this.compress = compress; + this.cookieDecoder = decoder; + this.cookieEncoder = encoder; + this.formDecoderProvider = formDecoderProvider; + this.forwardedHeaderHandler = forwardedHeaderHandler; + this.httpMessageLogFactory = httpMessageLogFactory; + this.listener = listener; + this.mapHandle = mapHandle; + this.readTimeout = readTimeout; + this.requestTimeout = requestTimeout; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + if (HttpServerOperations.log.isDebugEnabled()) { + HttpServerOperations.log.debug(format(ctx.channel(), "New HTTP/3 stream")); + } + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + ctx.read(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (remoteAddress == null) { + remoteAddress = ctx.channel().parent().remoteAddress(); + } + if (msg instanceof HttpRequest) { + HttpRequest request = (HttpRequest) msg; + HttpServerOperations ops; + ZonedDateTime timestamp = ZonedDateTime.now(ReactorNetty.ZONE_ID_SYSTEM); + ConnectionInfo connectionInfo = null; + try { + pendingResponse = true; + connectionInfo = ConnectionInfo.from(ctx.channel(), + request, + true, + remoteAddress, + forwardedHeaderHandler); + ops = new HttpServerOperations(Connection.from(ctx.channel()), + listener, + request, + compress, + connectionInfo, + cookieDecoder, + cookieEncoder, + formDecoderProvider, + httpMessageLogFactory, + true, + mapHandle, + readTimeout, + requestTimeout, + true, + timestamp); + } + catch (RuntimeException e) { + pendingResponse = false; + request.setDecoderResult(DecoderResult.failure(e.getCause() != null ? e.getCause() : e)); + HttpServerOperations.sendDecodingFailures(ctx, listener, true, e, msg, httpMessageLogFactory, true, timestamp, connectionInfo, remoteAddress); + return; + } + ops.bind(); + listener.onStateChange(ops, ConnectionObserver.State.CONFIGURED); + } + else if (!pendingResponse) { + if (HttpServerOperations.log.isDebugEnabled()) { + HttpServerOperations.log.debug( + format(ctx.channel(), "Dropped HTTP content, since response has been sent already: {}"), + msg instanceof HttpObject ? + httpMessageLogFactory.debug(HttpMessageArgProviderFactory.create(msg)) : msg); + } + ReferenceCountUtil.release(msg); + ctx.read(); + return; + } + ctx.fireChannelRead(msg); + } + + @Override + @SuppressWarnings("FutureReturnValueIgnored") + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) { + if (msg instanceof ByteBuf) { + //"FutureReturnValueIgnored" this is deliberate + ctx.write(new DefaultHttpContent((ByteBuf) msg), promise); + } + else if (msg instanceof HttpResponse && HttpResponseStatus.CONTINUE.equals(((HttpResponse) msg).status())) { + //"FutureReturnValueIgnored" this is deliberate + ctx.write(msg, promise); + } + else { + //"FutureReturnValueIgnored" this is deliberate + ChannelFuture f = ctx.write(msg, promise); + if (msg instanceof LastHttpContent) { + pendingResponse = false; + f.addListener(this) + .addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + ctx.read(); + } + } + } + + @Override + public void operationComplete(ChannelFuture future) { + if (!future.isSuccess()) { + if (HttpServerOperations.log.isDebugEnabled()) { + HttpServerOperations.log.debug(format(future.channel(), + "Sending last HTTP packet was not successful, terminating the channel"), + future.cause()); + } + } + else { + if (HttpServerOperations.log.isDebugEnabled()) { + HttpServerOperations.log.debug(format(future.channel(), + "Last HTTP packet was sent, terminating the channel")); + } + } + + HttpServerOperations.cleanHandlerTerminate(future.channel()); + } +} diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java index 98df7a3174..696663b279 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServer.java @@ -466,6 +466,11 @@ public final HttpServer http2Settings(Consumer http2S */ public final HttpServer http3Settings(Consumer http3Settings) { Objects.requireNonNull(http3Settings, "http3Settings"); + if (!isHttp3Available()) { + throw new UnsupportedOperationException( + "To enable HTTP/3 support, you must add the dependency `io.netty.incubator:netty-incubator-codec-http3`" + + " to the class path first"); + } Http3SettingsSpec.Builder builder = Http3SettingsSpec.builder(); http3Settings.accept(builder); Http3SettingsSpec settings = builder.build(); diff --git a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java index bfc604acfc..ba92218deb 100644 --- a/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java +++ b/reactor-netty-http/src/main/java/reactor/netty/http/server/HttpServerConfig.java @@ -23,6 +23,9 @@ import io.netty.channel.ChannelOption; import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.channel.unix.ServerDomainSocketChannel; import io.netty.handler.codec.haproxy.HAProxyMessageDecoder; import io.netty.handler.codec.http.HttpDecoderConfig; import io.netty.handler.codec.http.HttpHeaderNames; @@ -89,6 +92,7 @@ import static reactor.netty.ReactorNetty.ACCESS_LOG_ENABLED; import static reactor.netty.ReactorNetty.format; +import static reactor.netty.http.server.Http3Codec.newHttp3ServerConnectionHandler; import static reactor.netty.http.server.HttpServerFormDecoderProvider.DEFAULT_FORM_DECODER_SPEC; /** @@ -370,6 +374,19 @@ public Function uriTagValue() { this.uriTagValue = parent.uriTagValue; } + @Override + public ChannelInitializer channelInitializer(ConnectionObserver connectionObserver, + @Nullable SocketAddress remoteAddress, boolean onServer) { + ChannelInitializer channelInitializer = super.channelInitializer(connectionObserver, remoteAddress, onServer); + return (_protocols & h3) == h3 ? new Http3ChannelInitializer(this, channelInitializer) : channelInitializer; + } + + @Override + protected Class channelType(boolean isDomainSocket) { + return isDomainSocket ? ServerDomainSocketChannel.class : + (_protocols & h3) == h3 ? DatagramChannel.class : ServerSocketChannel.class; + } + @Override protected LoggingHandler defaultLoggingHandler() { return LOGGING_HANDLER; @@ -569,6 +586,27 @@ static BiPredicate compressPredicate( return lengthPredicate; } + static void configureHttp3Pipeline( + ChannelPipeline p, + @Nullable BiPredicate compressPredicate, + ServerCookieDecoder cookieDecoder, + ServerCookieEncoder cookieEncoder, + HttpServerFormDecoderProvider formDecoderProvider, + @Nullable BiFunction forwardedHeaderHandler, + HttpMessageLogFactory httpMessageLogFactory, + ConnectionObserver listener, + @Nullable BiFunction, ? super Connection, ? extends Mono> mapHandle, + ChannelOperations.OnSetup opsFactory, + @Nullable Duration readTimeout, + @Nullable Duration requestTimeout, + boolean validate) { + p.remove(NettyPipeline.ReactiveBridge); + + p.addLast(NettyPipeline.HttpCodec, newHttp3ServerConnectionHandler(compressPredicate, cookieDecoder, cookieEncoder, + formDecoderProvider, forwardedHeaderHandler, httpMessageLogFactory, listener, mapHandle, opsFactory, + readTimeout, requestTimeout, validate)); + } + static void configureH2Pipeline(ChannelPipeline p, boolean accessLogEnabled, @Nullable Function accessLog, @@ -1285,14 +1323,16 @@ public void onChannelInit(ConnectionObserver observer, Channel channel, @Nullabl if (sslProvider != null) { ChannelPipeline pipeline = channel.pipeline(); - if (redirectHttpToHttps && (protocols & h2) != h2) { - NonSslRedirectDetector nonSslRedirectDetector = new NonSslRedirectDetector(sslProvider, - remoteAddress, - SSL_DEBUG); - pipeline.addFirst(NettyPipeline.NonSslRedirectDetector, nonSslRedirectDetector); - } - else { - sslProvider.addSslHandler(channel, remoteAddress, SSL_DEBUG); + if ((protocols & h3) != h3) { + if (redirectHttpToHttps && (protocols & h2) != h2) { + NonSslRedirectDetector nonSslRedirectDetector = new NonSslRedirectDetector(sslProvider, + remoteAddress, + SSL_DEBUG); + pipeline.addFirst(NettyPipeline.NonSslRedirectDetector, nonSslRedirectDetector); + } + else { + sslProvider.addSslHandler(channel, remoteAddress, SSL_DEBUG); + } } if ((protocols & h11orH2) == h11orH2) { @@ -1350,6 +1390,22 @@ else if ((protocols & h2) == h2) { uriTagValue, decoder.validateHeaders()); } + else if ((protocols & h3) == h3) { + configureHttp3Pipeline( + channel.pipeline(), + compressPredicate(compressPredicate, minCompressionSize), + cookieDecoder, + cookieEncoder, + formDecoderProvider, + forwardedHeaderHandler, + httpMessageLogFactory, + observer, + mapHandle, + opsFactory, + readTimeout, + requestTimeout, + decoder.validateHeaders()); + } } else { if ((protocols & h11orH2C) == h11orH2C) { diff --git a/reactor-netty-http/src/main/resources/META-INF/native-image/io.projectreactor.netty/reactor-netty-http/reflect-config.json b/reactor-netty-http/src/main/resources/META-INF/native-image/io.projectreactor.netty/reactor-netty-http/reflect-config.json index 0b9dfc7db9..1d2e35dd4b 100644 --- a/reactor-netty-http/src/main/resources/META-INF/native-image/io.projectreactor.netty/reactor-netty-http/reflect-config.json +++ b/reactor-netty-http/src/main/resources/META-INF/native-image/io.projectreactor.netty/reactor-netty-http/reflect-config.json @@ -146,6 +146,27 @@ "name": "reactor.netty.http.server.HttpServerConfig$Http11OrH2CleartextCodec", "queryAllPublicMethods": true }, + { + "condition": { + "typeReachable": "reactor.netty.http.server.Http3ChannelInitializer" + }, + "name": "reactor.netty.http.server.Http3ChannelInitializer", + "queryAllPublicMethods": true + }, + { + "condition": { + "typeReachable": "reactor.netty.http.server.Http3Codec" + }, + "name": "reactor.netty.http.server.Http3Codec", + "queryAllPublicMethods": true + }, + { + "condition": { + "typeReachable": "reactor.netty.http.server.Http3StreamBridgeServerHandler" + }, + "name": "reactor.netty.http.server.Http3StreamBridgeServerHandler", + "queryAllPublicMethods": true + }, { "condition": { "typeReachable": "reactor.netty.http.server.HttpServerConfig$ReactorNettyHttpServerUpgradeHandler"