Skip to content

Commit 5279fbd

Browse files
[Backport 3.13] Tokenize Connection header values in Python HTTP parser (aio-libs#12249) (aio-libs#12256)
1 parent e00ca3c commit 5279fbd

4 files changed

Lines changed: 72 additions & 8 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ repos:
101101
- id: detect-private-key
102102
exclude: ^examples/
103103
- repo: https://github.com/asottile/pyupgrade
104-
rev: 'v3.15.2'
104+
rev: 'v3.21.2'
105105
hooks:
106106
- id: pyupgrade
107107
args: ['--py37-plus']

CHANGES/12249.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Aligned the pure-Python HTTP request parser with the C parser by splitting
2+
comma-separated and repeated ``Connection`` header values for keep-alive,
3+
close, and upgrade handling -- by :user:`rodrigobnogueira`.

aiohttp/http_parser.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -549,16 +549,24 @@ def parse_headers(
549549
if bad_hdr is not None:
550550
raise BadHttpMessage(f"Duplicate '{bad_hdr}' header found.")
551551

552-
# keep-alive
553-
conn = headers.get(hdrs.CONNECTION)
554-
if conn:
555-
v = conn.lower()
556-
if v == "close":
552+
# keep-alive and protocol switching
553+
# RFC 9110 section 7.6.1 defines Connection as a comma-separated list.
554+
conn_values = headers.getall(hdrs.CONNECTION, ())
555+
if conn_values:
556+
conn_tokens = {
557+
token.lower()
558+
for conn_value in conn_values
559+
for token in (part.strip(" \t") for part in conn_value.split(","))
560+
if token and token.isascii()
561+
}
562+
563+
if "close" in conn_tokens:
557564
close_conn = True
558-
elif v == "keep-alive":
565+
elif "keep-alive" in conn_tokens:
559566
close_conn = False
567+
560568
# https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols
561-
elif v == "upgrade" and headers.get(hdrs.UPGRADE):
569+
if "upgrade" in conn_tokens and headers.get(hdrs.UPGRADE):
562570
upgrade = True
563571

564572
# encoding

tests/test_http_parser.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,24 @@ def test_conn_keep_alive_1_1(parser) -> None:
497497
assert not msg.should_close
498498

499499

500+
def test_conn_close_comma_list(parser) -> None:
501+
text = b"GET /test HTTP/1.1\r\nconnection: close, keep-alive\r\n\r\n"
502+
messages, upgrade, tail = parser.feed_data(text)
503+
msg = messages[0][0]
504+
assert msg.should_close
505+
506+
507+
def test_conn_close_multiple_headers(parser) -> None:
508+
text = (
509+
b"GET /test HTTP/1.1\r\n"
510+
b"connection: keep-alive\r\n"
511+
b"connection: close\r\n\r\n"
512+
)
513+
messages, upgrade, tail = parser.feed_data(text)
514+
msg = messages[0][0]
515+
assert msg.should_close
516+
517+
500518
def test_conn_other_1_0(parser) -> None:
501519
text = b"GET /test HTTP/1.0\r\nconnection: test\r\n\r\n"
502520
messages, upgrade, tail = parser.feed_data(text)
@@ -586,6 +604,33 @@ def test_conn_upgrade(parser: Any) -> None:
586604
assert upgrade
587605

588606

607+
def test_conn_upgrade_comma_list(parser) -> None:
608+
text = (
609+
b"GET /test HTTP/1.1\r\n"
610+
b"connection: keep-alive, upgrade\r\n"
611+
b"upgrade: websocket\r\n\r\n"
612+
)
613+
messages, upgrade, tail = parser.feed_data(text)
614+
msg = messages[0][0]
615+
assert not msg.should_close
616+
assert msg.upgrade
617+
assert upgrade
618+
619+
620+
def test_conn_upgrade_multiple_headers(parser) -> None:
621+
text = (
622+
b"GET /test HTTP/1.1\r\n"
623+
b"connection: keep-alive\r\n"
624+
b"connection: upgrade\r\n"
625+
b"upgrade: websocket\r\n\r\n"
626+
)
627+
messages, upgrade, tail = parser.feed_data(text)
628+
msg = messages[0][0]
629+
assert not msg.should_close
630+
assert msg.upgrade
631+
assert upgrade
632+
633+
589634
def test_bad_upgrade(parser) -> None:
590635
"""Test not upgraded if missing Upgrade header."""
591636
text = b"GET /test HTTP/1.1\r\nconnection: upgrade\r\n\r\n"
@@ -956,6 +1001,14 @@ def test_http_request_message_after_close(parser: HttpRequestParser) -> None:
9561001
parser.feed_data(text)
9571002

9581003

1004+
def test_http_request_message_after_close_comma_list(parser: HttpRequestParser) -> None:
1005+
text = b"GET / HTTP/1.1\r\nConnection: close, keep-alive\r\n\r\nInvalid\r\n\r\n"
1006+
with pytest.raises(
1007+
http_exceptions.BadHttpMessage, match="Data after `Connection: close`"
1008+
):
1009+
parser.feed_data(text)
1010+
1011+
9591012
def test_http_request_upgrade(parser: HttpRequestParser) -> None:
9601013
text = (
9611014
b"GET /test HTTP/1.1\r\n"

0 commit comments

Comments
 (0)