socks.py 6.9 KB

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