Skip to content

Commit

Permalink
Upgrade to llhttp 9.2.1 (#8292) (#8297)
Browse files Browse the repository at this point in the history
Fixes #8291.

(cherry picked from commit 4d72dca)
  • Loading branch information
Dreamsorcerer authored Apr 5, 2024
1 parent 38dd9b8 commit d15f07c
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 19 deletions.
1 change: 1 addition & 0 deletions CHANGES/8292.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Upgraded to LLHTTP 9.2.1, and started rejecting obsolete line folding in Python parser to match -- by :user:`Dreamsorcerer`.
8 changes: 6 additions & 2 deletions aiohttp/http_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,12 @@ def __init__(
max_line_size: int = 8190,
max_headers: int = 32768,
max_field_size: int = 8190,
lax: bool = False,
) -> None:
self.max_line_size = max_line_size
self.max_headers = max_headers
self.max_field_size = max_field_size
self._lax = lax

def parse_headers(
self, lines: List[bytes]
Expand Down Expand Up @@ -178,7 +180,7 @@ def parse_headers(
line = lines[lines_idx]

# consume continuation lines
continuation = line and line[0] in (32, 9) # (' ', '\t')
continuation = self._lax and line and line[0] in (32, 9) # (' ', '\t')

# Deprecated: https://www.rfc-editor.org/rfc/rfc9112.html#name-obsolete-line-folding
if continuation:
Expand Down Expand Up @@ -273,7 +275,9 @@ def __init__(
self._payload_parser: Optional[HttpPayloadParser] = None
self._auto_decompress = auto_decompress
self._limit = limit
self._headers_parser = HeadersParser(max_line_size, max_headers, max_field_size)
self._headers_parser = HeadersParser(
max_line_size, max_headers, max_field_size, self.lax
)

@abc.abstractmethod
def parse_message(self, lines: List[bytes]) -> _MsgT:
Expand Down
65 changes: 49 additions & 16 deletions tests/test_http_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,22 +108,32 @@ def test_c_parser_loaded():

def test_parse_headers(parser: Any) -> None:
text = b"""GET /test HTTP/1.1\r
test: line\r
continue\r
test: a line\r
test2: data\r
\r
"""
messages, upgrade, tail = parser.feed_data(text)
assert len(messages) == 1
msg = messages[0][0]

assert list(msg.headers.items()) == [("test", "line continue"), ("test2", "data")]
assert msg.raw_headers == ((b"test", b"line continue"), (b"test2", b"data"))
assert list(msg.headers.items()) == [("test", "a line"), ("test2", "data")]
assert msg.raw_headers == ((b"test", b"a line"), (b"test2", b"data"))
assert not msg.should_close
assert msg.compression is None
assert not msg.upgrade


def test_reject_obsolete_line_folding(parser: Any) -> None:
text = b"""GET /test HTTP/1.1\r
test: line\r
Content-Length: 48\r
test2: data\r
\r
"""
with pytest.raises(http_exceptions.BadHttpMessage):
parser.feed_data(text)


@pytest.mark.skipif(NO_EXTENSIONS, reason="Only tests C parser.")
def test_invalid_character(loop: Any, protocol: Any, request: Any) -> None:
parser = HttpRequestParserC(
Expand Down Expand Up @@ -353,8 +363,8 @@ def test_parse_delayed(parser) -> None:

def test_headers_multi_feed(parser) -> None:
text1 = b"GET /test HTTP/1.1\r\n"
text2 = b"test: line\r"
text3 = b"\n continue\r\n\r\n"
text2 = b"test: line"
text3 = b" continue\r\n\r\n"

messages, upgrade, tail = parser.feed_data(text1)
assert len(messages) == 0
Expand Down Expand Up @@ -713,31 +723,30 @@ def test_max_header_value_size_under_limit(parser) -> None:


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

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


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

messages, upgrade, tail = parser.feed_data(text)
messages, upgrade, tail = response.feed_data(text)
msg = messages[0][0]
assert msg.method == "GET"
assert msg.path == "/test"
assert msg.code == 200
assert msg.reason == "Ok"
assert msg.version == (1, 1)
assert msg.headers == CIMultiDict({"data": "test " + value.decode()})
assert msg.raw_headers == ((b"data", b"test " + value),)
assert not msg.should_close
# assert not msg.should_close # TODO: https://github.com/nodejs/llhttp/issues/354
assert msg.compression is None
assert not msg.upgrade
assert not msg.chunked
assert msg.url == URL("/test")


def test_http_request_parser(parser) -> None:
Expand Down Expand Up @@ -991,6 +1000,30 @@ def test_http_response_parser_utf8_without_reason(response: Any) -> None:
assert not tail


def test_http_response_parser_obs_line_folding(response: Any) -> None:
text = b"HTTP/1.1 200 Ok\r\ntest: line\r\n continue\r\n\r\n"

messages, upgraded, tail = response.feed_data(text)
assert len(messages) == 1
msg = messages[0][0]

assert msg.version == (1, 1)
assert msg.code == 200
assert msg.reason == "Ok"
assert msg.headers == CIMultiDict([("TEST", "line continue")])
assert msg.raw_headers == ((b"test", b"line continue"),)
assert not upgraded
assert not tail


@pytest.mark.dev_mode
def test_http_response_parser_strict_obs_line_folding(response: Any) -> None:
text = b"HTTP/1.1 200 Ok\r\ntest: line\r\n continue\r\n\r\n"

with pytest.raises(http_exceptions.BadHttpMessage):
response.feed_data(text)


@pytest.mark.parametrize("size", [40962, 8191])
def test_http_response_parser_bad_status_line_too_long(response, size) -> None:
reason = b"t" * (size - 2)
Expand Down

0 comments on commit d15f07c

Please sign in to comment.