Skip to content

Commit 49ae683

Browse files
committed
H2 transport to enforce a max limit on number of CONTINUATION frames
1 parent 38d26fc commit 49ae683

File tree

3 files changed

+149
-8
lines changed

3 files changed

+149
-8
lines changed

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Config.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ public class H2Config {
5050
private final int maxFrameSize;
5151
private final int maxHeaderListSize;
5252
private final boolean compressionEnabled;
53+
private final int maxContinuations;
5354

5455
H2Config(final int headerTableSize, final boolean pushEnabled, final int maxConcurrentStreams,
5556
final int initialWindowSize, final int maxFrameSize, final int maxHeaderListSize,
56-
final boolean compressionEnabled) {
57+
final boolean compressionEnabled, final int maxContinuations) {
5758
super();
5859
this.headerTableSize = headerTableSize;
5960
this.pushEnabled = pushEnabled;
@@ -62,6 +63,7 @@ public class H2Config {
6263
this.maxFrameSize = maxFrameSize;
6364
this.maxHeaderListSize = maxHeaderListSize;
6465
this.compressionEnabled = compressionEnabled;
66+
this.maxContinuations = maxContinuations;
6567
}
6668

6769
public int getHeaderTableSize() {
@@ -92,6 +94,10 @@ public boolean isCompressionEnabled() {
9294
return compressionEnabled;
9395
}
9496

97+
public int getMaxContinuations() {
98+
return maxContinuations;
99+
}
100+
95101
@Override
96102
public String toString() {
97103
final StringBuilder builder = new StringBuilder();
@@ -102,6 +108,7 @@ public String toString() {
102108
.append(", maxFrameSize=").append(this.maxFrameSize)
103109
.append(", maxHeaderListSize=").append(this.maxHeaderListSize)
104110
.append(", compressionEnabled=").append(this.compressionEnabled)
111+
.append(", maxContinuations=").append(this.maxContinuations)
105112
.append("]");
106113
return builder.toString();
107114
}
@@ -147,6 +154,7 @@ public static class Builder {
147154
private int maxFrameSize;
148155
private int maxHeaderListSize;
149156
private boolean compressionEnabled;
157+
private int maxContinuations;
150158

151159
Builder() {
152160
this.headerTableSize = INIT_HEADER_TABLE_SIZE * 2;
@@ -156,6 +164,7 @@ public static class Builder {
156164
this.maxFrameSize = FrameConsts.MIN_FRAME_SIZE * 4;
157165
this.maxHeaderListSize = FrameConsts.MAX_FRAME_SIZE;
158166
this.compressionEnabled = true;
167+
this.maxContinuations = 100;
159168
}
160169

161170
public Builder setHeaderTableSize(final int headerTableSize) {
@@ -198,6 +207,18 @@ public Builder setCompressionEnabled(final boolean compressionEnabled) {
198207
return this;
199208
}
200209

210+
/**
211+
* Sets max limit on number of continuations.
212+
* <p>value zero represents no limit</p>
213+
*
214+
* @since 5,4
215+
*/
216+
public Builder setMaxContinuations(final int maxContinuations) {
217+
Args.positive(maxContinuations, "Max continuations");
218+
this.maxContinuations = maxContinuations;
219+
return this;
220+
}
221+
201222
public H2Config build() {
202223
return new H2Config(
203224
headerTableSize,
@@ -206,7 +227,8 @@ public H2Config build() {
206227
initialWindowSize,
207228
maxFrameSize,
208229
maxHeaderListSize,
209-
compressionEnabled);
230+
compressionEnabled,
231+
maxContinuations);
210232
}
211233

212234
}

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,7 +1061,8 @@ private void maximizeWindow(final int streamId, final AtomicInteger window) thro
10611061
private void consumePushPromiseFrame(final RawFrame frame, final ByteBuffer payload, final H2Stream promisedStream) throws HttpException, IOException {
10621062
final int promisedStreamId = promisedStream.getId();
10631063
if (!frame.isFlagSet(FrameFlag.END_HEADERS)) {
1064-
continuation = new Continuation(promisedStreamId, frame.getType(), true);
1064+
continuation = new Continuation(promisedStreamId, frame.getType(), true,
1065+
localConfig.getMaxContinuations());
10651066
}
10661067
if (continuation == null) {
10671068
final List<Header> headers = hPackDecoder.decodeHeaders(payload);
@@ -1087,7 +1088,8 @@ private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) thr
10871088
}
10881089
final int streamId = stream.getId();
10891090
if (!frame.isFlagSet(FrameFlag.END_HEADERS)) {
1090-
continuation = new Continuation(streamId, frame.getType(), frame.isFlagSet(FrameFlag.END_STREAM));
1091+
continuation = new Continuation(streamId, frame.getType(), frame.isFlagSet(FrameFlag.END_STREAM),
1092+
localConfig.getMaxContinuations());
10911093
}
10921094
final ByteBuffer payload = frame.getPayloadContent();
10931095
if (frame.isFlagSet(FrameFlag.PRIORITY)) {
@@ -1340,18 +1342,28 @@ private static class Continuation {
13401342
final int type;
13411343
final boolean endStream;
13421344
final ByteArrayBuffer headerBuffer;
1345+
final int maxContinuation;
1346+
final boolean enforceMacContinuations;
13431347

1344-
private Continuation(final int streamId, final int type, final boolean endStream) {
1348+
private int count;
1349+
1350+
private Continuation(final int streamId, final int type, final boolean endStream, final int maxContinuation) {
13451351
this.streamId = streamId;
13461352
this.type = type;
13471353
this.endStream = endStream;
1354+
this.maxContinuation = maxContinuation;
1355+
this.enforceMacContinuations = maxContinuation < Integer.MAX_VALUE;
13481356
this.headerBuffer = new ByteArrayBuffer(1024);
13491357
}
13501358

1351-
void copyPayload(final ByteBuffer payload) {
1359+
void copyPayload(final ByteBuffer payload) throws H2ConnectionException {
13521360
if (payload == null) {
13531361
return;
13541362
}
1363+
if (enforceMacContinuations && count > maxContinuation) {
1364+
throw new H2ConnectionException(H2Error.ENHANCE_YOUR_CALM, "Excessive number of continuation frames");
1365+
}
1366+
count++;
13551367
final int originalLength = headerBuffer.length();
13561368
final int toCopy = payload.remaining();
13571369
headerBuffer.ensureCapacity(toCopy);

httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java

Lines changed: 109 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636

3737
import org.apache.hc.core5.function.Supplier;
3838
import org.apache.hc.core5.http.Header;
39-
import org.apache.hc.core5.http.HttpException;
4039
import org.apache.hc.core5.http.config.CharCodingConfig;
4140
import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics;
4241
import org.apache.hc.core5.http.impl.CharCodingSupport;
@@ -245,7 +244,7 @@ void testInputMultipleFrames() throws Exception {
245244
}
246245

247246
@Test
248-
void testInputHeaderContinuationFrame() throws IOException, HttpException {
247+
void testInputHeaderContinuationFrame() throws Exception {
249248
final H2Config h2Config = H2Config.custom().setMaxFrameSize(FrameConsts.MIN_FRAME_SIZE)
250249
.build();
251250

@@ -538,5 +537,113 @@ void testContinuationAfterEndOfStream() throws Exception {
538537
Assertions.assertEquals(H2Error.PROTOCOL_ERROR, H2Error.getByCode(exception.getCode()));
539538
}
540539

540+
541+
@Test
542+
void testInputHeaderContinuationFramesNoLimit() throws Exception {
543+
final H2Config h2Config = H2Config.custom()
544+
.setMaxContinuations(Integer.MAX_VALUE)
545+
.build();
546+
547+
final ByteArrayBuffer headerBuf = new ByteArrayBuffer(19);
548+
final HPackEncoder encoder = new HPackEncoder(H2Config.INIT.getHeaderTableSize(), CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT));
549+
final List<Header> headers = new ArrayList<>();
550+
headers.add(new BasicHeader(":status", "200"));
551+
for (int i = 1; i <= 100; i++) {
552+
headers.add(new BasicHeader("test-header-key-" + i, "value-" + i));
553+
}
554+
encoder.encodeHeaders(headerBuf, headers, h2Config.isCompressionEnabled());
555+
556+
Assertions.assertTrue(headerBuf.length() > 750);
557+
558+
final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl(
559+
protocolIOSession,
560+
FRAME_FACTORY,
561+
StreamIdGenerator.ODD,
562+
httpProcessor,
563+
CharCodingConfig.DEFAULT,
564+
h2Config,
565+
h2StreamListener,
566+
() -> streamHandler);
567+
568+
final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024);
569+
final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024);
570+
571+
final RawFrame headerFrame = FRAME_FACTORY.createHeaders(2, ByteBuffer.wrap(headerBuf.array(), 0, 250), false, false);
572+
outBuffer.write(headerFrame, writableChannel);
573+
574+
streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()));
575+
576+
writableChannel.reset();
577+
final RawFrame continuationFrame1 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 250, 250), false);
578+
outBuffer.write(continuationFrame1, writableChannel);
579+
streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()));
580+
581+
writableChannel.reset();
582+
final RawFrame continuationFrame2 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 500, 250), false);
583+
outBuffer.write(continuationFrame2, writableChannel);
584+
streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()));
585+
586+
writableChannel.reset();
587+
final RawFrame continuationFrame3 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 750, headerBuf.length() - 750), true);
588+
outBuffer.write(continuationFrame3, writableChannel);
589+
streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()));
590+
591+
Mockito.verify(streamHandler).consumeHeader(headersCaptor.capture(), ArgumentMatchers.eq(false));
592+
Assertions.assertFalse(headersCaptor.getValue().isEmpty());
593+
}
594+
595+
@Test
596+
void testInputHeaderContinuationFramesMaxLimit() throws Exception {
597+
final H2Config h2Config = H2Config.custom()
598+
.setMaxContinuations(2)
599+
.build();
600+
601+
final ByteArrayBuffer headerBuf = new ByteArrayBuffer(19);
602+
final HPackEncoder encoder = new HPackEncoder(H2Config.INIT.getHeaderTableSize(), CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT));
603+
final List<Header> headers = new ArrayList<>();
604+
headers.add(new BasicHeader(":status", "200"));
605+
for (int i = 1; i <= 100; i++) {
606+
headers.add(new BasicHeader("test-header-key-" + i, "value-" + i));
607+
}
608+
encoder.encodeHeaders(headerBuf, headers, h2Config.isCompressionEnabled());
609+
610+
Assertions.assertTrue(headerBuf.length() > 750);
611+
612+
final AbstractH2StreamMultiplexer streamMultiplexer = new H2StreamMultiplexerImpl(
613+
protocolIOSession,
614+
FRAME_FACTORY,
615+
StreamIdGenerator.ODD,
616+
httpProcessor,
617+
CharCodingConfig.DEFAULT,
618+
h2Config,
619+
h2StreamListener,
620+
() -> streamHandler);
621+
622+
final WritableByteChannelMock writableChannel = new WritableByteChannelMock(1024);
623+
final FrameOutputBuffer outBuffer = new FrameOutputBuffer(16 * 1024);
624+
625+
final RawFrame headerFrame = FRAME_FACTORY.createHeaders(2, ByteBuffer.wrap(headerBuf.array(), 0, 250), false, false);
626+
outBuffer.write(headerFrame, writableChannel);
627+
628+
streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()));
629+
630+
writableChannel.reset();
631+
final RawFrame continuationFrame1 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 250, 250), false);
632+
outBuffer.write(continuationFrame1, writableChannel);
633+
streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()));
634+
635+
writableChannel.reset();
636+
final RawFrame continuationFrame2 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 500, 250), false);
637+
outBuffer.write(continuationFrame2, writableChannel);
638+
streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray()));
639+
640+
writableChannel.reset();
641+
final RawFrame continuationFrame3 = FRAME_FACTORY.createContinuation(2, ByteBuffer.wrap(headerBuf.array(), 750, headerBuf.length() - 750), true);
642+
outBuffer.write(continuationFrame3, writableChannel);
643+
644+
Assertions.assertThrows(H2ConnectionException.class, () ->
645+
streamMultiplexer.onInput(ByteBuffer.wrap(writableChannel.toByteArray())));
646+
}
647+
541648
}
542649

0 commit comments

Comments
 (0)