From 00eb5618df0509702aa03180729686870530a4d7 Mon Sep 17 00:00:00 2001 From: Bennett Lynch Date: Fri, 12 Mar 2021 04:49:51 -0800 Subject: [PATCH] Introduce HttpMessageDecoderResult to expose decoded header size (#11068) Motivation The HttpObjectDecoder accepts input parameters for maxInitialLineLength and maxHeaderSize. These are important variables since both message components must be buffered in memory. As such, many decoders (like Netty and others) introduce constraints. Due to their importance, many users may wish to add instrumentation on the values of successful decoder results, or otherwise be able to access these values to enforce their own supplemental constraints. While users can perhaps estimate the sizes today, they will not be exact, due to the decoder being responsible for consuming optional whitespace and the like. Modifications * Add HttpMessageDecoderResult class. This class extends DecoderResult and is intended for HttpMessage objects successfully decoded by the HttpObjectDecoder. It exposes attributes for the decoded initialLineLength and headerSize. * Modify HttpObjectDecoder to produce HttpMessageDecoderResults upon successfully decoding the last HTTP header. * Add corresponding tests to HttpRequestDecoderTest & HttpResponseDecoderTest. Co-authored-by: Bennett Lynch --- .../codec/http/HttpMessageDecoderResult.java | 58 +++++++++++++++++++ .../handler/codec/http/HttpObjectDecoder.java | 6 +- .../codec/http/HttpRequestDecoderTest.java | 21 +++++++ .../codec/http/HttpResponseDecoderTest.java | 20 +++++++ 4 files changed, 104 insertions(+), 1 deletion(-) create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/HttpMessageDecoderResult.java diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpMessageDecoderResult.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMessageDecoderResult.java new file mode 100644 index 0000000000..b89252b24e --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpMessageDecoderResult.java @@ -0,0 +1,58 @@ +/* + * Copyright 2021 The Netty Project + * + * The Netty Project licenses this file to you 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: + * + * https://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.netty.handler.codec.http; + +import io.netty.handler.codec.DecoderResult; + +/** + * A {@link DecoderResult} for {@link HttpMessage}s as produced by an {@link HttpObjectDecoder}. + *

+ * Please note that there is no guarantee that a {@link HttpObjectDecoder} will produce a {@link + * HttpMessageDecoderResult}. It may simply produce a regular {@link DecoderResult}. This result is intended for + * successful {@link HttpMessage} decoder results. + */ +public final class HttpMessageDecoderResult extends DecoderResult { + + private final int initialLineLength; + private final int headerSize; + + HttpMessageDecoderResult(int initialLineLength, int headerSize) { + super(SIGNAL_SUCCESS); + this.initialLineLength = initialLineLength; + this.headerSize = headerSize; + } + + /** + * The decoded initial line length (in bytes), as controlled by {@code maxInitialLineLength}. + */ + public int initialLineLength() { + return initialLineLength; + } + + /** + * The decoded header size (in bytes), as controlled by {@code maxHeaderSize}. + */ + public int headerSize() { + return headerSize; + } + + /** + * The decoded initial line length plus the decoded header size (in bytes). + */ + public int totalSize() { + return initialLineLength + headerSize; + } +} diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java index 3482fba8cd..98ee3c578b 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java @@ -614,6 +614,10 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { name = null; value = null; + // Done parsing initial line and headers. Set decoder result. + HttpMessageDecoderResult decoderResult = new HttpMessageDecoderResult(lineParser.size, headerParser.size); + message.setDecoderResult(decoderResult); + List contentLengthFields = headers.getAll(HttpHeaderNames.CONTENT_LENGTH); if (!contentLengthFields.isEmpty()) { @@ -912,7 +916,7 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { private static class HeaderParser implements ByteProcessor { private final AppendableCharSequence seq; private final int maxLength; - private int size; + int size; HeaderParser(AppendableCharSequence seq, int maxLength) { this.seq = seq; diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java index db6caaa25d..3e15abe5a3 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java @@ -20,6 +20,7 @@ import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.TooLongFrameException; import io.netty.util.AsciiString; import io.netty.util.CharsetUtil; + import org.junit.Test; import java.util.List; @@ -478,6 +479,26 @@ public class HttpRequestDecoderTest { assertFalse(channel.finish()); } + @Test + public void testHttpMessageDecoderResult() { + String requestStr = "PUT /some/path HTTP/1.1\r\n" + + "Content-Length: 11\r\n" + + "Connection: close\r\n\r\n" + + "Lorem ipsum"; + EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII))); + HttpRequest request = channel.readInbound(); + assertTrue(request.decoderResult().isSuccess()); + assertThat(request.decoderResult(), instanceOf(HttpMessageDecoderResult.class)); + HttpMessageDecoderResult decoderResult = (HttpMessageDecoderResult) request.decoderResult(); + assertThat(decoderResult.initialLineLength(), is(23)); + assertThat(decoderResult.headerSize(), is(35)); + assertThat(decoderResult.totalSize(), is(58)); + HttpContent c = channel.readInbound(); + c.release(); + assertFalse(channel.finish()); + } + private static void testInvalidHeaders0(String requestStr) { EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder()); assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII))); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java index e51626e0c7..9f2f07e3ac 100644 --- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java @@ -678,4 +678,24 @@ public class HttpResponseDecoderTest { assertEquals("netty.io", response.headers().get(HttpHeaderNames.HOST)); assertFalse(channel.finish()); } + + @Test + public void testHttpMessageDecoderResult() { + String responseStr = "HTTP/1.1 200 OK\r\n" + + "Content-Length: 11\r\n" + + "Connection: close\r\n\r\n" + + "Lorem ipsum"; + EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseDecoder()); + assertTrue(channel.writeInbound(Unpooled.copiedBuffer(responseStr, CharsetUtil.US_ASCII))); + HttpResponse response = channel.readInbound(); + assertTrue(response.decoderResult().isSuccess()); + assertThat(response.decoderResult(), instanceOf(HttpMessageDecoderResult.class)); + HttpMessageDecoderResult decoderResult = (HttpMessageDecoderResult) response.decoderResult(); + assertThat(decoderResult.initialLineLength(), is(15)); + assertThat(decoderResult.headerSize(), is(35)); + assertThat(decoderResult.totalSize(), is(50)); + HttpContent c = channel.readInbound(); + c.release(); + assertFalse(channel.finish()); + } }