""" This module contains provisional support for SOCKS proxies from within urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and SOCKS5. To enable its functionality, either install python-socks or install this module with the ``socks`` extra. The SOCKS implementation supports the full range of urllib3 features. It also supports the following SOCKS features: - SOCKS4A (``proxy_url='socks4a://...``) - SOCKS4 (``proxy_url='socks4://...``) - SOCKS5 with remote DNS (``proxy_url='socks5h://...``) - SOCKS5 with local DNS (``proxy_url='socks5://...``) - Usernames and passwords for the SOCKS proxy .. note:: It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in your ``proxy_url`` to ensure that DNS resolution is done from the remote server instead of client-side when connecting to a domain name. SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5 supports IPv4, IPv6, and domain names. When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url`` will be sent as the ``userid`` section of the SOCKS request: .. code-block:: python proxy_url="socks4a://@proxy-host" When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion of the ``proxy_url`` will be sent as the username/password to authenticate with the proxy: .. code-block:: python proxy_url="socks5h://:@proxy-host" """ from __future__ import annotations import warnings #: We purposely want to support PySocks[...] due to our shadowing of the legacy "urllib3". "Dot not disturb" policy. BYPASS_SOCKS_LEGACY: bool = False try: from python_socks import ( ProxyConnectionError, ProxyError, ProxyTimeoutError, ProxyType, ) from python_socks.sync import Proxy from ._socks_override import AsyncioProxy except ImportError: from ..exceptions import DependencyWarning try: import socks # noqa except ImportError: warnings.warn( ( "SOCKS support in urllib3.future requires the installation of an optional " "dependency: python-socks. For more information, see " "https://urllib3future.readthedocs.io/en/latest/contrib.html#socks-proxies" ), DependencyWarning, ) else: from ._socks_legacy import ( SOCKSConnection, SOCKSHTTPConnectionPool, SOCKSHTTPSConnection, SOCKSHTTPSConnectionPool, SOCKSProxyManager, ) BYPASS_SOCKS_LEGACY = True if not BYPASS_SOCKS_LEGACY: raise if not BYPASS_SOCKS_LEGACY: import typing from socket import socket from socket import timeout as SocketTimeout # asynchronous part from .._async.connection import AsyncHTTPConnection, AsyncHTTPSConnection from .._async.connectionpool import ( AsyncHTTPConnectionPool, AsyncHTTPSConnectionPool, ) from .._async.poolmanager import AsyncPoolManager from .._typing import _TYPE_SOCKS_OPTIONS from ..backend import HttpVersion # synchronous part from ..connection import HTTPConnection, HTTPSConnection from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool from ..contrib.ssa import AsyncSocket from ..exceptions import ConnectTimeoutError, NewConnectionError from ..poolmanager import PoolManager from ..util.url import parse_url try: import ssl except ImportError: ssl = None # type: ignore[assignment] class SOCKSConnection(HTTPConnection): # type: ignore[no-redef] """ A plain-text HTTP connection that connects via a SOCKS proxy. """ def __init__( self, _socks_options: _TYPE_SOCKS_OPTIONS, *args: typing.Any, **kwargs: typing.Any, ) -> None: self._socks_options = _socks_options super().__init__(*args, **kwargs) def _new_conn(self) -> socket: """ Establish a new connection via the SOCKS proxy. """ extra_kw: dict[str, typing.Any] = {} if self.source_address: extra_kw["source_address"] = self.source_address if self.socket_options: only_tcp_options = [] for opt in self.socket_options: if len(opt) == 3: only_tcp_options.append(opt) elif len(opt) == 4: protocol: str = opt[3].lower() if protocol == "udp": continue only_tcp_options.append(opt[:3]) extra_kw["socket_options"] = only_tcp_options try: assert self._socks_options["proxy_host"] is not None assert self._socks_options["proxy_port"] is not None p = Proxy( proxy_type=self._socks_options["socks_version"], # type: ignore[arg-type] host=self._socks_options["proxy_host"], port=int(self._socks_options["proxy_port"]), username=self._socks_options["username"], password=self._socks_options["password"], rdns=self._socks_options["rdns"], ) _socket = self._resolver.create_connection( ( self._socks_options["proxy_host"], int(self._socks_options["proxy_port"]), ), timeout=self.timeout, source_address=self.source_address, socket_options=extra_kw["socket_options"], quic_upgrade_via_dns_rr=False, timing_hook=lambda _: setattr(self, "_connect_timings", _), ) # our dependency started to deprecate passing "_socket" # which is ... vital for our integration. We'll start by silencing the warning. # then we'll think on how to proceed. # A) the maintainer agrees to revert https://github.com/romis2012/python-socks/commit/173a7390469c06aa033f8dca67c827854b462bc3#diff-e4086fa970d1c98b1eb341e58cb70e9ceffe7391b2feecc4b66c7e92ea2de76fR64 # B) the maintainer pursue the removal -> do we vendor our copy of python-socks? is there an alternative? with warnings.catch_warnings(): warnings.simplefilter("ignore", DeprecationWarning) return p.connect( self.host, self.port, self.timeout, _socket=_socket, ) except (SocketTimeout, ProxyTimeoutError) as e: raise ConnectTimeoutError( self, f"Connection to {self.host} timed out. (connect timeout={self.timeout})", ) from e except (ProxyConnectionError, ProxyError) as e: raise NewConnectionError( self, f"Failed to establish a new connection: {e}" ) from e except OSError as e: # Defensive: PySocks should catch all these. raise NewConnectionError( self, f"Failed to establish a new connection: {e}" ) from e # We don't need to duplicate the Verified/Unverified distinction from # urllib3/connection.py here because the HTTPSConnection will already have been # correctly set to either the Verified or Unverified form by that module. This # means the SOCKSHTTPSConnection will automatically be the correct type. class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): # type: ignore[no-redef] pass class SOCKSHTTPConnectionPool(HTTPConnectionPool): # type: ignore[no-redef] ConnectionCls = SOCKSConnection class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): # type: ignore[no-redef] ConnectionCls = SOCKSHTTPSConnection class SOCKSProxyManager(PoolManager): # type: ignore[no-redef] """ A version of the urllib3 ProxyManager that routes connections via the defined SOCKS proxy. """ pool_classes_by_scheme = { "http": SOCKSHTTPConnectionPool, "https": SOCKSHTTPSConnectionPool, } def __init__( self, proxy_url: str, username: str | None = None, password: str | None = None, num_pools: int = 10, headers: typing.Mapping[str, str] | None = None, **connection_pool_kw: typing.Any, ): parsed = parse_url(proxy_url) if username is None and password is None and parsed.auth is not None: split = parsed.auth.split(":") if len(split) == 2: username, password = split if parsed.scheme == "socks5": socks_version = ProxyType.SOCKS5 rdns = False elif parsed.scheme == "socks5h": socks_version = ProxyType.SOCKS5 rdns = True elif parsed.scheme == "socks4": socks_version = ProxyType.SOCKS4 rdns = False elif parsed.scheme == "socks4a": socks_version = ProxyType.SOCKS4 rdns = True else: raise ValueError(f"Unable to determine SOCKS version from {proxy_url}") self.proxy_url = proxy_url socks_options = { "socks_version": socks_version, "proxy_host": parsed.host, "proxy_port": parsed.port, "username": username, "password": password, "rdns": rdns, } connection_pool_kw["_socks_options"] = socks_options if "disabled_svn" not in connection_pool_kw: connection_pool_kw["disabled_svn"] = set() connection_pool_kw["disabled_svn"].add(HttpVersion.h3) super().__init__(num_pools, headers, **connection_pool_kw) self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme class AsyncSOCKSConnection(AsyncHTTPConnection): """ A plain-text HTTP connection that connects via a SOCKS proxy. """ def __init__( self, _socks_options: _TYPE_SOCKS_OPTIONS, *args: typing.Any, **kwargs: typing.Any, ) -> None: self._socks_options = _socks_options super().__init__(*args, **kwargs) async def _new_conn(self) -> AsyncSocket: # type: ignore[override] """ Establish a new connection via the SOCKS proxy. """ extra_kw: dict[str, typing.Any] = {} if self.source_address: extra_kw["source_address"] = self.source_address if self.socket_options: only_tcp_options = [] for opt in self.socket_options: if len(opt) == 3: only_tcp_options.append(opt) elif len(opt) == 4: protocol: str = opt[3].lower() if protocol == "udp": continue only_tcp_options.append(opt[:3]) extra_kw["socket_options"] = only_tcp_options try: assert self._socks_options["proxy_host"] is not None assert self._socks_options["proxy_port"] is not None p = AsyncioProxy( proxy_type=self._socks_options["socks_version"], # type: ignore[arg-type] host=self._socks_options["proxy_host"], port=int(self._socks_options["proxy_port"]), username=self._socks_options["username"], password=self._socks_options["password"], rdns=self._socks_options["rdns"], ) _socket = await self._resolver.create_connection( ( self._socks_options["proxy_host"], int(self._socks_options["proxy_port"]), ), timeout=self.timeout, source_address=self.source_address, socket_options=extra_kw["socket_options"], quic_upgrade_via_dns_rr=False, timing_hook=lambda _: setattr(self, "_connect_timings", _), ) return await p.connect( self.host, self.port, self.timeout, _socket, ) except (SocketTimeout, ProxyTimeoutError) as e: raise ConnectTimeoutError( self, f"Connection to {self.host} timed out. (connect timeout={self.timeout})", ) from e except (ProxyConnectionError, ProxyError) as e: raise NewConnectionError( self, f"Failed to establish a new connection: {e}" ) from e except OSError as e: # Defensive: PySocks should catch all these. raise NewConnectionError( self, f"Failed to establish a new connection: {e}" ) from e # We don't need to duplicate the Verified/Unverified distinction from # urllib3/connection.py here because the HTTPSConnection will already have been # correctly set to either the Verified or Unverified form by that module. This # means the SOCKSHTTPSConnection will automatically be the correct type. class AsyncSOCKSHTTPSConnection(AsyncSOCKSConnection, AsyncHTTPSConnection): pass class AsyncSOCKSHTTPConnectionPool(AsyncHTTPConnectionPool): ConnectionCls = AsyncSOCKSConnection class AsyncSOCKSHTTPSConnectionPool(AsyncHTTPSConnectionPool): ConnectionCls = AsyncSOCKSHTTPSConnection class AsyncSOCKSProxyManager(AsyncPoolManager): """ A version of the urllib3 ProxyManager that routes connections via the defined SOCKS proxy. """ pool_classes_by_scheme = { "http": AsyncSOCKSHTTPConnectionPool, "https": AsyncSOCKSHTTPSConnectionPool, } def __init__( self, proxy_url: str, username: str | None = None, password: str | None = None, num_pools: int = 10, headers: typing.Mapping[str, str] | None = None, **connection_pool_kw: typing.Any, ): parsed = parse_url(proxy_url) if username is None and password is None and parsed.auth is not None: split = parsed.auth.split(":") if len(split) == 2: username, password = split if parsed.scheme == "socks5": socks_version = ProxyType.SOCKS5 rdns = False elif parsed.scheme == "socks5h": socks_version = ProxyType.SOCKS5 rdns = True elif parsed.scheme == "socks4": socks_version = ProxyType.SOCKS4 rdns = False elif parsed.scheme == "socks4a": socks_version = ProxyType.SOCKS4 rdns = True else: raise ValueError(f"Unable to determine SOCKS version from {proxy_url}") self.proxy_url = proxy_url socks_options = { "socks_version": socks_version, "proxy_host": parsed.host, "proxy_port": parsed.port, "username": username, "password": password, "rdns": rdns, } connection_pool_kw["_socks_options"] = socks_options if "disabled_svn" not in connection_pool_kw: connection_pool_kw["disabled_svn"] = set() connection_pool_kw["disabled_svn"].add(HttpVersion.h3) super().__init__(num_pools, headers, **connection_pool_kw) self.pool_classes_by_scheme = AsyncSOCKSProxyManager.pool_classes_by_scheme __all__ = [ "SOCKSConnection", "SOCKSProxyManager", "SOCKSHTTPSConnection", "SOCKSHTTPSConnectionPool", "SOCKSHTTPConnectionPool", ] if not BYPASS_SOCKS_LEGACY: __all__ += [ "AsyncSOCKSConnection", "AsyncSOCKSHTTPSConnection", "AsyncSOCKSHTTPConnectionPool", "AsyncSOCKSHTTPSConnectionPool", "AsyncSOCKSProxyManager", ]