Always use python-socks when available

Relying on Python 3.6 or above to be installed to unconditionally
use this library would break user's code, because this is a new
optional dependency that users may not have installed.

Instead, always use the new library when available, which should
work better than pysocks because it natively supports asyncio.
This commit is contained in:
Lonami Exo 2020-11-28 12:09:46 +01:00
parent ab3c5acf9a
commit f2f43336c6

View File

@ -8,6 +8,11 @@ try:
except ImportError: except ImportError:
ssl_mod = None ssl_mod = None
try:
import python_socks
except ImportError:
python_socks = None
from ...errors import InvalidChecksumError from ...errors import InvalidChecksumError
from ... import helpers from ... import helpers
@ -47,7 +52,6 @@ class Connection(abc.ABC):
@staticmethod @staticmethod
def _wrap_socket_ssl(sock): def _wrap_socket_ssl(sock):
if ssl_mod is None: if ssl_mod is None:
raise RuntimeError( raise RuntimeError(
'Cannot use proxy that requires SSL ' 'Cannot use proxy that requires SSL '
@ -61,19 +65,17 @@ class Connection(abc.ABC):
ciphers='ADH-AES256-SHA') ciphers='ADH-AES256-SHA')
@staticmethod @staticmethod
def _parse_proxy(library, proxy_type, addr, port, rdns=True, username=None, password=None): def _parse_proxy(proxy_type, addr, port, rdns=True, username=None, password=None):
if isinstance(proxy_type, str): if isinstance(proxy_type, str):
proxy_type = proxy_type.lower() proxy_type = proxy_type.lower()
if library == "python-socks": # Always prefer `python_socks` when available
if python_socks:
from python_socks import ProxyType from python_socks import ProxyType
# We do the check for numerical values here # We do the check for numerical values here
# to be backwards compatible with PySocks proxy format, # to be backwards compatible with PySocks proxy format,
# (since socks.SOCKS5 == 2, socks.SOCKS4 == 1, socks.HTTP == 3) # (since socks.SOCKS5 == 2, socks.SOCKS4 == 1, socks.HTTP == 3)
if proxy_type == ProxyType.SOCKS5 or proxy_type == 2 or proxy_type == "socks5": if proxy_type == ProxyType.SOCKS5 or proxy_type == 2 or proxy_type == "socks5":
protocol = ProxyType.SOCKS5 protocol = ProxyType.SOCKS5
elif proxy_type == ProxyType.SOCKS4 or proxy_type == 1 or proxy_type == "socks4": elif proxy_type == ProxyType.SOCKS4 or proxy_type == 1 or proxy_type == "socks4":
@ -83,14 +85,10 @@ class Connection(abc.ABC):
else: else:
raise ValueError("Unknown proxy protocol type: {}".format(proxy_type)) raise ValueError("Unknown proxy protocol type: {}".format(proxy_type))
# NOTE: We return a tuple compatible with the # This tuple must be compatible with `python_socks`' `Proxy.create()` signature
# signature (order) of python_socks `Proxy.create()`
# I.E.: (proxy_type, host, port, username, password, rdns)
return protocol, addr, port, username, password, rdns return protocol, addr, port, username, password, rdns
elif library == "pysocks": else:
from socks import SOCKS5, SOCKS4, HTTP from socks import SOCKS5, SOCKS4, HTTP
if proxy_type == 2 or proxy_type == "socks5": if proxy_type == 2 or proxy_type == "socks5":
@ -102,23 +100,19 @@ class Connection(abc.ABC):
else: else:
raise ValueError("Unknown proxy protocol type: {}".format(proxy_type)) raise ValueError("Unknown proxy protocol type: {}".format(proxy_type))
# NOTE: We return a tuple compatible with the # This tuple must be compatible with `PySocks`' `socksocket.set_proxy()` signature
# signature of `PySocks` `socksocket.set_proxy()`
# I.E.: (proxy_type, addr, port, rdns, username, password)
return protocol, addr, port, rdns, username, password return protocol, addr, port, rdns, username, password
else:
raise ValueError("Unknown proxy library: {}".format(library))
async def _proxy_connect(self, timeout=None, local_addr=None): async def _proxy_connect(self, timeout=None, local_addr=None):
if isinstance(self._proxy, (tuple, list)):
parsed = self._parse_proxy(*self._proxy)
elif isinstance(self._proxy, dict):
parsed = self._parse_proxy(**self._proxy)
else:
raise TypeError("Proxy of unknown format: {}".format(type(self._proxy)))
# Use `python-socks` library for newer Python >= 3.6 # Always prefer `python_socks` when available
# Use `PySocks` library for older Python <= 3.5 if python_socks:
if sys.version_info >= (3, 6):
import python_socks
# python_socks internal errors are not inherited from # python_socks internal errors are not inherited from
# builtin IOError (just from Exception). Instead of adding those # builtin IOError (just from Exception). Instead of adding those
# in exceptions clauses everywhere through the code, we # in exceptions clauses everywhere through the code, we
@ -130,16 +124,6 @@ class Connection(abc.ABC):
from python_socks.async_.asyncio import Proxy from python_socks.async_.asyncio import Proxy
# We expect a dict/tuple in the format of `PySocks` (for compatibility)
# I.E.: (proxy_type, addr, port, rdns, username, password).
if isinstance(self._proxy, (tuple, list)):
parsed = self._parse_proxy("python-socks", *self._proxy)
elif isinstance(self._proxy, dict):
parsed = self._parse_proxy("python-socks", **self._proxy)
else:
raise TypeError("Proxy of unknown format!", type(self._proxy))
proxy = Proxy.create(*parsed) proxy = Proxy.create(*parsed)
# WARNING: If `local_addr` is set we use manual socket creation, because, # WARNING: If `local_addr` is set we use manual socket creation, because,
@ -154,13 +138,11 @@ class Connection(abc.ABC):
timeout=timeout timeout=timeout
) )
else: else:
# Here we start manual setup of the socket. # Here we start manual setup of the socket.
# The `address` represents the proxy ip and proxy port, # The `address` represents the proxy ip and proxy port,
# not the destination one (!), because the socket # not the destination one (!), because the socket
# connects to the proxy server, not destination server. # connects to the proxy server, not destination server.
# IPv family is also checked on proxy address. # IPv family is also checked on proxy address.
if ':' in proxy.proxy_host: if ':' in proxy.proxy_host:
mode, address = socket.AF_INET6, (proxy.proxy_host, proxy.proxy_port, 0, 0) mode, address = socket.AF_INET6, (proxy.proxy_host, proxy.proxy_port, 0, 0)
else: else:
@ -180,7 +162,6 @@ class Connection(abc.ABC):
# As our socket is already created and connected, # As our socket is already created and connected,
# this call sets the destination host/port and # this call sets the destination host/port and
# starts protocol negotiations with the proxy server. # starts protocol negotiations with the proxy server.
sock = await proxy.connect( sock = await proxy.connect(
dest_host=self._ip, dest_host=self._ip,
dest_port=self._port, dest_port=self._port,
@ -189,23 +170,11 @@ class Connection(abc.ABC):
) )
else: else:
import socks import socks
# We expect a dict/tuple in the format of `PySocks`.
# I.E.: (proxy_type, addr, port, rdns, username, password).
if isinstance(self._proxy, (tuple, list)):
parsed = self._parse_proxy("pysocks", *self._proxy)
elif isinstance(self._proxy, dict):
parsed = self._parse_proxy("pysocks", **self._proxy)
else:
raise TypeError("Proxy of unknown format!", type(self._proxy))
# Here `address` represents destination address (not proxy), because of # Here `address` represents destination address (not proxy), because of
# the `PySocks` implementation of the connection routine. # the `PySocks` implementation of the connection routine.
# IPv family is checked on proxy address, not destination address. # IPv family is checked on proxy address, not destination address.
if ':' in parsed[1]: if ':' in parsed[1]:
mode, address = socket.AF_INET6, (self._ip, self._port, 0, 0) mode, address = socket.AF_INET6, (self._ip, self._port, 0, 0)
else: else:
@ -230,13 +199,10 @@ class Connection(abc.ABC):
return sock return sock
async def _connect(self, timeout=None, ssl=None): async def _connect(self, timeout=None, ssl=None):
if self._local_addr is not None: if self._local_addr is not None:
# NOTE: If port is not specified, we use 0 port # NOTE: If port is not specified, we use 0 port
# to notify the OS that port should be chosen randomly # to notify the OS that port should be chosen randomly
# from the available ones. # from the available ones.
if isinstance(self._local_addr, tuple) and len(self._local_addr) == 2: if isinstance(self._local_addr, tuple) and len(self._local_addr) == 2:
local_addr = self._local_addr local_addr = self._local_addr
elif isinstance(self._local_addr, str): elif isinstance(self._local_addr, str):
@ -255,7 +221,6 @@ class Connection(abc.ABC):
local_addr=local_addr local_addr=local_addr
), timeout=timeout) ), timeout=timeout)
else: else:
# Proxy setup, connection and negotiation is performed here. # Proxy setup, connection and negotiation is performed here.
sock = await self._proxy_connect( sock = await self._proxy_connect(
timeout=timeout, timeout=timeout,