Skip to content

Commit 197c042

Browse files
committed
Spike SOCKS proxy implementation.
1 parent ec25970 commit 197c042

File tree

3 files changed

+109
-10
lines changed

3 files changed

+109
-10
lines changed

src/websockets/asyncio/client.py

+52-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import urllib.parse
88
from collections.abc import AsyncIterator, Generator, Sequence
99
from types import TracebackType
10-
from typing import Any, Callable
10+
from typing import Any, Callable, Literal
1111

1212
from ..client import ClientProtocol, backoff
1313
from ..datastructures import HeadersLike
@@ -18,7 +18,7 @@
1818
from ..http11 import USER_AGENT, Response
1919
from ..protocol import CONNECTING, Event
2020
from ..typing import LoggerLike, Origin, Subprotocol
21-
from ..uri import WebSocketURI, parse_uri
21+
from ..uri import ProxyURI, WebSocketURI, get_proxy, parse_proxy_uri, parse_uri
2222
from .compatibility import TimeoutError, asyncio_timeout
2323
from .connection import Connection
2424

@@ -208,6 +208,10 @@ class connect:
208208
user_agent_header: Value of the ``User-Agent`` request header.
209209
It defaults to ``"Python/x.y.z websockets/X.Y"``.
210210
Setting it to :obj:`None` removes the header.
211+
proxy: If a proxy is configured, it is used by default. Set ``proxy``
212+
to :obj:`None` to disable the proxy or to the address of a proxy
213+
to override the system configuration. See the :doc:`proxy docs
214+
<../../topics/proxy>` for details.
211215
process_exception: When reconnecting automatically, tell whether an
212216
error is transient or fatal. The default behavior is defined by
213217
:func:`process_exception`. Refer to its documentation for details.
@@ -279,6 +283,7 @@ def __init__(
279283
# HTTP
280284
additional_headers: HeadersLike | None = None,
281285
user_agent_header: str | None = USER_AGENT,
286+
proxy: str | Literal[True] | None = True,
282287
process_exception: Callable[[Exception], Exception | None] = process_exception,
283288
# Timeouts
284289
open_timeout: float | None = 10,
@@ -333,6 +338,7 @@ def protocol_factory(uri: WebSocketURI) -> ClientConnection:
333338
)
334339
return connection
335340

341+
self.proxy = proxy
336342
self.protocol_factory = protocol_factory
337343
self.handshake_args = (
338344
additional_headers,
@@ -346,9 +352,20 @@ def protocol_factory(uri: WebSocketURI) -> ClientConnection:
346352
async def create_connection(self) -> ClientConnection:
347353
"""Create TCP or Unix connection."""
348354
loop = asyncio.get_running_loop()
355+
kwargs = self.connection_kwargs.copy()
349356

350357
ws_uri = parse_uri(self.uri)
351-
kwargs = self.connection_kwargs.copy()
358+
359+
proxy = self.proxy
360+
proxy_uri: ProxyURI | None = None
361+
if kwargs.pop("unix", False):
362+
proxy = None
363+
if kwargs.get("sock") is not None:
364+
proxy = None
365+
if proxy is True:
366+
proxy = get_proxy(ws_uri)
367+
if proxy is not None:
368+
proxy_uri = parse_proxy_uri(proxy)
352369

353370
def factory() -> ClientConnection:
354371
return self.protocol_factory(ws_uri)
@@ -365,6 +382,38 @@ def factory() -> ClientConnection:
365382
if kwargs.pop("unix", False):
366383
_, connection = await loop.create_unix_connection(factory, **kwargs)
367384
else:
385+
if proxy_uri is not None:
386+
if proxy_uri.scheme[:5] == "socks":
387+
try:
388+
from python_socks import ProxyType
389+
from python_socks.async_.asyncio import Proxy
390+
except ImportError:
391+
raise ImportError(
392+
"python-socks is required to use a SOCKS proxy"
393+
)
394+
if proxy_uri.scheme[:6] == "socks5":
395+
proxy_type = ProxyType.SOCKS5
396+
elif proxy_uri.scheme[:6] == "socks4":
397+
proxy_type = ProxyType.SOCKS4
398+
else:
399+
raise AssertionError("unsupported SOCKS proxy")
400+
socks_proxy = Proxy(
401+
proxy_type,
402+
proxy_uri.host,
403+
proxy_uri.port,
404+
proxy_uri.username,
405+
proxy_uri.password,
406+
rdns=kwargs.pop("rdns", None),
407+
)
408+
kwargs["sock"] = await socks_proxy.connect(
409+
ws_uri.host,
410+
ws_uri.port,
411+
local_addr=kwargs.pop("local_addr", None),
412+
)
413+
else:
414+
raise NotImplementedError(
415+
f"proxy scheme not implemented yet: {proxy_uri.scheme}"
416+
)
368417
if kwargs.get("sock") is None:
369418
kwargs.setdefault("host", ws_uri.host)
370419
kwargs.setdefault("port", ws_uri.port)

src/websockets/sync/client.py

+56-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import threading
66
import warnings
77
from collections.abc import Sequence
8-
from typing import Any
8+
from typing import Any, Literal
99

1010
from ..client import ClientProtocol
1111
from ..datastructures import HeadersLike
@@ -15,7 +15,7 @@
1515
from ..http11 import USER_AGENT, Response
1616
from ..protocol import CONNECTING, Event
1717
from ..typing import LoggerLike, Origin, Subprotocol
18-
from ..uri import parse_uri
18+
from ..uri import ProxyURI, get_proxy, parse_proxy_uri, parse_uri
1919
from .connection import Connection
2020
from .utils import Deadline
2121

@@ -139,6 +139,7 @@ def connect(
139139
# HTTP
140140
additional_headers: HeadersLike | None = None,
141141
user_agent_header: str | None = USER_AGENT,
142+
proxy: str | Literal[True] | None = True,
142143
# Timeouts
143144
open_timeout: float | None = 10,
144145
ping_interval: float | None = 20,
@@ -189,6 +190,10 @@ def connect(
189190
user_agent_header: Value of the ``User-Agent`` request header.
190191
It defaults to ``"Python/x.y.z websockets/X.Y"``.
191192
Setting it to :obj:`None` removes the header.
193+
proxy: If a proxy is configured, it is used by default. Set ``proxy``
194+
to :obj:`None` to disable the proxy or to the address of a proxy
195+
to override the system configuration. See the :doc:`proxy docs
196+
<../../topics/proxy>` for details.
192197
open_timeout: Timeout for opening the connection in seconds.
193198
:obj:`None` disables the timeout.
194199
ping_interval: Interval between keepalive pings in seconds.
@@ -253,6 +258,16 @@ def connect(
253258
elif compression is not None:
254259
raise ValueError(f"unsupported compression: {compression}")
255260

261+
proxy_uri: ProxyURI | None = None
262+
if unix:
263+
proxy = None
264+
if sock is not None:
265+
proxy = None
266+
if proxy is True:
267+
proxy = get_proxy(ws_uri)
268+
if proxy is not None:
269+
proxy_uri = parse_proxy_uri(proxy)
270+
256271
# Calculate timeouts on the TCP, TLS, and WebSocket handshakes.
257272
# The TCP and TLS timeouts must be set on the socket, then removed
258273
# to avoid conflicting with the WebSocket timeout in handshake().
@@ -271,14 +286,49 @@ def connect(
271286
assert path is not None # mypy cannot figure this out
272287
sock.connect(path)
273288
else:
274-
kwargs.setdefault("timeout", deadline.timeout())
275-
sock = socket.create_connection((ws_uri.host, ws_uri.port), **kwargs)
289+
if proxy_uri is not None:
290+
if proxy_uri.scheme[:5] == "socks":
291+
try:
292+
from python_socks import ProxyType
293+
from python_socks.sync import Proxy
294+
except ImportError:
295+
raise ImportError(
296+
"python-socks is required to use a SOCKS proxy"
297+
)
298+
if proxy_uri.scheme[:6] == "socks5":
299+
proxy_type = ProxyType.SOCKS5
300+
elif proxy_uri.scheme[:6] == "socks4":
301+
proxy_type = ProxyType.SOCKS4
302+
else:
303+
raise AssertionError("unsupported SOCKS proxy")
304+
socks_proxy = Proxy(
305+
proxy_type,
306+
proxy_uri.host,
307+
proxy_uri.port,
308+
proxy_uri.username,
309+
proxy_uri.password,
310+
rdns=kwargs.pop("rdns", None),
311+
)
312+
sock = socks_proxy.connect(
313+
ws_uri.host,
314+
ws_uri.port,
315+
timeout=deadline.timeout(),
316+
local_addr=kwargs.pop("local_addr", None),
317+
)
318+
else:
319+
raise NotImplementedError(
320+
f"proxy scheme not implemented yet: {proxy_uri.scheme}"
321+
)
322+
else:
323+
kwargs.setdefault("timeout", deadline.timeout())
324+
sock = socket.create_connection(
325+
(ws_uri.host, ws_uri.port), **kwargs
326+
)
276327
sock.settimeout(None)
277328

278329
# Disable Nagle algorithm
279330

280-
if not unix:
281-
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
331+
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True)
282332

283333
# Initialize TLS wrapper and perform TLS handshake
284334

src/websockets/uri.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def parse_proxy_uri(uri: str) -> ProxyURI:
188188
189189
"""
190190
parsed = urllib.parse.urlparse(uri)
191-
if parsed.scheme not in ["socks5", "socks4", "https", "http"]:
191+
if parsed.scheme not in ["socks5h", "socks5", "socks4a", "socks4", "https", "http"]:
192192
raise InvalidURI(uri, f"proxy scheme isn't supported: {parsed.scheme}")
193193
if parsed.hostname is None:
194194
raise InvalidURI(uri, "proxy hostname isn't provided")

0 commit comments

Comments
 (0)