netty/CVE-2021-21295-pre3.patch
2021-04-06 15:28:11 +08:00

567 lines
27 KiB
Diff

From 9557c88da2aaa49b3c3fd7525462dc0694681c19 Mon Sep 17 00:00:00 2001
From: Bennett Lynch <bennett.lynch@gmail.com>
Date: Mon, 6 Jul 2020 01:25:13 -0700
Subject: [PATCH] Add option to HttpObjectDecoder to allow duplicate
Content-Lengths (#10349)
Motivation:
Since https://github.com/netty/netty/pull/9865 (Netty 4.1.44) the
default behavior of the HttpObjectDecoder has been to reject any HTTP
message that is found to have multiple Content-Length headers when
decoding. This behavior is well-justified as per the risks outlined in
https://github.com/netty/netty/issues/9861, however, we can see from the
cited RFC section that there are multiple possible options offered for
responding to this scenario:
> If a message is received that has multiple Content-Length header
> fields with field-values consisting of the same decimal value, or a
> single Content-Length header field with a field value containing a
> list of identical decimal values (e.g., "Content-Length: 42, 42"),
> indicating that duplicate Content-Length header fields have been
> generated or combined by an upstream message processor, then the
> recipient MUST either reject the message as invalid or replace the
> 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.
https://tools.ietf.org/html/rfc7230#section-3.3.2
Netty opted for the first option (rejecting as invalid), which seems
like the safest, but the second option (replacing duplicate values with
a single value) is also valid behavior.
Modifications:
* Introduce "allowDuplicateContentLengths" parameter to
HttpObjectDecoder (defaulting to false).
* When set to true, will allow multiple Content-Length headers only if
they are all the same value. The duplicated field-values will be
replaced with a single valid Content-Length field.
* Add new parameterized test class for testing different variations of
multiple Content-Length headers.
Result:
This is a backwards-compatible change with no functional change to the
existing behavior.
Note that the existing logic would result in NumberFormatExceptions
for header values like "Content-Length: 42, 42". The new logic correctly
reports these as IllegalArgumentException with the proper error message.
Additionally note that this behavior is only applied to HTTP/1.1, but I
suspect that we may want to expand that to include HTTP/1.0 as well...
That behavior is not modified here to minimize the scope of this change.
---
.../handler/codec/http/HttpClientCodec.java | 35 ++++-
.../handler/codec/http/HttpObjectDecoder.java | 75 ++++++++-
.../codec/http/HttpRequestDecoder.java | 7 +
.../codec/http/HttpResponseDecoder.java | 7 +
.../handler/codec/http/HttpServerCodec.java | 16 ++
.../MultipleContentLengthHeadersTest.java | 144 ++++++++++++++++++
6 files changed, 268 insertions(+), 16 deletions(-)
create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/MultipleContentLengthHeadersTest.java
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 a832bfdff3..9a99fff97f 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<HttpResponseDecoder, HttpRequestEncoder>
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<HttpMethod> queue = new ArrayDeque<HttpMethod>();
@@ -65,14 +69,15 @@ public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResp
* {@code maxChunkSize (8192)}).
*/
public HttpClientCodec() {
- this(DEFAULT_MAX_INITIAL_LINE_LENGTH, DEFAULT_MAX_HEADER_SIZE, DEFAULT_MAX_CHUNK_SIZE, false);
+ this(DEFAULT_MAX_INITIAL_LINE_LENGTH, DEFAULT_MAX_HEADER_SIZE, DEFAULT_MAX_CHUNK_SIZE,
+ DEFAULT_FAIL_ON_MISSING_RESPONSE);
}
/**
* Creates a new instance with the specified decoder options.
*/
public HttpClientCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
- this(maxInitialLineLength, maxHeaderSize, maxChunkSize, false);
+ this(maxInitialLineLength, maxHeaderSize, maxChunkSize, DEFAULT_FAIL_ON_MISSING_RESPONSE);
}
/**
@@ -80,7 +85,7 @@ public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResp
*/
public HttpClientCodec(
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse) {
- this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, true);
+ this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, DEFAULT_VALIDATE_HEADERS);
}
/**
@@ -89,7 +94,8 @@ public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResp
public HttpClientCodec(
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
boolean validateHeaders) {
- this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders, false);
+ this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders,
+ DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST);
}
/**
@@ -110,7 +116,7 @@ public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResp
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
boolean validateHeaders, int initialBufferSize) {
this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders,
- initialBufferSize, false);
+ initialBufferSize, DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST);
}
/**
@@ -119,7 +125,19 @@ public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResp
public HttpClientCodec(
int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
boolean validateHeaders, int initialBufferSize, boolean parseHttpAfterConnectRequest) {
- init(new Decoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize),
+ this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders,
+ initialBufferSize, parseHttpAfterConnectRequest, DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS);
+ }
+
+ /**
+ * Creates a new instance with the specified decoder options.
+ */
+ public HttpClientCodec(
+ int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
+ boolean validateHeaders, int initialBufferSize, boolean parseHttpAfterConnectRequest,
+ boolean allowDuplicateContentLengths) {
+ init(new Decoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize,
+ allowDuplicateContentLengths),
new Encoder());
this.parseHttpAfterConnectRequest = parseHttpAfterConnectRequest;
this.failOnMissingResponse = failOnMissingResponse;
@@ -186,8 +204,9 @@ public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResp
}
Decoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders,
- int initialBufferSize) {
- super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize);
+ int initialBufferSize, boolean allowDuplicateContentLengths) {
+ super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize,
+ allowDuplicateContentLengths);
}
@Override
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 ed7caa7801..b52e36ac92 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
@@ -16,6 +16,7 @@
package io.netty.handler.codec.http;
import static io.netty.util.internal.ObjectUtil.checkPositive;
+import static io.netty.util.internal.StringUtil.COMMA;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
@@ -29,6 +30,7 @@ import io.netty.util.ByteProcessor;
import io.netty.util.internal.AppendableCharSequence;
import java.util.List;
+import java.util.regex.Pattern;
/**
* Decodes {@link ByteBuf}s into {@link HttpMessage}s and
@@ -37,10 +39,11 @@ import java.util.List;
* <h3>Parameters that prevents excessive memory consumption</h3>
* <table border="1">
* <tr>
- * <th>Name</th><th>Meaning</th>
+ * <th>Name</th><th>Default value</th><th>Meaning</th>
* </tr>
* <tr>
* <td>{@code maxInitialLineLength}</td>
+ * <td>{@value #DEFAULT_MAX_INITIAL_LINE_LENGTH}</td>
* <td>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;
* </tr>
* <tr>
* <td>{@code maxHeaderSize}</td>
+ * <td>{@value #DEFAULT_MAX_HEADER_SIZE}</td>
* <td>The maximum length of all headers. If the sum of the length of each
* header exceeds this value, a {@link TooLongFrameException} will be raised.</td>
* </tr>
* <tr>
* <td>{@code maxChunkSize}</td>
+ * <td>{@value #DEFAULT_MAX_CHUNK_SIZE}</td>
* <td>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;
* </tr>
* </table>
*
+ * <h3>Parameters that control parsing behavior</h3>
+ * <table border="1">
+ * <tr>
+ * <th>Name</th><th>Default value</th><th>Meaning</th>
+ * </tr>
+ * <tr>
+ * <td>{@code allowDuplicateContentLengths}</td>
+ * <td>{@value #DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS}</td>
+ * <td>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 <a href="https://tools.ietf.org/html/rfc7230#section-3.3.2">RFC 7230, Section 3.3.2</a>.</td>
+ * </tr>
+ * </table>
+ *
* <h3>Chunked Content</h3>
*
* 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
@@ -602,10 +637,9 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
name = null;
value = null;
- List<String> values = headers.getAll(HttpHeaderNames.CONTENT_LENGTH);
- int contentLengthValuesCount = values.size();
+ List<String> 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:
//
@@ -619,17 +653,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 39d4d6a5ad..62f6dd3554 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 8ae6295cf7..b2b905e083 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<HttpRequ
new HttpServerResponseEncoder());
}
+ /**
+ * Creates a new instance with the specified decoder options.
+ */
+ public HttpServerCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders,
+ int initialBufferSize, boolean allowDuplicateContentLengths) {
+ init(new HttpServerRequestDecoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders,
+ initialBufferSize, allowDuplicateContentLengths),
+ new HttpServerResponseEncoder());
+ }
+
/**
* Upgrades to another protocol from HTTP. Removes the {@link HttpRequestDecoder} and
* {@link HttpResponseEncoder} from the pipeline.
@@ -99,6 +109,12 @@ public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequ
super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize);
}
+ HttpServerRequestDecoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
+ boolean validateHeaders, int initialBufferSize, boolean allowDuplicateContentLengths) {
+ super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize,
+ allowDuplicateContentLengths);
+ }
+
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> 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<Object[]> 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<String> 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"));
+ }
+}
--
2.23.0