Skip to content

Commit

Permalink
HTTP2-Settings needs to be encoded/decoded to Base64 with url dialect #…
Browse files Browse the repository at this point in the history
…8399

Signed-off-by: Daniel Kec <daniel.kec@oracle.com>
  • Loading branch information
danielkec committed Jun 5, 2024
1 parent 74d06e7 commit e1b04d6
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 8 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<Http2SubProtocolSelector> subProtocolProviders;
Expand Down Expand Up @@ -77,16 +76,15 @@ 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");
}
Http2Headers http2Headers = Http2Headers.create(headers);
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,
Expand All @@ -104,4 +102,18 @@ public ServerConnection upgrade(ConnectionContext ctx,
return connection;
}

/**
* <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-3.2.1">RFC7540 3.2.1</a>
* <pre>{@code
* HTTP2-Settings = token68
* token68 = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
* }</pre>
*
* @param bytes Base64URL encoded bytes
* @return HTTP/2 settings
*/
private static Http2Settings token68ToHttp2Settings(byte[] bytes) {
return Http2Settings.create(BufferData.create(Base64.getUrlDecoder().decode(bytes)));
}

}
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit e1b04d6

Please sign in to comment.