Skip to content

Commit c42ab4b

Browse files
committed
Fix header and initial line length counting
Related: netty#3445 Motivation: HttpObjectDecoder.HeaderParser does not reset its counter (the size field) when it failed to find the end of line. If a header is split into multiple fragments, the counter is increased as many times as the number of fragments, resulting an unexpected TooLongFrameException. Modifications: - Add test cases that reproduces the problem - Reset the HeaderParser.size field when no EOL is found. Result: One less bug
1 parent f230d59 commit c42ab4b

File tree

2 files changed

+86
-8
lines changed

2 files changed

+86
-8
lines changed

codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java

+21-8
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> ou
214214
currentState = State.READ_HEADER;
215215
// fall-through
216216
} catch (Exception e) {
217-
out.add(invalidMessage(e));
217+
out.add(invalidMessage(buffer, e));
218218
return;
219219
}
220220
case READ_HEADER: try {
@@ -261,7 +261,7 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> ou
261261
return;
262262
}
263263
} catch (Exception e) {
264-
out.add(invalidMessage(e));
264+
out.add(invalidMessage(buffer, e));
265265
return;
266266
}
267267
case READ_VARIABLE_LENGTH_CONTENT: {
@@ -320,7 +320,7 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> ou
320320
currentState = State.READ_CHUNKED_CONTENT;
321321
// fall-through
322322
} catch (Exception e) {
323-
out.add(invalidChunk(e));
323+
out.add(invalidChunk(buffer, e));
324324
return;
325325
}
326326
case READ_CHUNKED_CONTENT: {
@@ -363,7 +363,7 @@ protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> ou
363363
resetNow();
364364
return;
365365
} catch (Exception e) {
366-
out.add(invalidChunk(e));
366+
out.add(invalidChunk(buffer, e));
367367
return;
368368
}
369369
case BAD_MESSAGE: {
@@ -468,8 +468,13 @@ private void resetNow() {
468468
currentState = State.SKIP_CONTROL_CHARS;
469469
}
470470

471-
private HttpMessage invalidMessage(Exception cause) {
471+
private HttpMessage invalidMessage(ByteBuf in, Exception cause) {
472472
currentState = State.BAD_MESSAGE;
473+
474+
// Advance the readerIndex so that ByteToMessageDecoder does not complain
475+
// when we produced an invalid message without consuming anything.
476+
in.skipBytes(in.readableBytes());
477+
473478
if (message != null) {
474479
message.setDecoderResult(DecoderResult.failure(cause));
475480
} else {
@@ -482,8 +487,13 @@ private HttpMessage invalidMessage(Exception cause) {
482487
return ret;
483488
}
484489

485-
private HttpContent invalidChunk(Exception cause) {
490+
private HttpContent invalidChunk(ByteBuf in, Exception cause) {
486491
currentState = State.BAD_MESSAGE;
492+
493+
// Advance the readerIndex so that ByteToMessageDecoder does not complain
494+
// when we produced an invalid message without consuming anything.
495+
in.skipBytes(in.readableBytes());
496+
487497
HttpContent chunk = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER);
488498
chunk.setDecoderResult(DecoderResult.failure(cause));
489499
message = null;
@@ -735,9 +745,11 @@ private static class HeaderParser implements ByteBufProcessor {
735745
}
736746

737747
public AppendableCharSequence parse(ByteBuf buffer) {
748+
final int oldSize = size;
738749
seq.reset();
739750
int i = buffer.forEachByte(this);
740751
if (i == -1) {
752+
size = oldSize;
741753
return null;
742754
}
743755
buffer.readerIndex(i + 1);
@@ -757,14 +769,15 @@ public boolean process(byte value) throws Exception {
757769
if (nextByte == HttpConstants.LF) {
758770
return false;
759771
}
760-
if (size >= maxLength) {
772+
773+
if (++ size > maxLength) {
761774
// TODO: Respond with Bad Request and discard the traffic
762775
// or close the connection.
763776
// No need to notify the upstream handlers - just log.
764777
// If decoding a response, just throw an exception.
765778
throw newException(maxLength);
766779
}
767-
size++;
780+
768781
seq.append(nextByte);
769782
return true;
770783
}

codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java

+65
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,81 @@
1717

1818
import io.netty.buffer.Unpooled;
1919
import io.netty.channel.embedded.EmbeddedChannel;
20+
import io.netty.handler.codec.TooLongFrameException;
2021
import io.netty.util.CharsetUtil;
2122
import org.junit.Test;
2223

24+
import java.util.Arrays;
2325
import java.util.List;
2426

2527
import static org.hamcrest.CoreMatchers.*;
2628
import static org.junit.Assert.*;
2729

2830
public class HttpResponseDecoderTest {
2931

32+
/**
33+
* The size of headers should be calculated correctly even if a single header is split into multiple fragments.
34+
* @see <a href="https://github.com/netty/netty/issues/3445">#3445</a>
35+
*/
36+
@Test
37+
public void testMaxHeaderSize1() {
38+
final int maxHeaderSize = 8192;
39+
40+
final EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder(4096, maxHeaderSize, 8192));
41+
final char[] bytes = new char[maxHeaderSize / 2 - 2];
42+
Arrays.fill(bytes, 'a');
43+
44+
ch.writeInbound(Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\n", CharsetUtil.US_ASCII));
45+
46+
// Write two 4096-byte headers (= 8192 bytes)
47+
ch.writeInbound(Unpooled.copiedBuffer("A:", CharsetUtil.US_ASCII));
48+
ch.writeInbound(Unpooled.copiedBuffer(bytes, CharsetUtil.US_ASCII));
49+
ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII));
50+
assertNull(ch.readInbound());
51+
ch.writeInbound(Unpooled.copiedBuffer("B:", CharsetUtil.US_ASCII));
52+
ch.writeInbound(Unpooled.copiedBuffer(bytes, CharsetUtil.US_ASCII));
53+
ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII));
54+
ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII));
55+
56+
HttpResponse res = ch.readInbound();
57+
assertNull(res.decoderResult().cause());
58+
assertTrue(res.decoderResult().isSuccess());
59+
60+
assertNull(ch.readInbound());
61+
assertTrue(ch.finish());
62+
assertThat(ch.readInbound(), instanceOf(LastHttpContent.class));
63+
}
64+
65+
/**
66+
* Complementary test case of {@link #testMaxHeaderSize1()} When it actually exceeds the maximum, it should fail.
67+
*/
68+
@Test
69+
public void testMaxHeaderSize2() {
70+
final int maxHeaderSize = 8192;
71+
72+
final EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder(4096, maxHeaderSize, 8192));
73+
final char[] bytes = new char[maxHeaderSize / 2 - 2];
74+
Arrays.fill(bytes, 'a');
75+
76+
ch.writeInbound(Unpooled.copiedBuffer("HTTP/1.1 200 OK\r\n", CharsetUtil.US_ASCII));
77+
78+
// Write a 4096-byte header and a 4097-byte header to test an off-by-one case (= 8193 bytes)
79+
ch.writeInbound(Unpooled.copiedBuffer("A:", CharsetUtil.US_ASCII));
80+
ch.writeInbound(Unpooled.copiedBuffer(bytes, CharsetUtil.US_ASCII));
81+
ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII));
82+
assertNull(ch.readInbound());
83+
ch.writeInbound(Unpooled.copiedBuffer("B: ", CharsetUtil.US_ASCII)); // Note an extra space.
84+
ch.writeInbound(Unpooled.copiedBuffer(bytes, CharsetUtil.US_ASCII));
85+
ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII));
86+
ch.writeInbound(Unpooled.copiedBuffer("\r\n", CharsetUtil.US_ASCII));
87+
88+
HttpResponse res = ch.readInbound();
89+
assertTrue(res.decoderResult().cause() instanceof TooLongFrameException);
90+
91+
assertFalse(ch.finish());
92+
assertNull(ch.readInbound());
93+
}
94+
3095
@Test
3196
public void testResponseChunked() {
3297
EmbeddedChannel ch = new EmbeddedChannel(new HttpResponseDecoder());

0 commit comments

Comments
 (0)