| 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 |