master
py 189 lines 6.08 KB
Raw
1 # -*- coding: utf-8 -*-
2 # SPDX-License-Identifier: MIT
3 """
4 This module contains provisional support for SOCKS proxies from within
5 urllib3. This module supports SOCKS4 (specifically the SOCKS4A variant) and
6 SOCKS5. To enable its functionality, either install PySocks or install this
7 module with the ``socks`` extra.
8
9 The SOCKS implementation supports the full range of urllib3 features. It also
10 supports the following SOCKS features:
11
12 - SOCKS4
13 - SOCKS4a
14 - SOCKS5
15 - Usernames and passwords for the SOCKS proxy
16
17 Known Limitations:
18
19 - Currently PySocks does not support contacting remote websites via literal
20 IPv6 addresses. Any such connection attempt will fail. You must use a domain
21 name.
22 - Currently PySocks does not support IPv6 connections to the SOCKS proxy. Any
23 such connection attempt will fail.
24 """
25 from __future__ import absolute_import
26
27 try:
28 import socks
29 except ImportError:
30 import warnings
31 from ..exceptions import DependencyWarning
32
33 warnings.warn((
34 'SOCKS support in urllib3 requires the installation of optional '
35 'dependencies: specifically, PySocks. For more information, see '
36 'https://urllib3.readthedocs.io/en/latest/contrib.html#socks-proxies'
37 ),
38 DependencyWarning
39 )
40 raise
41
42 from socket import error as SocketError, timeout as SocketTimeout
43
44 from ..connection import (
45 HTTPConnection, HTTPSConnection
46 )
47 from ..connectionpool import (
48 HTTPConnectionPool, HTTPSConnectionPool
49 )
50 from ..exceptions import ConnectTimeoutError, NewConnectionError
51 from ..poolmanager import PoolManager
52 from ..util.url import parse_url
53
54 try:
55 import ssl
56 except ImportError:
57 ssl = None
58
59
60 class SOCKSConnection(HTTPConnection):
61 """
62 A plain-text HTTP connection that connects via a SOCKS proxy.
63 """
64 def __init__(self, *args, **kwargs):
65 self._socks_options = kwargs.pop('_socks_options')
66 super(SOCKSConnection, self).__init__(*args, **kwargs)
67
68 def _new_conn(self):
69 """
70 Establish a new connection via the SOCKS proxy.
71 """
72 extra_kw = {}
73 if self.source_address:
74 extra_kw['source_address'] = self.source_address
75
76 if self.socket_options:
77 extra_kw['socket_options'] = self.socket_options
78
79 try:
80 conn = socks.create_connection(
81 (self.host, self.port),
82 proxy_type=self._socks_options['socks_version'],
83 proxy_addr=self._socks_options['proxy_host'],
84 proxy_port=self._socks_options['proxy_port'],
85 proxy_username=self._socks_options['username'],
86 proxy_password=self._socks_options['password'],
87 proxy_rdns=self._socks_options['rdns'],
88 timeout=self.timeout,
89 **extra_kw
90 )
91
92 except SocketTimeout as e:
93 raise ConnectTimeoutError(
94 self, "Connection to %s timed out. (connect timeout=%s)" %
95 (self.host, self.timeout))
96
97 except socks.ProxyError as e:
98 # This is fragile as hell, but it seems to be the only way to raise
99 # useful errors here.
100 if e.socket_err:
101 error = e.socket_err
102 if isinstance(error, SocketTimeout):
103 raise ConnectTimeoutError(
104 self,
105 "Connection to %s timed out. (connect timeout=%s)" %
106 (self.host, self.timeout)
107 )
108 else:
109 raise NewConnectionError(
110 self,
111 "Failed to establish a new connection: %s" % error
112 )
113 else:
114 raise NewConnectionError(
115 self,
116 "Failed to establish a new connection: %s" % e
117 )
118
119 except SocketError as e: # Defensive: PySocks should catch all these.
120 raise NewConnectionError(
121 self, "Failed to establish a new connection: %s" % e)
122
123 return conn
124
125
126 # We don't need to duplicate the Verified/Unverified distinction from
127 # urllib3/connection.py here because the HTTPSConnection will already have been
128 # correctly set to either the Verified or Unverified form by that module. This
129 # means the SOCKSHTTPSConnection will automatically be the correct type.
130 class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection):
131 pass
132
133
134 class SOCKSHTTPConnectionPool(HTTPConnectionPool):
135 ConnectionCls = SOCKSConnection
136
137
138 class SOCKSHTTPSConnectionPool(HTTPSConnectionPool):
139 ConnectionCls = SOCKSHTTPSConnection
140
141
142 class SOCKSProxyManager(PoolManager):
143 """
144 A version of the urllib3 ProxyManager that routes connections via the
145 defined SOCKS proxy.
146 """
147 pool_classes_by_scheme = {
148 'http': SOCKSHTTPConnectionPool,
149 'https': SOCKSHTTPSConnectionPool,
150 }
151
152 def __init__(self, proxy_url, username=None, password=None,
153 num_pools=10, headers=None, **connection_pool_kw):
154 parsed = parse_url(proxy_url)
155
156 if parsed.scheme == 'socks5':
157 socks_version = socks.PROXY_TYPE_SOCKS5
158 rdns = False
159 elif parsed.scheme == 'socks5h':
160 socks_version = socks.PROXY_TYPE_SOCKS5
161 rdns = True
162 elif parsed.scheme == 'socks4':
163 socks_version = socks.PROXY_TYPE_SOCKS4
164 rdns = False
165 elif parsed.scheme == 'socks4a':
166 socks_version = socks.PROXY_TYPE_SOCKS4
167 rdns = True
168 else:
169 raise ValueError(
170 "Unable to determine SOCKS version from %s" % proxy_url
171 )
172
173 self.proxy_url = proxy_url
174
175 socks_options = {
176 'socks_version': socks_version,
177 'proxy_host': parsed.host,
178 'proxy_port': parsed.port,
179 'username': username,
180 'password': password,
181 'rdns': rdns
182 }
183 connection_pool_kw['_socks_options'] = socks_options
184
185 super(SOCKSProxyManager, self).__init__(
186 num_pools, headers, **connection_pool_kw
187 )
188
189 self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme