Skip to content

Commit 4d72dca

Browse files
Upgrade to llhttp 9.2.1 (#8292)
Fixes #8291.
1 parent 54e13b0 commit 4d72dca

File tree

4 files changed

+55
-22
lines changed

4 files changed

+55
-22
lines changed

CHANGES/8292.feature.rst

Lines changed: 1 addition & 0 deletions
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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,12 +123,11 @@ class ChunkState(IntEnum):
123123

124124
class HeadersParser:
125125
def __init__(
126-
self,
127-
max_line_size: int = 8190,
128-
max_field_size: int = 8190,
126+
self, max_line_size: int = 8190, max_field_size: int = 8190, lax: bool = False
129127
) -> None:
130128
self.max_line_size = max_line_size
131129
self.max_field_size = max_field_size
130+
self._lax = lax
132131

133132
def parse_headers(
134133
self, lines: List[bytes]
@@ -175,7 +174,7 @@ def parse_headers(
175174
line = lines[lines_idx]
176175

177176
# consume continuation lines
178-
continuation = line and line[0] in (32, 9) # (' ', '\t')
177+
continuation = self._lax and line and line[0] in (32, 9) # (' ', '\t')
179178

180179
# Deprecated: https://www.rfc-editor.org/rfc/rfc9112.html#name-obsolete-line-folding
181180
if continuation:
@@ -268,7 +267,7 @@ def __init__(
268267
self._payload_parser: Optional[HttpPayloadParser] = None
269268
self._auto_decompress = auto_decompress
270269
self._limit = limit
271-
self._headers_parser = HeadersParser(max_line_size, max_field_size)
270+
self._headers_parser = HeadersParser(max_line_size, max_field_size, self.lax)
272271

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

tests/test_http_parser.py

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -107,22 +107,32 @@ def test_c_parser_loaded():
107107

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

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

125124

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

353363
def test_headers_multi_feed(parser: Any) -> None:
354364
text1 = b"GET /test HTTP/1.1\r\n"
355-
text2 = b"test: line\r"
356-
text3 = b"\n continue\r\n\r\n"
365+
text2 = b"test: line"
366+
text3 = b" continue\r\n\r\n"
357367

358368
messages, upgrade, tail = parser.feed_data(text1)
359369
assert len(messages) == 0
@@ -714,31 +724,30 @@ def test_max_header_value_size_under_limit(parser: Any) -> None:
714724

715725

716726
@pytest.mark.parametrize("size", [40965, 8191])
717-
def test_max_header_value_size_continuation(parser: Any, size: Any) -> None:
727+
def test_max_header_value_size_continuation(response: Any, size: Any) -> None:
718728
name = b"T" * (size - 5)
719-
text = b"GET /test HTTP/1.1\r\n" b"data: test\r\n " + name + b"\r\n\r\n"
729+
text = b"HTTP/1.1 200 Ok\r\ndata: test\r\n " + name + b"\r\n\r\n"
720730

721731
match = f"400, message:\n Got more than 8190 bytes \\({size}\\) when reading"
722732
with pytest.raises(http_exceptions.LineTooLong, match=match):
723-
parser.feed_data(text)
733+
response.feed_data(text)
724734

725735

726-
def test_max_header_value_size_continuation_under_limit(parser: Any) -> None:
736+
def test_max_header_value_size_continuation_under_limit(response: Any) -> None:
727737
value = b"A" * 8185
728-
text = b"GET /test HTTP/1.1\r\n" b"data: test\r\n " + value + b"\r\n\r\n"
738+
text = b"HTTP/1.1 200 Ok\r\ndata: test\r\n " + value + b"\r\n\r\n"
729739

730-
messages, upgrade, tail = parser.feed_data(text)
740+
messages, upgrade, tail = response.feed_data(text)
731741
msg = messages[0][0]
732-
assert msg.method == "GET"
733-
assert msg.path == "/test"
742+
assert msg.code == 200
743+
assert msg.reason == "Ok"
734744
assert msg.version == (1, 1)
735745
assert msg.headers == CIMultiDict({"data": "test " + value.decode()})
736746
assert msg.raw_headers == ((b"data", b"test " + value),)
737-
assert not msg.should_close
747+
# assert not msg.should_close # TODO: https://github.com/nodejs/llhttp/issues/354
738748
assert msg.compression is None
739749
assert not msg.upgrade
740750
assert not msg.chunked
741-
assert msg.url == URL("/test")
742751

743752

744753
def test_http_request_parser(parser: Any) -> None:
@@ -992,6 +1001,30 @@ def test_http_response_parser_utf8_without_reason(response: Any) -> None:
9921001
assert not tail
9931002

9941003

1004+
def test_http_response_parser_obs_line_folding(response: Any) -> None:
1005+
text = b"HTTP/1.1 200 Ok\r\ntest: line\r\n continue\r\n\r\n"
1006+
1007+
messages, upgraded, tail = response.feed_data(text)
1008+
assert len(messages) == 1
1009+
msg = messages[0][0]
1010+
1011+
assert msg.version == (1, 1)
1012+
assert msg.code == 200
1013+
assert msg.reason == "Ok"
1014+
assert msg.headers == CIMultiDict([("TEST", "line continue")])
1015+
assert msg.raw_headers == ((b"test", b"line continue"),)
1016+
assert not upgraded
1017+
assert not tail
1018+
1019+
1020+
@pytest.mark.dev_mode
1021+
def test_http_response_parser_strict_obs_line_folding(response: Any) -> None:
1022+
text = b"HTTP/1.1 200 Ok\r\ntest: line\r\n continue\r\n\r\n"
1023+
1024+
with pytest.raises(http_exceptions.BadHttpMessage):
1025+
response.feed_data(text)
1026+
1027+
9951028
@pytest.mark.parametrize("size", [40962, 8191])
9961029
def test_http_response_parser_bad_status_line_too_long(
9971030
response: Any, size: Any

0 commit comments

Comments
 (0)