From c77954062a1e1c846414012bdb488399c7f5feeb Mon Sep 17 00:00:00 2001 From: Stephen Brown II Date: Thu, 9 Jan 2020 11:59:50 -0700 Subject: [PATCH 1/3] Normalize Content-Length into a single value if duplicated exactly --- h11/_headers.py | 11 +++++++---- h11/tests/test_headers.py | 7 +++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/h11/_headers.py b/h11/_headers.py index 294df8f..aaa90a4 100644 --- a/h11/_headers.py +++ b/h11/_headers.py @@ -118,7 +118,7 @@ def raw_items(self): def normalize_and_validate(headers, _parsed=False): new_headers = [] - saw_content_length = False + saw_content_length = None saw_transfer_encoding = False for name, value in headers: # For headers coming out of the parser, we can safely skip some steps, @@ -132,10 +132,13 @@ def normalize_and_validate(headers, _parsed=False): raw_name = name name = name.lower() if name == b"content-length": - if saw_content_length: - raise LocalProtocolError("multiple Content-Length headers") validate(_content_length_re, value, "bad Content-Length") - saw_content_length = True + if saw_content_length: + if saw_content_length != value: + raise LocalProtocolError("conflicting Content-Length headers") + # We have a duplicate Content-Length, so collapse it + new_headers.remove((name, saw_content_length)) + saw_content_length = value if name == b"transfer-encoding": # "A server that receives a request message with a transfer coding # it does not understand SHOULD respond with 501 (Not diff --git a/h11/tests/test_headers.py b/h11/tests/test_headers.py index 5c16102..10f5d8b 100644 --- a/h11/tests/test_headers.py +++ b/h11/tests/test_headers.py @@ -54,6 +54,13 @@ def test_normalize_and_validate(): normalize_and_validate([("Content-Length", "1x")]) with pytest.raises(LocalProtocolError): normalize_and_validate([("Content-Length", "1"), ("Content-Length", "2")]) + assert normalize_and_validate( + [("Content-Length", "1"), ("Content-Length", "1")] + ) == [(b"content-length", b"1")] + with pytest.raises(LocalProtocolError): + normalize_and_validate( + [("Content-Length", "1"), ("Content-Length", "1"), ("Content-Length", "2")] + ) # transfer-encoding assert normalize_and_validate([("Transfer-Encoding", "chunked")]) == [ From 4ca69c356b9ae7843337b185925a46d21e8848a3 Mon Sep 17 00:00:00 2001 From: pgjones Date: Sun, 4 Oct 2020 13:37:20 +0100 Subject: [PATCH 2/3] Minor fixes to previous commit This ensures that duplicate content length headers with a value of b"0" are correctly considered. --- h11/_headers.py | 18 ++++++++++-------- h11/tests/test_headers.py | 4 ++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/h11/_headers.py b/h11/_headers.py index aaa90a4..04f988b 100644 --- a/h11/_headers.py +++ b/h11/_headers.py @@ -118,7 +118,7 @@ def raw_items(self): def normalize_and_validate(headers, _parsed=False): new_headers = [] - saw_content_length = None + seen_content_length = None saw_transfer_encoding = False for name, value in headers: # For headers coming out of the parser, we can safely skip some steps, @@ -133,13 +133,13 @@ def normalize_and_validate(headers, _parsed=False): name = name.lower() if name == b"content-length": validate(_content_length_re, value, "bad Content-Length") - if saw_content_length: - if saw_content_length != value: + if seen_content_length is None: + seen_content_length = value + new_headers.append((raw_name, name, value)) + else: + if seen_content_length != value: raise LocalProtocolError("conflicting Content-Length headers") - # We have a duplicate Content-Length, so collapse it - new_headers.remove((name, saw_content_length)) - saw_content_length = value - if name == b"transfer-encoding": + elif name == b"transfer-encoding": # "A server that receives a request message with a transfer coding # it does not understand SHOULD respond with 501 (Not # Implemented)." @@ -157,7 +157,9 @@ def normalize_and_validate(headers, _parsed=False): error_status_hint=501, ) saw_transfer_encoding = True - new_headers.append((raw_name, name, value)) + new_headers.append((raw_name, name, value)) + else: + new_headers.append((raw_name, name, value)) return Headers(new_headers) diff --git a/h11/tests/test_headers.py b/h11/tests/test_headers.py index 10f5d8b..6b51e0b 100644 --- a/h11/tests/test_headers.py +++ b/h11/tests/test_headers.py @@ -55,8 +55,8 @@ def test_normalize_and_validate(): with pytest.raises(LocalProtocolError): normalize_and_validate([("Content-Length", "1"), ("Content-Length", "2")]) assert normalize_and_validate( - [("Content-Length", "1"), ("Content-Length", "1")] - ) == [(b"content-length", b"1")] + [("Content-Length", "0"), ("Content-Length", "0")] + ) == [(b"content-length", b"0")] with pytest.raises(LocalProtocolError): normalize_and_validate( [("Content-Length", "1"), ("Content-Length", "1"), ("Content-Length", "2")] From b53ff2a4f5e02f2e49ecebcee91d9d91386095a2 Mon Sep 17 00:00:00 2001 From: pgjones Date: Sun, 4 Oct 2020 13:57:39 +0100 Subject: [PATCH 3/3] Normalize Content-Length into a single value If duplicated exactly when merged into a comma seperated list (the unmerged case is handled already). This is allowed as per RFC 7230 Section 3.3.2. --- h11/_headers.py | 9 ++++++--- h11/tests/test_headers.py | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/h11/_headers.py b/h11/_headers.py index 04f988b..5229ac4 100644 --- a/h11/_headers.py +++ b/h11/_headers.py @@ -132,13 +132,16 @@ def normalize_and_validate(headers, _parsed=False): raw_name = name name = name.lower() if name == b"content-length": + lengths = set(length.strip() for length in value.split(b",")) + if len(lengths) != 1: + raise LocalProtocolError("conflicting Content-Length headers") + value = lengths.pop() validate(_content_length_re, value, "bad Content-Length") if seen_content_length is None: seen_content_length = value new_headers.append((raw_name, name, value)) - else: - if seen_content_length != value: - raise LocalProtocolError("conflicting Content-Length headers") + elif seen_content_length != value: + raise LocalProtocolError("conflicting Content-Length headers") elif name == b"transfer-encoding": # "A server that receives a request message with a transfer coding # it does not understand SHOULD respond with 501 (Not diff --git a/h11/tests/test_headers.py b/h11/tests/test_headers.py index 6b51e0b..ff3dc8d 100644 --- a/h11/tests/test_headers.py +++ b/h11/tests/test_headers.py @@ -57,10 +57,15 @@ def test_normalize_and_validate(): assert normalize_and_validate( [("Content-Length", "0"), ("Content-Length", "0")] ) == [(b"content-length", b"0")] + assert normalize_and_validate([("Content-Length", "0 , 0")]) == [ + (b"content-length", b"0") + ] with pytest.raises(LocalProtocolError): normalize_and_validate( [("Content-Length", "1"), ("Content-Length", "1"), ("Content-Length", "2")] ) + with pytest.raises(LocalProtocolError): + normalize_and_validate([("Content-Length", "1 , 1,2")]) # transfer-encoding assert normalize_and_validate([("Transfer-Encoding", "chunked")]) == [