Skip to content

Commit 270ae9c

Browse files
[PR #8297/d15f07cf backport][3.9] Upgrade to llhttp 9.2.1 (#8292) (#8298)
**This is a backport of PR #8297 as merged into 3.10 (d15f07c).** Fixes #8291. (cherry picked from commit 4d72dca) Co-authored-by: Sam Bull <[email protected]>
1 parent bb23105 commit 270ae9c

File tree

4 files changed

+57
-19
lines changed

4 files changed

+57
-19
lines changed

CHANGES/8292.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Upgraded to LLHTTP 9.2.1, and started rejecting obsolete line folding in Python parser to match -- by :user:`Dreamsorcerer`.

aiohttp/http_parser.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,12 @@ def __init__(
128128
max_line_size: int = 8190,
129129
max_headers: int = 32768,
130130
max_field_size: int = 8190,
131+
lax: bool = False,
131132
) -> None:
132133
self.max_line_size = max_line_size
133134
self.max_headers = max_headers
134135
self.max_field_size = max_field_size
136+
self._lax = lax
135137

136138
def parse_headers(
137139
self, lines: List[bytes]
@@ -178,7 +180,7 @@ def parse_headers(
178180
line = lines[lines_idx]
179181

180182
# consume continuation lines
181-
continuation = line and line[0] in (32, 9) # (' ', '\t')
183+
continuation = self._lax and line and line[0] in (32, 9) # (' ', '\t')
182184

183185
# Deprecated: https://www.rfc-editor.org/rfc/rfc9112.html#name-obsolete-line-folding
184186
if continuation:
@@ -273,7 +275,9 @@ def __init__(
273275
self._payload_parser: Optional[HttpPayloadParser] = None
274276
self._auto_decompress = auto_decompress
275277
self._limit = limit
276-
self._headers_parser = HeadersParser(max_line_size, max_headers, max_field_size)
278+
self._headers_parser = HeadersParser(
279+
max_line_size, max_headers, max_field_size, self.lax
280+
)
277281

278282
@abc.abstractmethod
279283
def parse_message(self, lines: List[bytes]) -> _MsgT:

tests/test_http_parser.py

+49-16
Original file line numberDiff line numberDiff line change
@@ -108,22 +108,32 @@ def test_c_parser_loaded():
108108

109109
def test_parse_headers(parser: Any) -> None:
110110
text = b"""GET /test HTTP/1.1\r
111-
test: line\r
112-
continue\r
111+
test: a line\r
113112
test2: data\r
114113
\r
115114
"""
116115
messages, upgrade, tail = parser.feed_data(text)
117116
assert len(messages) == 1
118117
msg = messages[0][0]
119118

120-
assert list(msg.headers.items()) == [("test", "line continue"), ("test2", "data")]
121-
assert msg.raw_headers == ((b"test", b"line continue"), (b"test2", b"data"))
119+
assert list(msg.headers.items()) == [("test", "a line"), ("test2", "data")]
120+
assert msg.raw_headers == ((b"test", b"a line"), (b"test2", b"data"))
122121
assert not msg.should_close
123122
assert msg.compression is None
124123
assert not msg.upgrade
125124

126125

126+
def test_reject_obsolete_line_folding(parser: Any) -> None:
127+
text = b"""GET /test HTTP/1.1\r
128+
test: line\r
129+
Content-Length: 48\r
130+
test2: data\r
131+
\r
132+
"""
133+
with pytest.raises(http_exceptions.BadHttpMessage):
134+
parser.feed_data(text)
135+
136+
127137
@pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.")
128138
def test_invalid_character(loop: Any, protocol: Any, request: Any) -> None:
129139
parser = HttpRequestParserC(
@@ -342,8 +352,8 @@ def test_parse_delayed(parser) -> None:
342352

343353
def test_headers_multi_feed(parser) -> None:
344354
text1 = b"GET /test HTTP/1.1\r\n"
345-
text2 = b"test: line\r"
346-
text3 = b"\n continue\r\n\r\n"
355+
text2 = b"test: line"
356+
text3 = b" continue\r\n\r\n"
347357

348358
messages, upgrade, tail = parser.feed_data(text1)
349359
assert len(messages) == 0
@@ -705,31 +715,30 @@ def test_max_header_value_size_under_limit(parser) -> None:
705715

706716

707717
@pytest.mark.parametrize("size", [40965, 8191])
708-
def test_max_header_value_size_continuation(parser, size) -> None:
718+
def test_max_header_value_size_continuation(response, size) -> None:
709719
name = b"T" * (size - 5)
710-
text = b"GET /test HTTP/1.1\r\n" b"data: test\r\n " + name + b"\r\n\r\n"
720+
text = b"HTTP/1.1 200 Ok\r\ndata: test\r\n " + name + b"\r\n\r\n"
711721

712722
match = f"400, message:\n Got more than 8190 bytes \\({size}\\) when reading"
713723
with pytest.raises(http_exceptions.LineTooLong, match=match):
714-
parser.feed_data(text)
724+
response.feed_data(text)
715725

716726

717-
def test_max_header_value_size_continuation_under_limit(parser) -> None:
727+
def test_max_header_value_size_continuation_under_limit(response) -> None:
718728
value = b"A" * 8185
719-
text = b"GET /test HTTP/1.1\r\n" b"data: test\r\n " + value + b"\r\n\r\n"
729+
text = b"HTTP/1.1 200 Ok\r\ndata: test\r\n " + value + b"\r\n\r\n"
720730

721-
messages, upgrade, tail = parser.feed_data(text)
731+
messages, upgrade, tail = response.feed_data(text)
722732
msg = messages[0][0]
723-
assert msg.method == "GET"
724-
assert msg.path == "/test"
733+
assert msg.code == 200
734+
assert msg.reason == "Ok"
725735
assert msg.version == (1, 1)
726736
assert msg.headers == CIMultiDict({"data": "test " + value.decode()})
727737
assert msg.raw_headers == ((b"data", b"test " + value),)
728-
assert not msg.should_close
738+
# assert not msg.should_close # TODO: https://github.com/nodejs/llhttp/issues/354
729739
assert msg.compression is None
730740
assert not msg.upgrade
731741
assert not msg.chunked
732-
assert msg.url == URL("/test")
733742

734743

735744
def test_http_request_parser(parser) -> None:
@@ -970,6 +979,30 @@ def test_http_response_parser_utf8_without_reason(response: Any) -> None:
970979
assert not tail
971980

972981

982+
def test_http_response_parser_obs_line_folding(response: Any) -> None:
983+
text = b"HTTP/1.1 200 Ok\r\ntest: line\r\n continue\r\n\r\n"
984+
985+
messages, upgraded, tail = response.feed_data(text)
986+
assert len(messages) == 1
987+
msg = messages[0][0]
988+
989+
assert msg.version == (1, 1)
990+
assert msg.code == 200
991+
assert msg.reason == "Ok"
992+
assert msg.headers == CIMultiDict([("TEST", "line continue")])
993+
assert msg.raw_headers == ((b"test", b"line continue"),)
994+
assert not upgraded
995+
assert not tail
996+
997+
998+
@pytest.mark.dev_mode
999+
def test_http_response_parser_strict_obs_line_folding(response: Any) -> None:
1000+
text = b"HTTP/1.1 200 Ok\r\ntest: line\r\n continue\r\n\r\n"
1001+
1002+
with pytest.raises(http_exceptions.BadHttpMessage):
1003+
response.feed_data(text)
1004+
1005+
9731006
@pytest.mark.parametrize("size", [40962, 8191])
9741007
def test_http_response_parser_bad_status_line_too_long(response, size) -> None:
9751008
reason = b"t" * (size - 2)

0 commit comments

Comments
 (0)