Repository: python-websockets/websockets

Issue:
ValueError: unsupported protocol with HTTP/1.0 proxy server
When using websockets 15.0.1 to establish a connection through a HTTP/1.0 only proxy server, I get the following traceback:
```
venv/lib64/python3.11/site-packages/websockets/asyncio/client.py:765: in run_parser
    next(self.parser)
venv/lib64/python3.11/site-packages/websockets/http11.py:250: in parse
    raise ValueError(
E   ValueError: unsupported protocol; expected HTTP/1.1: HTTP/1.0 200 Connection Established

The above exception was the direct cause of the following exception:
tests/test_notification.py:31: in test_websocket
    async with websockets.connect(uri, origin = gateway_url, additional_headers = additional_headers, ssl = ssl_context, proxy = proxy) as websocket:
venv/lib64/python3.11/site-packages/websockets/asyncio/client.py:587: in __aenter__
    return await self
venv/lib64/python3.11/site-packages/websockets/asyncio/client.py:541: in __await_impl__
    self.connection = await self.create_connection()
venv/lib64/python3.11/site-packages/websockets/asyncio/client.py:440: in create_connection
    transport = await connect_http_proxy(
venv/lib64/python3.11/site-packages/websockets/asyncio/client.py:815: in connect_http_proxy
    await protocol.response
E   websockets.exceptions.InvalidProxyMessage: did not receive a valid HTTP response from proxy
```

Manually checking the behavior of the proxy server:
```bash
$ nc proxy-host.com 3128
CONNECT websocket-host.com:443 HTTP/1.1
Host: websocket-host.com

HTTP/1.0 200 Connection Established
```

It is my understanding [reading the spec](https://httpwg.org/specs/rfc9112.html#http.version) that responding with HTTP/1.0 to a HTTP/1.1 request is allowed.

Hints from issue comments:
@aaugustin: This is a valid improvement request (although I didn't really want to become an expert in HTTP proxies when I started this library...)

Out of curiosity, in what environment are you hitting this problem? Do you know what proxy is being used?@bgK: > Out of curiosity, in what environment are you hitting this problem? Do you know what proxy is being used?

The proxy server is very much out of my reach, but looks like McAfee Web Gateway / Skyhigh Secure Web Gateway.

@aaugustin: OK - looks like a typical corporate environment - clearly in scope of what I wanted to support when I implemented support for HTTP proxies. I'll do something about it.@aaugustin: Hello, would you be able to confirm that #1618 fixes your problem, please?

Solution patch:
diff --git a/docs/project/changelog.rst b/docs/project/changelog.rst
index 7e29688..12fc8c3 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 38a56dd..aa0d6f7 100644
--- a/src/websockets/asyncio/client.py
+++ b/src/websockets/asyncio/client.py
@@ -754,7 +754,7 @@ class HTTPProxyConnection(asyncio.Protocol):
             self.reader.read_line,
             self.reader.read_exact,
             self.reader.read_to_eof,
-            include_body=False,
+            proxy=True,
         )
 
         loop = asyncio.get_running_loop()
diff --git a/src/websockets/http11.py b/src/websockets/http11.py
index 177c927..290ef08 100644
--- a/src/websockets/http11.py
+++ b/src/websockets/http11.py
@@ -213,7 +213,7 @@ class Response:
         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,
+        proxy: bool = False,
     ) -> Generator[None, None, Response]:
         """
         Parse a WebSocket handshake response.
@@ -249,10 +249,17 @@ class Response:
             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 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: "
+                    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
@@ -269,12 +276,12 @@ class Response:
 
         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 58cb847..b161523 100644
--- a/src/websockets/sync/client.py
+++ b/src/websockets/sync/client.py
@@ -498,7 +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,
+        proxy=True,
     )
     try:
         while True:


Test patch:
diff --git a/tests/test_http11.py b/tests/test_http11.py
index ca7a1bc..3328b3b 100644
--- a/tests/test_http11.py
+++ b/tests/test_http11.py
@@ -328,11 +328,28 @@ class ResponseTests(GeneratorTestCase):
         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_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(proxy=True))
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.reason_phrase, "OK")
+        self.assertEqual(response.body, b"")
+
+    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(proxy=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(