Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial HTTP/3 server support #3176

Merged
merged 1 commit into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/http-client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down
30 changes: 29 additions & 1 deletion docs/modules/ROOT/pages/http-server.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -110,9 +111,13 @@ public Mono<? extends DisposableServer> 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<Channel> 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);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<Channel> channelInitializer(ConnectionObserver connectionObserver,
public ChannelInitializer<Channel> channelInitializer(ConnectionObserver connectionObserver,
@Nullable SocketAddress remoteAddress, boolean onServer) {
requireNonNull(connectionObserver, "connectionObserver");
return new TransportChannelInitializer(this, connectionObserver, remoteAddress, onServer);
Expand Down
1 change: 1 addition & 0 deletions reactor-netty-examples/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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) {
Expand All @@ -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
Expand All @@ -147,19 +173,22 @@ 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;
}

final long maxData;
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 {
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Channel> {

final Map<AttributeKey<?>, ?> attributes;
final Map<AttributeKey<?>, ?> childAttributes;
final Map<ChannelOption<?>, ?> childOptions;
final Duration idleTimeout;
final Http3SettingsSpec http3Settings;
final Map<ChannelOption<?>, ?> options;
final ChannelInitializer<Channel> quicChannelInitializer;
final QuicSslContext quicSslContext;

Http3ChannelInitializer(HttpServerConfig config, ChannelInitializer<Channel> 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<AttributeKey<?>, ?> attrs) {
for (Map.Entry<AttributeKey<?>, ?> e : attrs.entrySet()) {
quicServerCodecBuilder.attr((AttributeKey<Object>) e.getKey(), e.getValue());
}
}

@SuppressWarnings({"unchecked", "ReferenceEquality"})
static void channelOptions(QuicServerCodecBuilder quicServerCodecBuilder, Map<ChannelOption<?>, ?> options) {
for (Map.Entry<ChannelOption<?>, ?> e : options.entrySet()) {
// ReferenceEquality is deliberate
if (e.getKey() != ChannelOption.SO_REUSEADDR) {
quicServerCodecBuilder.option((ChannelOption<Object>) e.getKey(), e.getValue());
}
}
}

@SuppressWarnings("unchecked")
static void streamAttributes(QuicServerCodecBuilder quicServerCodecBuilder, Map<AttributeKey<?>, ?> attrs) {
for (Map.Entry<AttributeKey<?>, ?> e : attrs.entrySet()) {
quicServerCodecBuilder.streamAttr((AttributeKey<Object>) e.getKey(), e.getValue());
}
}

@SuppressWarnings({"unchecked", "ReferenceEquality"})
static void streamChannelOptions(QuicServerCodecBuilder quicServerCodecBuilder, Map<ChannelOption<?>, ?> options) {
for (Map.Entry<ChannelOption<?>, ?> e : options.entrySet()) {
// ReferenceEquality is deliberate
if (e.getKey() != ChannelOption.TCP_NODELAY) {
quicServerCodecBuilder.streamOption((ChannelOption<Object>) e.getKey(), e.getValue());
}
}
}
}
Loading
Loading