diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java index 2e6652c52a..ea1fdee7d4 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpClientCodec.java @@ -28,9 +28,11 @@ import java.util.List; import java.util.Queue; import java.util.concurrent.atomic.AtomicLong; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS; import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_CHUNK_SIZE; import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_HEADER_SIZE; import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_INITIAL_LINE_LENGTH; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_VALIDATE_HEADERS; /** * A combination of {@link HttpRequestEncoder} and {@link HttpResponseDecoder} @@ -48,6 +50,8 @@ import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_INITIAL_ */ public final class HttpClientCodec extends CombinedChannelDuplexHandler implements HttpClientUpgradeHandler.SourceCodec { + public static final boolean DEFAULT_FAIL_ON_MISSING_RESPONSE = false; + public static final boolean DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST = false; /** A queue that is used for correlating a request and a response. */ private final Queue queue = new ArrayDeque(); @@ -65,14 +69,15 @@ public final class HttpClientCodec extends CombinedChannelDuplexHandlerParameters that prevents excessive memory consumption * * - * + * * * * + * * * * + * * * * * + * * *
NameMeaningNameDefault valueMeaning
{@code maxInitialLineLength}{@value #DEFAULT_MAX_INITIAL_LINE_LENGTH}The maximum length of the initial line * (e.g. {@code "GET / HTTP/1.0"} or {@code "HTTP/1.0 200 OK"}) * If the length of the initial line exceeds this value, a @@ -48,11 +51,13 @@ import java.util.List; *
{@code maxHeaderSize}{@value #DEFAULT_MAX_HEADER_SIZE}The maximum length of all headers. If the sum of the length of each * header exceeds this value, a {@link TooLongFrameException} will be raised.
{@code maxChunkSize}{@value #DEFAULT_MAX_CHUNK_SIZE}The maximum length of the content or each chunk. If the content length * (or the length of each chunk) exceeds this value, the content or chunk * will be split into multiple {@link HttpContent}s whose length is @@ -60,6 +65,21 @@ import java.util.List; *
* + *

Parameters that control parsing behavior

+ * + * + * + * + * + * + * + * + * + *
NameDefault valueMeaning
{@code allowDuplicateContentLengths}{@value #DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS}When set to {@code false}, will reject any messages that contain multiple Content-Length header fields. + * When set to {@code true}, will allow multiple Content-Length headers only if they are all the same decimal value. + * The duplicated field-values will be replaced with a single valid Content-Length field. + * See RFC 7230, Section 3.3.2.
+ * *

Chunked Content

* * If the content of an HTTP message is greater than {@code maxChunkSize} or @@ -108,12 +128,15 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { public static final int DEFAULT_MAX_CHUNK_SIZE = 8192; public static final boolean DEFAULT_VALIDATE_HEADERS = true; public static final int DEFAULT_INITIAL_BUFFER_SIZE = 128; + public static final boolean DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS = false; private static final String EMPTY_VALUE = ""; + private static final Pattern COMMA_PATTERN = Pattern.compile(","); private final int maxChunkSize; private final boolean chunkedSupported; protected final boolean validateHeaders; + private final boolean allowDuplicateContentLengths; private final HeaderParser headerParser; private final LineParser lineParser; @@ -176,9 +199,20 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { DEFAULT_INITIAL_BUFFER_SIZE); } + /** + * Creates a new instance with the specified parameters. + */ protected HttpObjectDecoder( int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean chunkedSupported, boolean validateHeaders, int initialBufferSize) { + this(maxInitialLineLength, maxHeaderSize, maxChunkSize, chunkedSupported, validateHeaders, initialBufferSize, + DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS); + } + + protected HttpObjectDecoder( + int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, + boolean chunkedSupported, boolean validateHeaders, int initialBufferSize, + boolean allowDuplicateContentLengths) { checkPositive(maxInitialLineLength, "maxInitialLineLength"); checkPositive(maxHeaderSize, "maxHeaderSize"); checkPositive(maxChunkSize, "maxChunkSize"); @@ -189,6 +223,7 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { this.maxChunkSize = maxChunkSize; this.chunkedSupported = chunkedSupported; this.validateHeaders = validateHeaders; + this.allowDuplicateContentLengths = allowDuplicateContentLengths; } @Override @@ -594,10 +629,9 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { name = null; value = null; - List values = headers.getAll(HttpHeaderNames.CONTENT_LENGTH); - int contentLengthValuesCount = values.size(); + List contentLengthFields = headers.getAll(HttpHeaderNames.CONTENT_LENGTH); - if (contentLengthValuesCount > 0) { + if (!contentLengthFields.isEmpty()) { // Guard against multiple Content-Length headers as stated in // https://tools.ietf.org/html/rfc7230#section-3.3.2: // @@ -611,17 +645,42 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder { // duplicated field-values with a single valid Content-Length field // containing that decimal value prior to determining the message body // length or forwarding the message. - if (contentLengthValuesCount > 1 && message.protocolVersion() == HttpVersion.HTTP_1_1) { - throw new IllegalArgumentException("Multiple Content-Length headers found"); + boolean multipleContentLengths = + contentLengthFields.size() > 1 || contentLengthFields.get(0).indexOf(COMMA) >= 0; + if (multipleContentLengths && message.protocolVersion() == HttpVersion.HTTP_1_1) { + if (allowDuplicateContentLengths) { + // Find and enforce that all Content-Length values are the same + String firstValue = null; + for (String field : contentLengthFields) { + String[] tokens = COMMA_PATTERN.split(field, -1); + for (String token : tokens) { + String trimmed = token.trim(); + if (firstValue == null) { + firstValue = trimmed; + } else if (!trimmed.equals(firstValue)) { + throw new IllegalArgumentException( + "Multiple Content-Length values found: " + contentLengthFields); + } + } + } + // Replace the duplicated field-values with a single valid Content-Length field + headers.set(HttpHeaderNames.CONTENT_LENGTH, firstValue); + contentLength = Long.parseLong(firstValue); + } else { + // Reject the message as invalid + throw new IllegalArgumentException( + "Multiple Content-Length values found: " + contentLengthFields); + } + } else { + contentLength = Long.parseLong(contentLengthFields.get(0)); } - contentLength = Long.parseLong(values.get(0)); } if (isContentAlwaysEmpty(message)) { HttpUtil.setTransferEncodingChunked(message, false); return State.SKIP_CONTROL_CHARS; } else if (HttpUtil.isTransferEncodingChunked(message)) { - if (contentLengthValuesCount > 0 && message.protocolVersion() == HttpVersion.HTTP_1_1) { + if (!contentLengthFields.isEmpty() && message.protocolVersion() == HttpVersion.HTTP_1_1) { handleTransferEncodingChunkedWithContentLength(message); } return State.READ_CHUNK_SIZE; diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestDecoder.java index 70c1db5540..ba2d79ecb4 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpRequestDecoder.java @@ -82,6 +82,13 @@ public class HttpRequestDecoder extends HttpObjectDecoder { initialBufferSize); } + public HttpRequestDecoder( + int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders, + int initialBufferSize, boolean allowDuplicateContentLengths) { + super(maxInitialLineLength, maxHeaderSize, maxChunkSize, DEFAULT_CHUNKED_SUPPORTED, validateHeaders, + initialBufferSize, allowDuplicateContentLengths); + } + @Override protected HttpMessage createMessage(String[] initialLine) throws Exception { return new DefaultHttpRequest( diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseDecoder.java index bd26b3c1fb..78e9e8643d 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseDecoder.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpResponseDecoder.java @@ -113,6 +113,13 @@ public class HttpResponseDecoder extends HttpObjectDecoder { initialBufferSize); } + public HttpResponseDecoder( + int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders, + int initialBufferSize, boolean allowDuplicateContentLengths) { + super(maxInitialLineLength, maxHeaderSize, maxChunkSize, DEFAULT_CHUNKED_SUPPORTED, validateHeaders, + initialBufferSize, allowDuplicateContentLengths); + } + @Override protected HttpMessage createMessage(String[] initialLine) { return new DefaultHttpResponse( diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java index d08474974b..51a654014c 100644 --- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerCodec.java @@ -75,6 +75,16 @@ public final class HttpServerCodec extends CombinedChannelDuplexHandler out) throws Exception { int oldSize = out.size(); diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/MultipleContentLengthHeadersTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/MultipleContentLengthHeadersTest.java new file mode 100644 index 0000000000..29c7d84b71 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/MultipleContentLengthHeadersTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2020 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: + * + * 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.netty.handler.codec.http; + +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.CharsetUtil; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_INITIAL_BUFFER_SIZE; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_CHUNK_SIZE; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_HEADER_SIZE; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_INITIAL_LINE_LENGTH; +import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_VALIDATE_HEADERS; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +@RunWith(Parameterized.class) +public class MultipleContentLengthHeadersTest { + + private final boolean allowDuplicateContentLengths; + private final boolean sameValue; + private final boolean singleField; + + private EmbeddedChannel channel; + + @Parameters + public static Collection parameters() { + return Arrays.asList(new Object[][] { + { false, false, false }, + { false, false, true }, + { false, true, false }, + { false, true, true }, + { true, false, false }, + { true, false, true }, + { true, true, false }, + { true, true, true } + }); + } + + public MultipleContentLengthHeadersTest( + boolean allowDuplicateContentLengths, boolean sameValue, boolean singleField) { + this.allowDuplicateContentLengths = allowDuplicateContentLengths; + this.sameValue = sameValue; + this.singleField = singleField; + } + + @Before + public void setUp() { + HttpRequestDecoder decoder = new HttpRequestDecoder( + DEFAULT_MAX_INITIAL_LINE_LENGTH, + DEFAULT_MAX_HEADER_SIZE, + DEFAULT_MAX_CHUNK_SIZE, + DEFAULT_VALIDATE_HEADERS, + DEFAULT_INITIAL_BUFFER_SIZE, + allowDuplicateContentLengths); + channel = new EmbeddedChannel(decoder); + } + + @Test + public void testMultipleContentLengthHeadersBehavior() { + String requestStr = setupRequestString(); + assertThat(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)), is(true)); + HttpRequest request = channel.readInbound(); + + if (allowDuplicateContentLengths) { + if (sameValue) { + assertValid(request); + List contentLengths = request.headers().getAll(HttpHeaderNames.CONTENT_LENGTH); + assertThat(contentLengths, contains("1")); + LastHttpContent body = channel.readInbound(); + assertThat(body.content().readableBytes(), is(1)); + assertThat(body.content().readCharSequence(1, CharsetUtil.US_ASCII).toString(), is("a")); + } else { + assertInvalid(request); + } + } else { + assertInvalid(request); + } + assertThat(channel.finish(), is(false)); + } + + private String setupRequestString() { + String firstValue = "1"; + String secondValue = sameValue ? firstValue : "2"; + String contentLength; + if (singleField) { + contentLength = "Content-Length: " + firstValue + ", " + secondValue + "\r\n\r\n"; + } else { + contentLength = "Content-Length: " + firstValue + "\r\n" + + "Content-Length: " + secondValue + "\r\n\r\n"; + } + return "PUT /some/path HTTP/1.1\r\n" + + contentLength + + "ab"; + } + + @Test + public void testDanglingComma() { + String requestStr = "GET /some/path HTTP/1.1\r\n" + + "Content-Length: 1,\r\n" + + "Connection: close\n\n" + + "ab"; + assertThat(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)), is(true)); + HttpRequest request = channel.readInbound(); + assertInvalid(request); + assertThat(channel.finish(), is(false)); + } + + private static void assertValid(HttpRequest request) { + assertThat(request.decoderResult().isFailure(), is(false)); + } + + private static void assertInvalid(HttpRequest request) { + assertThat(request.decoderResult().isFailure(), is(true)); + assertThat(request.decoderResult().cause(), instanceOf(IllegalArgumentException.class)); + assertThat(request.decoderResult().cause().getMessage(), + containsString("Multiple Content-Length values found")); + } +}