diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnectionHandler.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnectionHandler.java index 539b1a378f6..abb5a8f25dd 100644 --- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnectionHandler.java +++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnectionHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -208,7 +208,7 @@ private String settingsForUpgrade(Http2ClientProtocolConfig protocolConfig) { .data(); byte[] b = new byte[settingsFrameData.available()]; settingsFrameData.read(b); - return Base64.getEncoder().encodeToString(b); + return Base64.getUrlEncoder().encodeToString(b); } private Http2ConnectionAttemptResult http1(Http2ClientImpl http2Client, diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java index 3713d68f4e6..fa70fa15fb9 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -321,6 +321,11 @@ Http2Settings serverSettings() { return serverSettings; } + // jUnit Http2Settings pkg only visible test accessor. + Http2Settings clientSettings() { + return clientSettings; + } + private void doHandle(Semaphore requestSemaphore) throws InterruptedException { myThread = Thread.currentThread(); while (canRun && state != State.FINISHED) { diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Upgrader.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Upgrader.java index 8ec43b0bc5d..b2e4ca99421 100644 --- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Upgrader.java +++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Upgrader.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022, 2023 Oracle and/or its affiliates. + * Copyright (c) 2022, 2024 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. @@ -43,7 +43,6 @@ public class Http2Upgrader implements Http1Upgrader { + "Upgrade: h2c\r\n\r\n") .getBytes(StandardCharsets.UTF_8); private static final HeaderName HTTP2_SETTINGS_HEADER_NAME = HeaderNames.create("HTTP2-Settings"); - private static final Base64.Decoder BASE_64_DECODER = Base64.getDecoder(); private final Http2Config config; private final List subProtocolProviders; @@ -77,8 +76,7 @@ public ServerConnection upgrade(ConnectionContext ctx, WritableHeaders headers) { Http2Connection connection = new Http2Connection(ctx, config, subProtocolProviders); if (headers.contains(HTTP2_SETTINGS_HEADER_NAME)) { - connection.clientSettings(Http2Settings.create(BufferData.create(BASE_64_DECODER.decode(headers.get( - HTTP2_SETTINGS_HEADER_NAME).value().getBytes(StandardCharsets.US_ASCII))))); + connection.clientSettings(token68ToHttp2Settings(headers.get(HTTP2_SETTINGS_HEADER_NAME).valueBytes())); } else { throw new RuntimeException("Bad request -> not " + HTTP2_SETTINGS_HEADER_NAME + " header"); } @@ -86,7 +84,7 @@ public ServerConnection upgrade(ConnectionContext ctx, http2Headers.path(prologue.uriPath().rawPath()); http2Headers.method(prologue.method()); headers.remove(HeaderNames.HOST, - it -> http2Headers.authority(it.value())); + it -> http2Headers.authority(it.get())); http2Headers.scheme("http"); // TODO need to get if https (ctx)? HttpPrologue newPrologue = HttpPrologue.create(Http2Connection.FULL_PROTOCOL, @@ -104,4 +102,18 @@ public ServerConnection upgrade(ConnectionContext ctx, return connection; } + /** + * RFC7540 3.2.1 + *
{@code
+     * HTTP2-Settings    = token68
+     * token68           = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
+     * }
+ * + * @param bytes Base64URL encoded bytes + * @return HTTP/2 settings + */ + private static Http2Settings token68ToHttp2Settings(byte[] bytes) { + return Http2Settings.create(BufferData.create(Base64.getUrlDecoder().decode(bytes))); + } + } diff --git a/webserver/http2/src/test/java/io/helidon/webserver/http2/UpgradeSettingsTest.java b/webserver/http2/src/test/java/io/helidon/webserver/http2/UpgradeSettingsTest.java new file mode 100644 index 00000000000..409350b646e --- /dev/null +++ b/webserver/http2/src/test/java/io/helidon/webserver/http2/UpgradeSettingsTest.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2024 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; + +import java.util.Base64; + +import io.helidon.common.buffers.BufferData; +import io.helidon.common.buffers.DataWriter; +import io.helidon.http.HeaderValues; +import io.helidon.http.HttpPrologue; +import io.helidon.http.Method; +import io.helidon.http.WritableHeaders; +import io.helidon.http.http2.Http2Flag; +import io.helidon.http.http2.Http2Settings; +import io.helidon.webserver.ConnectionContext; +import io.helidon.webserver.ListenerContext; +import io.helidon.webserver.Router; + +import org.junit.jupiter.api.Test; + +import static io.helidon.http.http2.Http2Setting.ENABLE_PUSH; +import static io.helidon.http.http2.Http2Setting.HEADER_TABLE_SIZE; +import static io.helidon.http.http2.Http2Setting.INITIAL_WINDOW_SIZE; +import static io.helidon.http.http2.Http2Setting.MAX_CONCURRENT_STREAMS; +import static io.helidon.http.http2.Http2Setting.MAX_FRAME_SIZE; +import static io.helidon.http.http2.Http2Setting.MAX_HEADER_LIST_SIZE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class UpgradeSettingsTest { + + static final long MAX_UNSIGNED_INT = 0xFFFFFFFFL; + + private final ConnectionContext ctx; + private final HttpPrologue prologue; + + public UpgradeSettingsTest() { + ctx = mock(ConnectionContext.class); + prologue = HttpPrologue.create("http/1.1", + "http", + "1.1", + Method.GET, + "/resource.txt", + false); + DataWriter dataWriter = mock(DataWriter.class); + when(ctx.router()).thenReturn(Router.empty()); + when(ctx.listenerContext()).thenReturn(mock(ListenerContext.class)); + when(ctx.dataWriter()).thenReturn(dataWriter); + } + + @Test + void urlEncodedSettingsGH8399() { + Http2Settings s = upgrade("AAEAABAAAAIAAAABAAN_____AAQAAP__AAUAAEAAAAYAACAA"); + assertThat(s.presentValue(HEADER_TABLE_SIZE).orElseThrow(), is(4096L)); + assertThat(s.presentValue(ENABLE_PUSH).orElseThrow(), is(true)); + assertThat(s.presentValue(MAX_CONCURRENT_STREAMS).orElseThrow(), is(MAX_UNSIGNED_INT / 2)); + assertThat(s.presentValue(INITIAL_WINDOW_SIZE).orElseThrow(), is(65_535L)); + assertThat(s.presentValue(MAX_FRAME_SIZE).orElseThrow(), is(16_384L)); + assertThat(s.presentValue(MAX_HEADER_LIST_SIZE).orElseThrow(), is(8192L)); + } + + @Test + void urlEncodedSettings() { + Http2Settings settings2 = Http2Settings.builder() + .add(HEADER_TABLE_SIZE, 4096L) + .add(ENABLE_PUSH, false) + .add(MAX_CONCURRENT_STREAMS, MAX_UNSIGNED_INT - 5) + .add(INITIAL_WINDOW_SIZE, 65535L) + .add(MAX_FRAME_SIZE, 16384L) + .add(MAX_HEADER_LIST_SIZE, 256L) + .build(); + String encSett = Base64.getUrlEncoder().encodeToString(settingsToBytes(settings2)); + Http2Settings s = upgrade(encSett); + assertThat(s.presentValue(MAX_CONCURRENT_STREAMS).orElseThrow(), is(MAX_UNSIGNED_INT - 5)); + assertThat(s.presentValue(MAX_HEADER_LIST_SIZE).orElseThrow(), is(256L)); + } + + Http2Settings upgrade(String http2Settings) { + WritableHeaders headers = WritableHeaders.create().add(HeaderValues.create("HTTP2-Settings", http2Settings)); + Http2Upgrader http2Upgrader = Http2Upgrader.create(Http2Config.create()); + Http2Connection connection = (Http2Connection) http2Upgrader.upgrade(ctx, prologue, headers); + return connection.clientSettings(); + } + + byte[] settingsToBytes(Http2Settings settings) { + BufferData settingsFrameData = + settings.toFrameData(null, 0, Http2Flag.SettingsFlags.create(0)).data(); + byte[] b = new byte[settingsFrameData.available()]; + settingsFrameData.read(b); + return b; + } +}