|
""" |
|
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 PySocks 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://<userid>@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://<username>:<password>@proxy-host" |
|
|
|
""" |
|
|
|
from __future__ import annotations |
|
|
|
try: |
|
import socks |
|
except ImportError: |
|
import warnings |
|
|
|
from ..exceptions import DependencyWarning |
|
|
|
warnings.warn( |
|
( |
|
"SOCKS support in urllib3 requires the installation of optional " |
|
"dependencies: specifically, PySocks. For more information, see " |
|
"https://urllib3.readthedocs.io/en/latest/advanced-usage.html#socks-proxies" |
|
), |
|
DependencyWarning, |
|
) |
|
raise |
|
|
|
import typing |
|
from socket import timeout as SocketTimeout |
|
|
|
from ..connection import HTTPConnection, HTTPSConnection |
|
from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool |
|
from ..exceptions import ConnectTimeoutError, NewConnectionError |
|
from ..poolmanager import PoolManager |
|
from ..util.url import parse_url |
|
|
|
try: |
|
import ssl |
|
except ImportError: |
|
ssl = None |
|
|
|
from typing import TypedDict |
|
|
|
|
|
class _TYPE_SOCKS_OPTIONS(TypedDict): |
|
socks_version: int |
|
proxy_host: str | None |
|
proxy_port: str | None |
|
username: str | None |
|
password: str | None |
|
rdns: bool |
|
|
|
|
|
class SOCKSConnection(HTTPConnection): |
|
""" |
|
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) -> socks.socksocket: |
|
""" |
|
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: |
|
extra_kw["socket_options"] = self.socket_options |
|
|
|
try: |
|
conn = socks.create_connection( |
|
(self.host, self.port), |
|
proxy_type=self._socks_options["socks_version"], |
|
proxy_addr=self._socks_options["proxy_host"], |
|
proxy_port=self._socks_options["proxy_port"], |
|
proxy_username=self._socks_options["username"], |
|
proxy_password=self._socks_options["password"], |
|
proxy_rdns=self._socks_options["rdns"], |
|
timeout=self.timeout, |
|
**extra_kw, |
|
) |
|
|
|
except SocketTimeout as e: |
|
raise ConnectTimeoutError( |
|
self, |
|
f"Connection to {self.host} timed out. (connect timeout={self.timeout})", |
|
) from e |
|
|
|
except socks.ProxyError as e: |
|
|
|
|
|
if e.socket_err: |
|
error = e.socket_err |
|
if isinstance(error, SocketTimeout): |
|
raise ConnectTimeoutError( |
|
self, |
|
f"Connection to {self.host} timed out. (connect timeout={self.timeout})", |
|
) from e |
|
else: |
|
|
|
|
|
raise NewConnectionError( |
|
self, f"Failed to establish a new connection: {error}" |
|
) |
|
else: |
|
raise NewConnectionError( |
|
self, f"Failed to establish a new connection: {e}" |
|
) from e |
|
|
|
except OSError as e: |
|
raise NewConnectionError( |
|
self, f"Failed to establish a new connection: {e}" |
|
) from e |
|
|
|
return conn |
|
|
|
|
|
|
|
|
|
|
|
|
|
class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection): |
|
pass |
|
|
|
|
|
class SOCKSHTTPConnectionPool(HTTPConnectionPool): |
|
ConnectionCls = SOCKSConnection |
|
|
|
|
|
class SOCKSHTTPSConnectionPool(HTTPSConnectionPool): |
|
ConnectionCls = SOCKSHTTPSConnection |
|
|
|
|
|
class SOCKSProxyManager(PoolManager): |
|
""" |
|
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 = socks.PROXY_TYPE_SOCKS5 |
|
rdns = False |
|
elif parsed.scheme == "socks5h": |
|
socks_version = socks.PROXY_TYPE_SOCKS5 |
|
rdns = True |
|
elif parsed.scheme == "socks4": |
|
socks_version = socks.PROXY_TYPE_SOCKS4 |
|
rdns = False |
|
elif parsed.scheme == "socks4a": |
|
socks_version = socks.PROXY_TYPE_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 |
|
|
|
super().__init__(num_pools, headers, **connection_pool_kw) |
|
|
|
self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme |
|
|