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

6543 HTTP/2 header continuation #6907

Merged
merged 4 commits into from
Jun 8, 2023
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
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,62 @@ public int writeHeaders(Http2Headers headers, int streamId, Http2Flag.HeaderFlag
// we must enforce parallelism of exactly 1, to make sure the dynamic table is updated
// and then immediately written

int maxFrameSize = flowControl.maxFrameSize();

return withStreamLock(() -> {
int written = 0;
headerBuffer.clear();
headers.write(outboundDynamicTable, responseHuffman, headerBuffer);
Http2FrameHeader frameHeader = Http2FrameHeader.create(headerBuffer.available(),
Http2FrameTypes.HEADERS,
flags,
streamId);

// Fast path when headers fits within the SETTINGS_MAX_FRAME_SIZE
if (headerBuffer.available() <= maxFrameSize) {
Http2FrameHeader frameHeader = Http2FrameHeader.create(headerBuffer.available(),
Http2FrameTypes.HEADERS,
flags,
streamId);
written += frameHeader.length();
written += Http2FrameHeader.LENGTH;

noLockWrite(new Http2FrameData(frameHeader, headerBuffer));
return written;
}

// Split header frame to smaller continuation frames RFC 9113 §6.10
BufferData[] fragments = Http2Headers.split(headerBuffer, maxFrameSize);

// First header fragment
BufferData fragment = fragments[0];
Http2FrameHeader frameHeader;
frameHeader = Http2FrameHeader.create(fragment.available(),
Http2FrameTypes.HEADERS,
Http2Flag.HeaderFlags.create(0),
streamId);
written += frameHeader.length();
written += Http2FrameHeader.LENGTH;
noLockWrite(new Http2FrameData(frameHeader, fragment));

// Header continuation fragments in the middle
for (int i = 1; i < fragments.length; i++) {
fragment = fragments[i];
frameHeader = Http2FrameHeader.create(fragment.available(),
Http2FrameTypes.CONTINUATION,
Http2Flag.ContinuationFlags.create(0),
streamId);
written += frameHeader.length();
written += Http2FrameHeader.LENGTH;
noLockWrite(new Http2FrameData(frameHeader, fragment));
}

noLockWrite(new Http2FrameData(frameHeader, headerBuffer));

// Last header continuation fragment
fragment = fragments[fragments.length - 1];
frameHeader = Http2FrameHeader.create(fragment.available(),
Http2FrameTypes.CONTINUATION,
// Last fragment needs to indicate the end of headers
Http2Flag.ContinuationFlags.create(flags.value() | Http2Flag.END_OF_HEADERS),
streamId);
written += frameHeader.length();
written += Http2FrameHeader.LENGTH;
noLockWrite(new Http2FrameData(frameHeader, fragment));
return written;
});
}
Expand All @@ -104,17 +147,8 @@ public int writeHeaders(Http2Headers headers,
return withStreamLock(() -> {
int bytesWritten = 0;

headerBuffer.clear();
headers.write(outboundDynamicTable, responseHuffman, headerBuffer);
bytesWritten += headerBuffer.available();

Http2FrameHeader frameHeader = Http2FrameHeader.create(headerBuffer.available(),
Http2FrameTypes.HEADERS,
flags,
streamId);
bytesWritten += Http2FrameHeader.LENGTH;
bytesWritten += writeHeaders(headers, streamId, flags, flowControl);

noLockWrite(new Http2FrameData(frameHeader, headerBuffer));
writeData(dataFrame, flowControl);
bytesWritten += Http2FrameHeader.LENGTH;
bytesWritten += dataFrame.header().length();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2022 Oracle and/or its affiliates.
* Copyright (c) 2022, 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 Down Expand Up @@ -96,7 +96,12 @@ public enum Http2ErrorCode {
* The endpoint requires that HTTP/1.1 be used
* instead of HTTP/2.
*/
HTTP_1_1_REQUIRED(0xd);
HTTP_1_1_REQUIRED(0xd),
/**
* Request header fields are too large.
* RFC6585
*/
REQUEST_HEADER_FIELDS_TOO_LARGE(431);

private static final Map<Integer, Http2ErrorCode> BY_CODE;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,27 @@ public void write(DynamicTable table, Http2HuffmanEncoder huffman, BufferData gr
}
}

static BufferData[] split(BufferData bufferData, int size) {
int length = bufferData.available();
if (length <= size) {
return new BufferData[]{bufferData};
}

int lastFragmentSize = length % size;
// Avoid creating 0 length last fragment
int allFrames = (length / size) + (lastFragmentSize != 0 ? 1 : 0);
BufferData[] result = new BufferData[allFrames];

for (int i = 0; i < allFrames; i++) {
boolean lastFrame = (allFrames == i + 1);
byte[] frag = new byte[lastFrame ? (lastFragmentSize != 0 ? lastFragmentSize : size) : size];
bufferData.read(frag);
result[i] = BufferData.create(frag);
}

return result;
}

private static Http2Headers create(ServerRequestHeaders httpHeaders, PseudoHeaders pseudoHeaders) {
return new Http2Headers(httpHeaders, pseudoHeaders);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.helidon.logging.common.LogConfig;

import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;

Expand Down Expand Up @@ -65,6 +66,15 @@ private static Stream<SplitTest> splitMultiple() {
);
}

@Test
void splitHeaders() {
BufferData bf = BufferData.create("This is so long text!");
BufferData[] split = Http2Headers.split(bf, 12);
assertThat(split.length, is(2));
assertThat(split[0].available(), is(12));
assertThat(split[1].available(), is(9));
}

@ParameterizedTest
@MethodSource
void splitMultiple(SplitTest args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ public interface Http2Config {
/**
* The maximum field section size that the sender is prepared to accept in bytes.
* See RFC 9113 section 6.5.2 for details.
* Default is maximal unsigned int.
* Default is 8192.
*
* @return maximal header list size in bytes
*/
@ConfiguredOption("0xFFFFFFFFL")
@ConfiguredOption("8192")
long maxHeaderListSize();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,11 +400,10 @@ private void readFrame() {

private void doContinuation() {
Http2Flag.ContinuationFlags flags = frameHeader.flags(Http2FrameTypes.CONTINUATION);
List<Http2FrameData> continuationData = stream(frameHeader.streamId()).contData();
if (continuationData.isEmpty()) {
throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Received continuation without headers.");
}
continuationData.add(new Http2FrameData(frameHeader, inProgressFrame()));

stream(frameHeader.streamId())
.addContinuation(new Http2FrameData(frameHeader, inProgressFrame()));

if (flags.endOfHeaders()) {
state = State.HEADERS;
} else {
Expand Down Expand Up @@ -544,9 +543,7 @@ private void doHeaders() {
// first frame, expecting continuation
if (frameHeader.type() == Http2FrameType.HEADERS && !frameHeader.flags(Http2FrameTypes.HEADERS).endOfHeaders()) {
// this needs to retain the data until we receive last continuation, cannot use the same data
streamContext.contData().clear();
streamContext.contData().add(new Http2FrameData(frameHeader, inProgressFrame().copy()));
streamContext.continuationHeader = frameHeader;
streamContext.addHeadersToBeContinued(frameHeader, inProgressFrame().copy());
this.continuationExpectedStreamId = streamId;
this.state = State.READ_FRAME;
return;
Expand All @@ -559,14 +556,12 @@ private void doHeaders() {

if (frameHeader.type() == Http2FrameType.CONTINUATION) {
// end of continuations with header frames
List<Http2FrameData> frames = streamContext.contData();
headers = Http2Headers.create(stream,
requestDynamicTable,
requestHuffman,
frames.toArray(new Http2FrameData[0]));
endOfStream = streamContext.continuationHeader.flags(Http2FrameTypes.HEADERS).endOfStream();
frames.clear();
streamContext.continuationHeader = null;
streamContext.contData());
endOfStream = streamContext.contHeader().flags(Http2FrameTypes.HEADERS).endOfStream();
streamContext.clearContinuations();
continuationExpectedStreamId = 0;
} else {
endOfStream = frameHeader.flags(Http2FrameTypes.HEADERS).endOfStream();
Expand Down Expand Up @@ -716,6 +711,7 @@ private StreamContext stream(int streamId) {
}

streamContext = new StreamContext(streamId,
http2Config.maxHeaderListSize(),
new Http2Stream(ctx,
routing,
http2Config,
Expand Down Expand Up @@ -788,26 +784,58 @@ public void run() {

private static class StreamContext {
private final List<Http2FrameData> continuationData = new ArrayList<>();
private final long maxHeaderListSize;
private final int streamId;
private final Http2Stream stream;
private long headerListSize = 0;

private Http2FrameHeader continuationHeader;

StreamContext(int streamId, Http2Stream stream) {
StreamContext(int streamId, long maxHeaderListSize, Http2Stream stream) {
this.streamId = streamId;
this.maxHeaderListSize = maxHeaderListSize;
this.stream = stream;
}

public Http2Stream stream() {
return stream;
}

public Http2FrameHeader contHeader() {
Http2FrameData[] contData() {
return continuationData.toArray(new Http2FrameData[0]);
}

Http2FrameHeader contHeader() {
return continuationHeader;
}

public List<Http2FrameData> contData() {
return continuationData;
void addContinuation(Http2FrameData frameData) {
if (continuationData.isEmpty()) {
throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Received continuation without headers.");
}
this.continuationData.add(frameData);
addAndValidateHeaderListSize(frameData.header().length());
}

void addHeadersToBeContinued(Http2FrameHeader frameHeader, BufferData bufferData) {
clearContinuations();
continuationHeader = frameHeader;
this.continuationData.add(new Http2FrameData(frameHeader, bufferData));
addAndValidateHeaderListSize(frameHeader.length());
}

private void addAndValidateHeaderListSize(int headerSizeIncrement){
// Check MAX_HEADER_LIST_SIZE
headerListSize += headerSizeIncrement;
if (headerListSize > maxHeaderListSize){
throw new Http2Exception(Http2ErrorCode.REQUEST_HEADER_FIELDS_TOO_LARGE,
"Request Header Fields Too Large");
}
}

private void clearContinuations() {
continuationData.clear();
headerListSize = 0;
}
}
}
Loading