From 94cef1d96b9c5bc2ca7fdbbbd772c743f07a5284 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 14 Apr 2025 21:28:58 +0200 Subject: [PATCH 1/2] Add support for HTTP/1.0 proxies. It is legal to answer an HTTP/1.1 request with an HTTP/1.0 response. Fix #1609. --- docs/project/changelog.rst | 5 +++++ src/websockets/asyncio/client.py | 1 + src/websockets/http11.py | 16 ++++++++++++---- src/websockets/sync/client.py | 1 + tests/test_http11.py | 17 +++++++++++++++++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/docs/project/changelog.rst b/docs/project/changelog.rst index 7e2968826..12fc8c32e 100644 --- a/docs/project/changelog.rst +++ b/docs/project/changelog.rst @@ -32,6 +32,11 @@ notice. *In development* +Improvements +............ + +* Added support for HTTP/1.0 proxies. + 15.0.1 ------ diff --git a/src/websockets/asyncio/client.py b/src/websockets/asyncio/client.py index 38a56ddda..ff9c0cf0f 100644 --- a/src/websockets/asyncio/client.py +++ b/src/websockets/asyncio/client.py @@ -755,6 +755,7 @@ def __init__( self.reader.read_exact, self.reader.read_to_eof, include_body=False, + allow_http10=True, ) loop = asyncio.get_running_loop() diff --git a/src/websockets/http11.py b/src/websockets/http11.py index 177c927fb..a94d72e14 100644 --- a/src/websockets/http11.py +++ b/src/websockets/http11.py @@ -214,6 +214,7 @@ def parse( read_exact: Callable[[int], Generator[None, None, bytes]], read_to_eof: Callable[[int], Generator[None, None, bytes]], include_body: bool = True, + allow_http10: bool = False, ) -> Generator[None, None, Response]: """ Parse a WebSocket handshake response. @@ -249,10 +250,17 @@ def parse( protocol, raw_status_code, raw_reason = status_line.split(b" ", 2) except ValueError: # not enough values to unpack (expected 3, got 1-2) raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None - if protocol != b"HTTP/1.1": - raise ValueError( - f"unsupported protocol; expected HTTP/1.1: {d(status_line)}" - ) + if allow_http10: # some proxies still use HTTP/1.0 + if protocol not in [b"HTTP/1.1", b"HTTP/1.0"]: + raise ValueError( + f"unsupported protocol; expected HTTP/1.1 or HTTP/1.0: " + f"{d(status_line)}" + ) + else: + if protocol != b"HTTP/1.1": + raise ValueError( + f"unsupported protocol; expected HTTP/1.1: {d(status_line)}" + ) try: status_code = int(raw_status_code) except ValueError: # invalid literal for int() with base 10 diff --git a/src/websockets/sync/client.py b/src/websockets/sync/client.py index 58cb84710..0895ad8ea 100644 --- a/src/websockets/sync/client.py +++ b/src/websockets/sync/client.py @@ -499,6 +499,7 @@ def read_connect_response(sock: socket.socket, deadline: Deadline) -> Response: reader.read_exact, reader.read_to_eof, include_body=False, + allow_http10=True, ) try: while True: diff --git a/tests/test_http11.py b/tests/test_http11.py index ca7a1bc86..2c4253f67 100644 --- a/tests/test_http11.py +++ b/tests/test_http11.py @@ -333,6 +333,23 @@ def test_parse_without_body(self): response = self.assertGeneratorReturns(self.parse(include_body=False)) self.assertEqual(response.body, b"") + def test_parse_http10(self): + self.reader.feed_data(b"HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n") + response = self.assertGeneratorReturns(self.parse(allow_http10=True)) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.reason_phrase, "OK") + self.assertEqual(response.body, b"") + + def test_parse_http10_unsupported_protocol(self): + self.reader.feed_data(b"HTTP/1.2 400 Bad Request\r\n\r\n") + with self.assertRaises(ValueError) as raised: + next(self.parse(allow_http10=True)) + self.assertEqual( + str(raised.exception), + "unsupported protocol; expected HTTP/1.1 or HTTP/1.0: " + "HTTP/1.2 400 Bad Request", + ) + def test_serialize(self): # Example from the protocol overview in RFC 6455 response = Response( From 849d4b6ded4d174e26b01e027a1e2369a49be223 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Mon, 21 Apr 2025 07:52:05 +0200 Subject: [PATCH 2/2] Deduplicate include_body and allow_http10. The use case for these parameters is the same. --- src/websockets/asyncio/client.py | 3 +-- src/websockets/http11.py | 11 +++++------ src/websockets/sync/client.py | 3 +-- tests/test_http11.py | 12 ++++++------ 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/websockets/asyncio/client.py b/src/websockets/asyncio/client.py index ff9c0cf0f..aa0d6f774 100644 --- a/src/websockets/asyncio/client.py +++ b/src/websockets/asyncio/client.py @@ -754,8 +754,7 @@ def __init__( self.reader.read_line, self.reader.read_exact, self.reader.read_to_eof, - include_body=False, - allow_http10=True, + proxy=True, ) loop = asyncio.get_running_loop() diff --git a/src/websockets/http11.py b/src/websockets/http11.py index a94d72e14..290ef087e 100644 --- a/src/websockets/http11.py +++ b/src/websockets/http11.py @@ -213,8 +213,7 @@ def parse( read_line: Callable[[int], Generator[None, None, bytes]], read_exact: Callable[[int], Generator[None, None, bytes]], read_to_eof: Callable[[int], Generator[None, None, bytes]], - include_body: bool = True, - allow_http10: bool = False, + proxy: bool = False, ) -> Generator[None, None, Response]: """ Parse a WebSocket handshake response. @@ -250,7 +249,7 @@ def parse( protocol, raw_status_code, raw_reason = status_line.split(b" ", 2) except ValueError: # not enough values to unpack (expected 3, got 1-2) raise ValueError(f"invalid HTTP status line: {d(status_line)}") from None - if allow_http10: # some proxies still use HTTP/1.0 + if proxy: # some proxies still use HTTP/1.0 if protocol not in [b"HTTP/1.1", b"HTTP/1.0"]: raise ValueError( f"unsupported protocol; expected HTTP/1.1 or HTTP/1.0: " @@ -277,12 +276,12 @@ def parse( headers = yield from parse_headers(read_line) - if include_body: + if proxy: + body = b"" + else: body = yield from read_body( status_code, headers, read_line, read_exact, read_to_eof ) - else: - body = b"" return cls(status_code, reason, headers, body) diff --git a/src/websockets/sync/client.py b/src/websockets/sync/client.py index 0895ad8ea..b161523f8 100644 --- a/src/websockets/sync/client.py +++ b/src/websockets/sync/client.py @@ -498,8 +498,7 @@ def read_connect_response(sock: socket.socket, deadline: Deadline) -> Response: reader.read_line, reader.read_exact, reader.read_to_eof, - include_body=False, - allow_http10=True, + proxy=True, ) try: while True: diff --git a/tests/test_http11.py b/tests/test_http11.py index 2c4253f67..3328b3b5e 100644 --- a/tests/test_http11.py +++ b/tests/test_http11.py @@ -328,22 +328,22 @@ def test_parse_body_not_modified(self): response = self.assertGeneratorReturns(self.parse()) self.assertEqual(response.body, b"") - def test_parse_without_body(self): + def test_parse_proxy_response_does_not_read_body(self): self.reader.feed_data(b"HTTP/1.1 200 Connection Established\r\n\r\n") - response = self.assertGeneratorReturns(self.parse(include_body=False)) + response = self.assertGeneratorReturns(self.parse(proxy=True)) self.assertEqual(response.body, b"") - def test_parse_http10(self): + def test_parse_proxy_http10(self): self.reader.feed_data(b"HTTP/1.0 200 OK\r\nContent-Length: 0\r\n\r\n") - response = self.assertGeneratorReturns(self.parse(allow_http10=True)) + response = self.assertGeneratorReturns(self.parse(proxy=True)) self.assertEqual(response.status_code, 200) self.assertEqual(response.reason_phrase, "OK") self.assertEqual(response.body, b"") - def test_parse_http10_unsupported_protocol(self): + def test_parse_proxy_unsupported_protocol(self): self.reader.feed_data(b"HTTP/1.2 400 Bad Request\r\n\r\n") with self.assertRaises(ValueError) as raised: - next(self.parse(allow_http10=True)) + next(self.parse(proxy=True)) self.assertEqual( str(raised.exception), "unsupported protocol; expected HTTP/1.1 or HTTP/1.0: "