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

