Skip to content

Commit

Permalink
Support for non-GET HTTP/2 upgrades (helidon-io#6383)
Browse files Browse the repository at this point in the history
* Support for non-GET HTTP/2 upgrades. Improved mapping of HTTP/1 request to HTTP/2 to support non-GET upgrades with payloads. Issue helidon-io#6353.

* Set authority header as before.

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>

* Additional testing for HTTP route versions.

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>

---------

Signed-off-by: Santiago Pericasgeertsen <santiago.pericasgeertsen@oracle.com>
  • Loading branch information
spericas authored Mar 9, 2023
1 parent 829c0c0 commit b368a84
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2018, 2022 Oracle and/or its affiliates.
* Copyright (c) 2018, 2023 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -16,11 +16,12 @@

package io.helidon.webserver.http2;

import java.util.Map;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpScheme;
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http2.AbstractHttp2ConnectionHandlerBuilder;
Expand Down Expand Up @@ -61,20 +62,29 @@ public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof HttpServerUpgradeHandler.UpgradeEvent upgradeEvent) {

// Map initial request headers to HTTP2
FullHttpRequest request = upgradeEvent.upgradeRequest();

// Create HTTP/2 headers from HTTP upgrade request
Http2Headers headers = new DefaultHttp2Headers()
.method(HttpMethod.GET.asciiName())
.method(request.method().asciiName())
.path(request.uri())
.scheme(HttpScheme.HTTP.name());
CharSequence host = request.headers().get(HttpHeaderNames.HOST);
if (host != null) {
headers.authority(host);
}
for (Map.Entry<String, String> e : request.headers()) {
headers.add(e.getKey().toLowerCase(), e.getValue());
}

// Process mapped headers
onHeadersRead(ctx, 1, headers, 0, true);
// Support non-GET upgrade requests possibly with non-empty payloads
ByteBuf payload = request.content();
if (payload.readableBytes() > 0) {
onHeadersRead(ctx, 1, headers, 0, false);
onDataRead(ctx, 1, payload, 0, true);
} else {
onHeadersRead(ctx, 1, headers, 0, true);
}
}
super.userEventTriggered(ctx, evt);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright (c) 2023 Oracle and/or its affiliates.
*
* 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
*
* http://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 io.helidon.webserver.http2.test;

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse;
import java.time.Duration;

import io.helidon.common.LogConfig;
import io.helidon.common.http.Http;
import io.helidon.webserver.Http1Route;
import io.helidon.webserver.WebServer;
import io.helidon.webserver.http2.Http2Route;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import static java.net.http.HttpClient.Version;
import static java.net.http.HttpClient.Version.HTTP_2;
import static java.net.http.HttpClient.Version.HTTP_1_1;
import static java.net.http.HttpClient.newHttpClient;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

class H2UpgradeTest {

private static WebServer webServer;
private static HttpClient httpClient;

@BeforeAll
public static void startServer() {
LogConfig.configureRuntime();
webServer = WebServer.builder()
.defaultSocket(s -> s
.bindAddress("localhost")
.port(0)
)
.routing(r -> r
.get("/", (req, res) ->
res.send("GET " + req.version()))
.post("/", (req, res) ->
req.content().as(String.class).thenAccept(s ->
res.send("POST " + req.version() + " " + s)))
.put("/", (req, res) ->
req.content().as(String.class).thenAccept(s ->
res.send("PUT " + req.version() + " " + s)))
.route(Http1Route.route(Http.Method.PUT, "/version", (req, res) ->
req.content().as(String.class).thenAccept(s ->
res.send("HTTP1 PUT " + req.version() + " " + s))))
.route(Http2Route.route(Http.Method.PUT, "/version", (req, res) ->
req.content().as(String.class).thenAccept(s ->
res.send("HTTP2 PUT " + req.version() + " " + s))))
)
.build()
.start()
.await(Duration.ofSeconds(10));

httpClient = newHttpClient();
}

@AfterAll
static void afterAll() {
webServer.shutdown().await(Duration.ofSeconds(10));
}

@Test
void testGetUpgrade() throws IOException, InterruptedException {
HttpResponse<String> r = httpClient(HTTP_2, "GET", "/",
BodyPublishers.noBody());
assertThat(r.body(), is("GET V2_0"));
}

@Test
void testPostUpgrade() throws IOException, InterruptedException {
HttpResponse<String> r = httpClient(HTTP_2, "POST", "/",
BodyPublishers.ofString("Hello World"));
assertThat(r.body(), is("POST V2_0 Hello World"));
}

@Test
void testPutUpgrade() throws IOException, InterruptedException {
HttpResponse<String> r = httpClient(HTTP_2, "PUT", "/",
BodyPublishers.ofString("Hello World"));
assertThat(r.body(), is("PUT V2_0 Hello World"));
}

@Test
void testPutUpgradeAndRoute() throws IOException, InterruptedException {
HttpResponse<String> r = httpClient(HTTP_2, "PUT", "/version",
BodyPublishers.ofString("Hello World"));
assertThat(r.body(), is("HTTP2 PUT V2_0 Hello World"));
}

@Test
void testPutNoUpgradeAndRoute() throws IOException, InterruptedException {
HttpResponse<String> r = httpClient(HTTP_1_1, "PUT", "/version",
BodyPublishers.ofString("Hello World"));
assertThat(r.body(), is("HTTP1 PUT V1_1 Hello World"));
}

private HttpResponse<String> httpClient(Version version, String method, String path,
HttpRequest.BodyPublisher publisher) throws IOException, InterruptedException {
return httpClient.send(HttpRequest.newBuilder()
.version(version) // always upgrade, no prior knowledge support
.uri(URI.create("http://localhost:" + webServer.port() + path))
.method(method, publisher)
.build(), HttpResponse.BodyHandlers.ofString());
}
}

0 comments on commit b368a84

Please sign in to comment.