Skip to content

Commit d127987

Browse files
committed
Fix: Pass timeout to SOCKS5 handshake to prevent hanging
1 parent 10a6582 commit d127987

2 files changed

Lines changed: 18 additions & 68 deletions

File tree

httpcore/_async/socks_proxy.py

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ async def _init_socks5_connection(
4545
host: bytes,
4646
port: int,
4747
auth: tuple[bytes, bytes] | None = None,
48+
timeout: float | None = None, # <--- FIX 1: Add timeout argument
4849
) -> None:
4950
conn = socksio.socks5.SOCKS5Connection()
5051

@@ -56,10 +57,10 @@ async def _init_socks5_connection(
5657
)
5758
conn.send(socksio.socks5.SOCKS5AuthMethodsRequest([auth_method]))
5859
outgoing_bytes = conn.data_to_send()
59-
await stream.write(outgoing_bytes)
60+
await stream.write(outgoing_bytes, timeout=timeout) # <--- FIX 2: Pass timeout
6061

6162
# Auth method response
62-
incoming_bytes = await stream.read(max_bytes=4096)
63+
incoming_bytes = await stream.read(max_bytes=4096, timeout=timeout) # <--- FIX 3: Pass timeout
6364
response = conn.receive_data(incoming_bytes)
6465
assert isinstance(response, socksio.socks5.SOCKS5AuthReply)
6566
if response.method != auth_method:
@@ -75,10 +76,10 @@ async def _init_socks5_connection(
7576
username, password = auth
7677
conn.send(socksio.socks5.SOCKS5UsernamePasswordRequest(username, password))
7778
outgoing_bytes = conn.data_to_send()
78-
await stream.write(outgoing_bytes)
79+
await stream.write(outgoing_bytes, timeout=timeout) # <--- FIX 4: Pass timeout
7980

8081
# Username/password response
81-
incoming_bytes = await stream.read(max_bytes=4096)
82+
incoming_bytes = await stream.read(max_bytes=4096, timeout=timeout) # <--- FIX 5: Pass timeout
8283
response = conn.receive_data(incoming_bytes)
8384
assert isinstance(response, socksio.socks5.SOCKS5UsernamePasswordReply)
8485
if not response.success:
@@ -91,10 +92,10 @@ async def _init_socks5_connection(
9192
)
9293
)
9394
outgoing_bytes = conn.data_to_send()
94-
await stream.write(outgoing_bytes)
95+
await stream.write(outgoing_bytes, timeout=timeout) # <--- FIX 6: Pass timeout
9596

9697
# Connect response
97-
incoming_bytes = await stream.read(max_bytes=4096)
98+
incoming_bytes = await stream.read(max_bytes=4096, timeout=timeout) # <--- FIX 7: Pass timeout
9899
response = conn.receive_data(incoming_bytes)
99100
assert isinstance(response, socksio.socks5.SOCKS5Reply)
100101
if response.reply_code != socksio.socks5.SOCKS5ReplyCode.SUCCEEDED:
@@ -122,33 +123,6 @@ def __init__(
122123
) -> None:
123124
"""
124125
A connection pool for making HTTP requests.
125-
126-
Parameters:
127-
proxy_url: The URL to use when connecting to the proxy server.
128-
For example `"http://127.0.0.1:8080/"`.
129-
ssl_context: An SSL context to use for verifying connections.
130-
If not specified, the default `httpcore.default_ssl_context()`
131-
will be used.
132-
max_connections: The maximum number of concurrent HTTP connections that
133-
the pool should allow. Any attempt to send a request on a pool that
134-
would exceed this amount will block until a connection is available.
135-
max_keepalive_connections: The maximum number of idle HTTP connections
136-
that will be maintained in the pool.
137-
keepalive_expiry: The duration in seconds that an idle HTTP connection
138-
may be maintained for before being expired from the pool.
139-
http1: A boolean indicating if HTTP/1.1 requests should be supported
140-
by the connection pool. Defaults to True.
141-
http2: A boolean indicating if HTTP/2 requests should be supported by
142-
the connection pool. Defaults to False.
143-
retries: The maximum number of retries when trying to establish
144-
a connection.
145-
local_address: Local address to connect from. Can also be used to
146-
connect using a particular address family. Using
147-
`local_address="0.0.0.0"` will connect using an `AF_INET` address
148-
(IPv4), while using `local_address="::"` will connect using an
149-
`AF_INET6` address (IPv6).
150-
uds: Path to a Unix Domain Socket to use instead of TCP sockets.
151-
network_backend: A backend instance to use for handling network I/O.
152126
"""
153127
super().__init__(
154128
ssl_context=ssl_context,
@@ -237,6 +211,7 @@ async def handle_async_request(self, request: Request) -> Response:
237211
"host": self._remote_origin.host.decode("ascii"),
238212
"port": self._remote_origin.port,
239213
"auth": self._proxy_auth,
214+
"timeout": timeout, # <--- FIX 8: Pass timeout argument
240215
}
241216
async with Trace(
242217
"setup_socks5_connection", logger, request, kwargs
@@ -338,4 +313,4 @@ def info(self) -> str:
338313
return self._connection.info()
339314

340315
def __repr__(self) -> str:
341-
return f"<{self.__class__.__name__} [{self.info()}]>"
316+
return f"<{self.__class__.__name__} [{self.info()}]>"

httpcore/_sync/socks_proxy.py

Lines changed: 9 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def _init_socks5_connection(
4545
host: bytes,
4646
port: int,
4747
auth: tuple[bytes, bytes] | None = None,
48+
timeout: float | None = None, # <--- FIX 1: Add timeout argument
4849
) -> None:
4950
conn = socksio.socks5.SOCKS5Connection()
5051

@@ -56,10 +57,10 @@ def _init_socks5_connection(
5657
)
5758
conn.send(socksio.socks5.SOCKS5AuthMethodsRequest([auth_method]))
5859
outgoing_bytes = conn.data_to_send()
59-
stream.write(outgoing_bytes)
60+
stream.write(outgoing_bytes, timeout=timeout) # <--- FIX 2: Pass timeout
6061

6162
# Auth method response
62-
incoming_bytes = stream.read(max_bytes=4096)
63+
incoming_bytes = stream.read(max_bytes=4096, timeout=timeout) # <--- FIX 3: Pass timeout
6364
response = conn.receive_data(incoming_bytes)
6465
assert isinstance(response, socksio.socks5.SOCKS5AuthReply)
6566
if response.method != auth_method:
@@ -75,10 +76,10 @@ def _init_socks5_connection(
7576
username, password = auth
7677
conn.send(socksio.socks5.SOCKS5UsernamePasswordRequest(username, password))
7778
outgoing_bytes = conn.data_to_send()
78-
stream.write(outgoing_bytes)
79+
stream.write(outgoing_bytes, timeout=timeout) # <--- FIX 4: Pass timeout
7980

8081
# Username/password response
81-
incoming_bytes = stream.read(max_bytes=4096)
82+
incoming_bytes = stream.read(max_bytes=4096, timeout=timeout) # <--- FIX 5: Pass timeout
8283
response = conn.receive_data(incoming_bytes)
8384
assert isinstance(response, socksio.socks5.SOCKS5UsernamePasswordReply)
8485
if not response.success:
@@ -91,10 +92,10 @@ def _init_socks5_connection(
9192
)
9293
)
9394
outgoing_bytes = conn.data_to_send()
94-
stream.write(outgoing_bytes)
95+
stream.write(outgoing_bytes, timeout=timeout) # <--- FIX 6: Pass timeout
9596

9697
# Connect response
97-
incoming_bytes = stream.read(max_bytes=4096)
98+
incoming_bytes = stream.read(max_bytes=4096, timeout=timeout) # <--- FIX 7: Pass timeout
9899
response = conn.receive_data(incoming_bytes)
99100
assert isinstance(response, socksio.socks5.SOCKS5Reply)
100101
if response.reply_code != socksio.socks5.SOCKS5ReplyCode.SUCCEEDED:
@@ -122,33 +123,6 @@ def __init__(
122123
) -> None:
123124
"""
124125
A connection pool for making HTTP requests.
125-
126-
Parameters:
127-
proxy_url: The URL to use when connecting to the proxy server.
128-
For example `"http://127.0.0.1:8080/"`.
129-
ssl_context: An SSL context to use for verifying connections.
130-
If not specified, the default `httpcore.default_ssl_context()`
131-
will be used.
132-
max_connections: The maximum number of concurrent HTTP connections that
133-
the pool should allow. Any attempt to send a request on a pool that
134-
would exceed this amount will block until a connection is available.
135-
max_keepalive_connections: The maximum number of idle HTTP connections
136-
that will be maintained in the pool.
137-
keepalive_expiry: The duration in seconds that an idle HTTP connection
138-
may be maintained for before being expired from the pool.
139-
http1: A boolean indicating if HTTP/1.1 requests should be supported
140-
by the connection pool. Defaults to True.
141-
http2: A boolean indicating if HTTP/2 requests should be supported by
142-
the connection pool. Defaults to False.
143-
retries: The maximum number of retries when trying to establish
144-
a connection.
145-
local_address: Local address to connect from. Can also be used to
146-
connect using a particular address family. Using
147-
`local_address="0.0.0.0"` will connect using an `AF_INET` address
148-
(IPv4), while using `local_address="::"` will connect using an
149-
`AF_INET6` address (IPv6).
150-
uds: Path to a Unix Domain Socket to use instead of TCP sockets.
151-
network_backend: A backend instance to use for handling network I/O.
152126
"""
153127
super().__init__(
154128
ssl_context=ssl_context,
@@ -237,6 +211,7 @@ def handle_request(self, request: Request) -> Response:
237211
"host": self._remote_origin.host.decode("ascii"),
238212
"port": self._remote_origin.port,
239213
"auth": self._proxy_auth,
214+
"timeout": timeout, # <--- FIX 8: Pass timeout to handshake
240215
}
241216
with Trace(
242217
"setup_socks5_connection", logger, request, kwargs
@@ -338,4 +313,4 @@ def info(self) -> str:
338313
return self._connection.info()
339314

340315
def __repr__(self) -> str:
341-
return f"<{self.__class__.__name__} [{self.info()}]>"
316+
return f"<{self.__class__.__name__} [{self.info()}]>"

0 commit comments

Comments
 (0)