From 87b9a5ed1125073a843341fe66a93296da294779 Mon Sep 17 00:00:00 2001 From: Jinseo Vik Lee Date: Wed, 9 Aug 2023 02:41:30 +0900 Subject: [PATCH 1/7] Handle `sni_hostname` extension when SOCKS proxy is activated. --- httpcore/_async/socks_proxy.py | 4 +++- httpcore/_sync/socks_proxy.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/httpcore/_async/socks_proxy.py b/httpcore/_async/socks_proxy.py index f12cb373..08a065d6 100644 --- a/httpcore/_async/socks_proxy.py +++ b/httpcore/_async/socks_proxy.py @@ -216,6 +216,7 @@ def __init__( async def handle_async_request(self, request: Request) -> Response: timeouts = request.extensions.get("timeout", {}) + sni_hostname = request.extensions.get("sni_hostname", None) timeout = timeouts.get("connect", None) async with self._connect_lock: @@ -258,7 +259,8 @@ async def handle_async_request(self, request: Request) -> Response: kwargs = { "ssl_context": ssl_context, - "server_hostname": self._remote_origin.host.decode("ascii"), + "server_hostname": sni_hostname + or self._remote_origin.host.decode("ascii"), "timeout": timeout, } async with Trace("start_tls", logger, request, kwargs) as trace: diff --git a/httpcore/_sync/socks_proxy.py b/httpcore/_sync/socks_proxy.py index 407351d0..502e4d7f 100644 --- a/httpcore/_sync/socks_proxy.py +++ b/httpcore/_sync/socks_proxy.py @@ -216,6 +216,7 @@ def __init__( def handle_request(self, request: Request) -> Response: timeouts = request.extensions.get("timeout", {}) + sni_hostname = request.extensions.get("sni_hostname", None) timeout = timeouts.get("connect", None) with self._connect_lock: @@ -258,7 +259,8 @@ def handle_request(self, request: Request) -> Response: kwargs = { "ssl_context": ssl_context, - "server_hostname": self._remote_origin.host.decode("ascii"), + "server_hostname": sni_hostname + or self._remote_origin.host.decode("ascii"), "timeout": timeout, } with Trace("start_tls", logger, request, kwargs) as trace: From 704d7d077d60d93e3a918ff4423f1bbcbee7a755 Mon Sep 17 00:00:00 2001 From: Jinseo Vik Lee Date: Wed, 9 Aug 2023 02:44:08 +0900 Subject: [PATCH 2/7] Add tests. --- tests/_async/test_socks_proxy.py | 63 ++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/_async/test_socks_proxy.py b/tests/_async/test_socks_proxy.py index 3f5dd1cc..5d2382b5 100644 --- a/tests/_async/test_socks_proxy.py +++ b/tests/_async/test_socks_proxy.py @@ -191,3 +191,66 @@ async def test_socks5_request_incorrect_auth(): assert str(exc_info.value) == "Invalid username/password" assert not proxy.connections + + +@pytest.mark.anyio +async def test_socks5_sni_hostname(): + """ + Send an HTTP request via a SOCKS proxy utilizing `sni_hostname` extension. + """ + network_backend = httpcore.AsyncMockBackend( + [ + # The initial socks CONNECT + # v5 NOAUTH + b"\x05\x00", + # v5 SUC RSV IP4 127 .0 .0 .1 :80 + b"\x05\x00\x00\x01\xff\x00\x00\x01\x00\x50", + # The actual response from the remote server + b"HTTP/1.1 200 OK\r\n", + b"Content-Type: plain/text\r\n", + b"Content-Length: 13\r\n", + b"\r\n", + b"Hello, world!", + ] + ) + + async with httpcore.AsyncSOCKSProxy( + proxy_url="socks5://localhost:8080/", + network_backend=network_backend, + ) as proxy: + # Sending an intial request, which once complete will return to the pool, IDLE. + async with proxy.stream( + "GET", "https://93.184.216.34/", + headers=[(b'Host', 'example.com')], + extensions={'sni_hostname': 'example.com'} + ) as response: + + info = [repr(c) for c in proxy.connections] + assert info == [ + "" + ] + await response.aread() + + assert response.status == 200 + assert response.content == b"Hello, world!" + info = [repr(c) for c in proxy.connections] + assert info == [ + "" + ] + assert proxy.connections[0].is_idle() + assert proxy.connections[0].is_available() + assert not proxy.connections[0].is_closed() + + # A connection on a tunneled proxy can only handle HTTPS requests to the same origin. + assert not proxy.connections[0].can_handle_request( + httpcore.Origin(b"http", b"93.184.216.34", 80) + ) + assert not proxy.connections[0].can_handle_request( + httpcore.Origin(b"http", b"other.com", 80) + ) + assert proxy.connections[0].can_handle_request( + httpcore.Origin(b"https", b"93.184.216.34", 443) + ) + assert not proxy.connections[0].can_handle_request( + httpcore.Origin(b"https", b"other.com", 443) + ) From e6b274c8a7c0523ee73a3d7e3e3f9ac43381cfb2 Mon Sep 17 00:00:00 2001 From: Jinseo Vik Lee Date: Wed, 9 Aug 2023 02:51:50 +0900 Subject: [PATCH 3/7] Reformat the test. --- tests/_async/test_socks_proxy.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/_async/test_socks_proxy.py b/tests/_async/test_socks_proxy.py index 5d2382b5..42f75b34 100644 --- a/tests/_async/test_socks_proxy.py +++ b/tests/_async/test_socks_proxy.py @@ -220,10 +220,11 @@ async def test_socks5_sni_hostname(): ) as proxy: # Sending an intial request, which once complete will return to the pool, IDLE. async with proxy.stream( - "GET", "https://93.184.216.34/", - headers=[(b'Host', 'example.com')], - extensions={'sni_hostname': 'example.com'} - ) as response: + "GET", + "https://93.184.216.34/", + headers=[(b"Host", "example.com")], + extensions={"sni_hostname": "example.com"}, + ) as response: info = [repr(c) for c in proxy.connections] assert info == [ From d3a801c14b22571cccf3c37fc6ffa3d0e2436914 Mon Sep 17 00:00:00 2001 From: Jinseo Vik Lee Date: Wed, 9 Aug 2023 02:59:15 +0900 Subject: [PATCH 4/7] Run linting checks locally and reformat again. --- tests/_async/test_socks_proxy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/_async/test_socks_proxy.py b/tests/_async/test_socks_proxy.py index 42f75b34..b0da0f6b 100644 --- a/tests/_async/test_socks_proxy.py +++ b/tests/_async/test_socks_proxy.py @@ -225,7 +225,6 @@ async def test_socks5_sni_hostname(): headers=[(b"Host", "example.com")], extensions={"sni_hostname": "example.com"}, ) as response: - info = [repr(c) for c in proxy.connections] assert info == [ "" From 6fd26415223b1247214b28eabda505625348475a Mon Sep 17 00:00:00 2001 From: Jinseo Vik Lee Date: Wed, 9 Aug 2023 13:17:29 +0900 Subject: [PATCH 5/7] Update changelog and add a missing test. --- CHANGELOG.md | 1 + tests/_sync/test_socks_proxy.py | 63 +++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd18429..4d0ad2c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Unreleased +- Handle `sni_hostname` extension with SOCKS proxy. (#774) - Change the type of `Extensions` from `Mapping[Str, Any]` to `MutableMapping[Str, Any]`. (#762) - Handle HTTP/1.1 half-closed connections gracefully. (#641) diff --git a/tests/_sync/test_socks_proxy.py b/tests/_sync/test_socks_proxy.py index 2d39bb97..4157a781 100644 --- a/tests/_sync/test_socks_proxy.py +++ b/tests/_sync/test_socks_proxy.py @@ -191,3 +191,66 @@ def test_socks5_request_incorrect_auth(): assert str(exc_info.value) == "Invalid username/password" assert not proxy.connections + + + +def test_socks5_sni_hostname(): + """ + Send an HTTP request via a SOCKS proxy utilizing `sni_hostname` extension. + """ + network_backend = httpcore.MockBackend( + [ + # The initial socks CONNECT + # v5 NOAUTH + b"\x05\x00", + # v5 SUC RSV IP4 127 .0 .0 .1 :80 + b"\x05\x00\x00\x01\xff\x00\x00\x01\x00\x50", + # The actual response from the remote server + b"HTTP/1.1 200 OK\r\n", + b"Content-Type: plain/text\r\n", + b"Content-Length: 13\r\n", + b"\r\n", + b"Hello, world!", + ] + ) + + with httpcore.SOCKSProxy( + proxy_url="socks5://localhost:8080/", + network_backend=network_backend, + ) as proxy: + # Sending an intial request, which once complete will return to the pool, IDLE. + with proxy.stream( + "GET", + "https://93.184.216.34/", + headers=[(b"Host", "example.com")], + extensions={"sni_hostname": "example.com"}, + ) as response: + info = [repr(c) for c in proxy.connections] + assert info == [ + "" + ] + response.read() + + assert response.status == 200 + assert response.content == b"Hello, world!" + info = [repr(c) for c in proxy.connections] + assert info == [ + "" + ] + assert proxy.connections[0].is_idle() + assert proxy.connections[0].is_available() + assert not proxy.connections[0].is_closed() + + # A connection on a tunneled proxy can only handle HTTPS requests to the same origin. + assert not proxy.connections[0].can_handle_request( + httpcore.Origin(b"http", b"93.184.216.34", 80) + ) + assert not proxy.connections[0].can_handle_request( + httpcore.Origin(b"http", b"other.com", 80) + ) + assert proxy.connections[0].can_handle_request( + httpcore.Origin(b"https", b"93.184.216.34", 443) + ) + assert not proxy.connections[0].can_handle_request( + httpcore.Origin(b"https", b"other.com", 443) + ) From 35729734712c1d7b697e9ac85ddb3fa39fe12141 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 1 Sep 2023 13:00:57 +0100 Subject: [PATCH 6/7] Update tests/_async/test_socks_proxy.py --- tests/_async/test_socks_proxy.py | 63 -------------------------------- 1 file changed, 63 deletions(-) diff --git a/tests/_async/test_socks_proxy.py b/tests/_async/test_socks_proxy.py index b0da0f6b..3f5dd1cc 100644 --- a/tests/_async/test_socks_proxy.py +++ b/tests/_async/test_socks_proxy.py @@ -191,66 +191,3 @@ async def test_socks5_request_incorrect_auth(): assert str(exc_info.value) == "Invalid username/password" assert not proxy.connections - - -@pytest.mark.anyio -async def test_socks5_sni_hostname(): - """ - Send an HTTP request via a SOCKS proxy utilizing `sni_hostname` extension. - """ - network_backend = httpcore.AsyncMockBackend( - [ - # The initial socks CONNECT - # v5 NOAUTH - b"\x05\x00", - # v5 SUC RSV IP4 127 .0 .0 .1 :80 - b"\x05\x00\x00\x01\xff\x00\x00\x01\x00\x50", - # The actual response from the remote server - b"HTTP/1.1 200 OK\r\n", - b"Content-Type: plain/text\r\n", - b"Content-Length: 13\r\n", - b"\r\n", - b"Hello, world!", - ] - ) - - async with httpcore.AsyncSOCKSProxy( - proxy_url="socks5://localhost:8080/", - network_backend=network_backend, - ) as proxy: - # Sending an intial request, which once complete will return to the pool, IDLE. - async with proxy.stream( - "GET", - "https://93.184.216.34/", - headers=[(b"Host", "example.com")], - extensions={"sni_hostname": "example.com"}, - ) as response: - info = [repr(c) for c in proxy.connections] - assert info == [ - "" - ] - await response.aread() - - assert response.status == 200 - assert response.content == b"Hello, world!" - info = [repr(c) for c in proxy.connections] - assert info == [ - "" - ] - assert proxy.connections[0].is_idle() - assert proxy.connections[0].is_available() - assert not proxy.connections[0].is_closed() - - # A connection on a tunneled proxy can only handle HTTPS requests to the same origin. - assert not proxy.connections[0].can_handle_request( - httpcore.Origin(b"http", b"93.184.216.34", 80) - ) - assert not proxy.connections[0].can_handle_request( - httpcore.Origin(b"http", b"other.com", 80) - ) - assert proxy.connections[0].can_handle_request( - httpcore.Origin(b"https", b"93.184.216.34", 443) - ) - assert not proxy.connections[0].can_handle_request( - httpcore.Origin(b"https", b"other.com", 443) - ) From 2516328be815f5ba89bf96c4c3131bec4b659adf Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 1 Sep 2023 13:01:22 +0100 Subject: [PATCH 7/7] Update tests/_sync/test_socks_proxy.py --- tests/_sync/test_socks_proxy.py | 63 --------------------------------- 1 file changed, 63 deletions(-) diff --git a/tests/_sync/test_socks_proxy.py b/tests/_sync/test_socks_proxy.py index 4157a781..2d39bb97 100644 --- a/tests/_sync/test_socks_proxy.py +++ b/tests/_sync/test_socks_proxy.py @@ -191,66 +191,3 @@ def test_socks5_request_incorrect_auth(): assert str(exc_info.value) == "Invalid username/password" assert not proxy.connections - - - -def test_socks5_sni_hostname(): - """ - Send an HTTP request via a SOCKS proxy utilizing `sni_hostname` extension. - """ - network_backend = httpcore.MockBackend( - [ - # The initial socks CONNECT - # v5 NOAUTH - b"\x05\x00", - # v5 SUC RSV IP4 127 .0 .0 .1 :80 - b"\x05\x00\x00\x01\xff\x00\x00\x01\x00\x50", - # The actual response from the remote server - b"HTTP/1.1 200 OK\r\n", - b"Content-Type: plain/text\r\n", - b"Content-Length: 13\r\n", - b"\r\n", - b"Hello, world!", - ] - ) - - with httpcore.SOCKSProxy( - proxy_url="socks5://localhost:8080/", - network_backend=network_backend, - ) as proxy: - # Sending an intial request, which once complete will return to the pool, IDLE. - with proxy.stream( - "GET", - "https://93.184.216.34/", - headers=[(b"Host", "example.com")], - extensions={"sni_hostname": "example.com"}, - ) as response: - info = [repr(c) for c in proxy.connections] - assert info == [ - "" - ] - response.read() - - assert response.status == 200 - assert response.content == b"Hello, world!" - info = [repr(c) for c in proxy.connections] - assert info == [ - "" - ] - assert proxy.connections[0].is_idle() - assert proxy.connections[0].is_available() - assert not proxy.connections[0].is_closed() - - # A connection on a tunneled proxy can only handle HTTPS requests to the same origin. - assert not proxy.connections[0].can_handle_request( - httpcore.Origin(b"http", b"93.184.216.34", 80) - ) - assert not proxy.connections[0].can_handle_request( - httpcore.Origin(b"http", b"other.com", 80) - ) - assert proxy.connections[0].can_handle_request( - httpcore.Origin(b"https", b"93.184.216.34", 443) - ) - assert not proxy.connections[0].can_handle_request( - httpcore.Origin(b"https", b"other.com", 443) - )