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