1"""
2This module contains provisional support for SOCKS proxies from within
3urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and
4SOCKS5. To enable its functionality, either install PySocks or install this
5module with the ``socks`` extra.
6
7The SOCKS implementation supports the full range of urllib3 features. It also
8supports the following SOCKS features:
9
10- SOCKS4A (``proxy_url='socks4a://...``)
11- SOCKS4 (``proxy_url='socks4://...``)
12- SOCKS5 with remote DNS (``proxy_url='socks5h://...``)
13- SOCKS5 with local DNS (``proxy_url='socks5://...``)
14- Usernames and passwords for the SOCKS proxy
15
16.. note::
17 It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in
18 your ``proxy_url`` to ensure that DNS resolution is done from the remote
19 server instead of client-side when connecting to a domain name.
20
21SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5
22supports IPv4, IPv6, and domain names.
23
24When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url``
25will be sent as the ``userid`` section of the SOCKS request:
26
27.. code-block:: python
28
29 proxy_url="socks4a://<userid>@proxy-host"
30
31When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion
32of the ``proxy_url`` will be sent as the username/password to authenticate
33with the proxy:
34
35.. code-block:: python
36
37 proxy_url="socks5h://<username>:<password>@proxy-host"
38
39"""
40
41from __future__ import annotations
42
43try:
44 import socks # type: ignore[import-not-found]
45except ImportError:
46 import warnings
47
48 from ..exceptions import DependencyWarning
49
50 warnings.warn(
51 (
52 "SOCKS support in urllib3 requires the installation of optional "
53 "dependencies: specifically, PySocks. For more information, see "
54 "https://urllib3.readthedocs.io/en/latest/advanced-usage.html#socks-proxies"
55 ),
56 DependencyWarning,
57 )
58 raise
59
60import typing
61from socket import timeout as SocketTimeout
62
63from ..connection import HTTPConnection, HTTPSConnection
64from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool
65from ..exceptions import ConnectTimeoutError, NewConnectionError
66from ..poolmanager import PoolManager
67from ..util.url import parse_url
68
69try:
70 import ssl
71except ImportError:
72 ssl = None # type: ignore[assignment]
73
74
75class _TYPE_SOCKS_OPTIONS(typing.TypedDict):
76 socks_version: int
77 proxy_host: str | None
78 proxy_port: str | None
79 username: str | None
80 password: str | None
81 rdns: bool
82
83
84class SOCKSConnection(HTTPConnection):
85 """
86 A plain-text HTTP connection that connects via a SOCKS proxy.
87 """
88
89 def __init__(
90 self,
91 _socks_options: _TYPE_SOCKS_OPTIONS,
92 *args: typing.Any,
93 **kwargs: typing.Any,
94 ) -> None:
95 self._socks_options = _socks_options
96 super().__init__(*args, **kwargs)
97
98 def _new_conn(self) -> socks.socksocket:
99 """
100 Establish a new connection via the SOCKS proxy.
101 """
102 extra_kw: dict[str, typing.Any] = {}
103 if self.source_address:
104 extra_kw["source_address"] = self.source_address
105
106 if self.socket_options:
107 extra_kw["socket_options"] = self.socket_options
108
109 try:
110 conn = socks.create_connection(
111 (self.host, self.port),
112 proxy_type=self._socks_options["socks_version"],
113 proxy_addr=self._socks_options["proxy_host"],
114 proxy_port=self._socks_options["proxy_port"],
115 proxy_username=self._socks_options["username"],
116 proxy_password=self._socks_options["password"],
117 proxy_rdns=self._socks_options["rdns"],
118 timeout=self.timeout,
119 **extra_kw,
120 )
121
122 except SocketTimeout as e:
123 raise ConnectTimeoutError(
124 self,
125 f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
126 ) from e
127
128 except socks.ProxyError as e:
129 # This is fragile as hell, but it seems to be the only way to raise
130 # useful errors here.
131 if e.socket_err:
132 error = e.socket_err
133 if isinstance(error, SocketTimeout):
134 raise ConnectTimeoutError(
135 self,
136 f"Connection to {self.host} timed out. (connect timeout={self.timeout})",
137 ) from e
138 else:
139 # Adding `from e` messes with coverage somehow, so it's omitted.
140 # See #2386.
141 raise NewConnectionError(
142 self, f"Failed to establish a new connection: {error}"
143 )
144 else:
145 raise NewConnectionError(
146 self, f"Failed to establish a new connection: {e}"
147 ) from e
148
149 except OSError as e: # Defensive: PySocks should catch all these.
150 raise NewConnectionError(
151 self, f"Failed to establish a new connection: {e}"
152 ) from e
153
154 return conn
155
156
157# We don't need to duplicate the Verified/Unverified distinction from
158# urllib3/connection.py here because the HTTPSConnection will already have been
159# correctly set to either the Verified or Unverified form by that module. This
160# means the SOCKSHTTPSConnection will automatically be the correct type.
161class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection):
162 pass
163
164
165class SOCKSHTTPConnectionPool(HTTPConnectionPool):
166 ConnectionCls = SOCKSConnection
167
168
169class SOCKSHTTPSConnectionPool(HTTPSConnectionPool):
170 ConnectionCls = SOCKSHTTPSConnection
171
172
173class SOCKSProxyManager(PoolManager):
174 """
175 A version of the urllib3 ProxyManager that routes connections via the
176 defined SOCKS proxy.
177 """
178
179 pool_classes_by_scheme = {
180 "http": SOCKSHTTPConnectionPool,
181 "https": SOCKSHTTPSConnectionPool,
182 }
183
184 def __init__(
185 self,
186 proxy_url: str,
187 username: str | None = None,
188 password: str | None = None,
189 num_pools: int = 10,
190 headers: typing.Mapping[str, str] | None = None,
191 **connection_pool_kw: typing.Any,
192 ):
193 parsed = parse_url(proxy_url)
194
195 if username is None and password is None and parsed.auth is not None:
196 split = parsed.auth.split(":")
197 if len(split) == 2:
198 username, password = split
199 if parsed.scheme == "socks5":
200 socks_version = socks.PROXY_TYPE_SOCKS5
201 rdns = False
202 elif parsed.scheme == "socks5h":
203 socks_version = socks.PROXY_TYPE_SOCKS5
204 rdns = True
205 elif parsed.scheme == "socks4":
206 socks_version = socks.PROXY_TYPE_SOCKS4
207 rdns = False
208 elif parsed.scheme == "socks4a":
209 socks_version = socks.PROXY_TYPE_SOCKS4
210 rdns = True
211 else:
212 raise ValueError(f"Unable to determine SOCKS version from {proxy_url}")
213
214 self.proxy_url = proxy_url
215
216 socks_options = {
217 "socks_version": socks_version,
218 "proxy_host": parsed.host,
219 "proxy_port": parsed.port,
220 "username": username,
221 "password": password,
222 "rdns": rdns,
223 }
224 connection_pool_kw["_socks_options"] = socks_options
225
226 super().__init__(num_pools, headers, **connection_pool_kw)
227
228 self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme