|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
This module contains provisional support for SOCKS proxies from within
|
|
|
|
urllib3. This module supports SOCKS4, SOCKS4A (an extension of SOCKS4), and
|
|
|
|
SOCKS5. To enable its functionality, either install PySocks or install this
|
|
|
|
module with the ``socks`` extra.
|
|
|
|
|
|
|
|
The SOCKS implementation supports the full range of urllib3 features. It also
|
|
|
|
supports the following SOCKS features:
|
|
|
|
|
|
|
|
- SOCKS4A (``proxy_url='socks4a://...``)
|
|
|
|
- SOCKS4 (``proxy_url='socks4://...``)
|
|
|
|
- SOCKS5 with remote DNS (``proxy_url='socks5h://...``)
|
|
|
|
- SOCKS5 with local DNS (``proxy_url='socks5://...``)
|
|
|
|
- Usernames and passwords for the SOCKS proxy
|
|
|
|
|
|
|
|
.. note::
|
|
|
|
It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in
|
|
|
|
your ``proxy_url`` to ensure that DNS resolution is done from the remote
|
|
|
|
server instead of client-side when connecting to a domain name.
|
|
|
|
|
|
|
|
SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5
|
|
|
|
supports IPv4, IPv6, and domain names.
|
|
|
|
|
|
|
|
When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url``
|
|
|
|
will be sent as the ``userid`` section of the SOCKS request::
|
|
|
|
|
|
|
|
proxy_url="socks4a://<userid>@proxy-host"
|
|
|
|
|
|
|
|
When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion
|
|
|
|
of the ``proxy_url`` will be sent as the username/password to authenticate
|
|
|
|
with the proxy::
|
|
|
|
|
|
|
|
proxy_url="socks5h://<username>:<password>@proxy-host"
|
|
|
|
|
|
|
|
"""
|
|
|
|
from __future__ import absolute_import
|
|
|
|
|
|
|
|
try:
|
|
|
|
import socks
|
|
|
|
except ImportError:
|
|
|
|
import warnings
|
|
|
|
from ..exceptions import DependencyWarning
|
|
|
|
|
|
|
|
warnings.warn((
|
|
|
|
'SOCKS support in urllib3 requires the installation of optional '
|
|
|
|
'dependencies: specifically, PySocks. For more information, see '
|
|
|
|
'https://urllib3.readthedocs.io/en/latest/contrib.html#socks-proxies'
|
|
|
|
),
|
|
|
|
DependencyWarning
|
|
|
|
)
|
|
|
|
raise
|
|
|
|
|
|
|
|
from socket import error as SocketError, timeout as SocketTimeout
|
|
|
|
|
|
|
|
from ..connection import (
|
|
|
|
HTTPConnection, HTTPSConnection
|
|
|
|
)
|
|
|
|
from ..connectionpool import (
|
|
|
|
HTTPConnectionPool, HTTPSConnectionPool
|
|
|
|
)
|
|
|
|
from ..exceptions import ConnectTimeoutError, NewConnectionError
|
|
|
|
from ..poolmanager import PoolManager
|
|
|
|
from ..util.url import parse_url
|
|
|
|
|
|
|
|
try:
|
|
|
|
import ssl
|
|
|
|
except ImportError:
|
|
|
|
ssl = None
|
|
|
|
|
|
|
|
|
|
|
|
class SOCKSConnection(HTTPConnection):
|
|
|
|
"""
|
|
|
|
A plain-text HTTP connection that connects via a SOCKS proxy.
|
|
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
self._socks_options = kwargs.pop('_socks_options')
|
|
|
|
super(SOCKSConnection, self).__init__(*args, **kwargs)
|
|
|
|
|
|
|
|
def _new_conn(self):
|
|
|
|
"""
|
|
|
|
Establish a new connection via the SOCKS proxy.
|
|
|
|
"""
|
|
|
|
extra_kw = {}
|
|
|
|
if self.source_address:
|
|
|
|
extra_kw['source_address'] = self.source_address
|
|
|
|
|
|
|
|
if self.socket_options:
|
|
|
|
extra_kw['socket_options'] = self.socket_options
|
|
|
|
|
|
|
|
try:
|
|
|
|
conn = socks.create_connection(
|
|
|
|
(self.host, self.port),
|
|
|
|
proxy_type=self._socks_options['socks_version'],
|
|
|
|
proxy_addr=self._socks_options['proxy_host'],
|
|
|
|
proxy_port=self._socks_options['proxy_port'],
|
|
|
|
proxy_username=self._socks_options['username'],
|
|
|
|
proxy_password=self._socks_options['password'],
|
|
|
|
proxy_rdns=self._socks_options['rdns'],
|
|
|
|
timeout=self.timeout,
|
|
|
|
**extra_kw
|
|
|
|
)
|
|
|
|
|
|
|
|
except SocketTimeout:
|
|
|
|
raise ConnectTimeoutError(
|
|
|
|
self, "Connection to %s timed out. (connect timeout=%s)" %
|
|
|
|
(self.host, self.timeout))
|
|
|
|
|
|
|
|
except socks.ProxyError as e:
|
|
|
|
# This is fragile as hell, but it seems to be the only way to raise
|
|
|
|
# useful errors here.
|
|
|
|
if e.socket_err:
|
|
|
|
error = e.socket_err
|
|
|
|
if isinstance(error, SocketTimeout):
|
|
|
|
raise ConnectTimeoutError(
|
|
|
|
self,
|
|
|
|
"Connection to %s timed out. (connect timeout=%s)" %
|
|
|
|
(self.host, self.timeout)
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
raise NewConnectionError(
|
|
|
|
self,
|
|
|
|
"Failed to establish a new connection: %s" % error
|
|
|
|
)
|
|
|
|
else:
|
|
|
|
raise NewConnectionError(
|
|
|
|
self,
|
|
|
|
"Failed to establish a new connection: %s" % e
|
|
|
|
)
|
|
|
|
|
|
|
|
except SocketError as e: # Defensive: PySocks should catch all these.
|
|
|
|
raise NewConnectionError(
|
|
|
|
self, "Failed to establish a new connection: %s" % e)
|
|
|
|
|
|
|
|
return conn
|
|
|
|
|
|
|
|
|
|
|
|
# We don't need to duplicate the Verified/Unverified distinction from
|
|
|
|
# urllib3/connection.py here because the HTTPSConnection will already have been
|
|
|
|
# correctly set to either the Verified or Unverified form by that module. This
|
|
|
|
# means the SOCKSHTTPSConnection will automatically be the correct type.
|
|
|
|
class SOCKSHTTPSConnection(SOCKSConnection, HTTPSConnection):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
class SOCKSHTTPConnectionPool(HTTPConnectionPool):
|
|
|
|
ConnectionCls = SOCKSConnection
|
|
|
|
|
|
|
|
|
|
|
|
class SOCKSHTTPSConnectionPool(HTTPSConnectionPool):
|
|
|
|
ConnectionCls = SOCKSHTTPSConnection
|
|
|
|
|
|
|
|
|
|
|
|
class SOCKSProxyManager(PoolManager):
|
|
|
|
"""
|
|
|
|
A version of the urllib3 ProxyManager that routes connections via the
|
|
|
|
defined SOCKS proxy.
|
|
|
|
"""
|
|
|
|
pool_classes_by_scheme = {
|
|
|
|
'http': SOCKSHTTPConnectionPool,
|
|
|
|
'https': SOCKSHTTPSConnectionPool,
|
|
|
|
}
|
|
|
|
|
|
|
|
def __init__(self, proxy_url, username=None, password=None,
|
|
|
|
num_pools=10, headers=None, **connection_pool_kw):
|
|
|
|
parsed = parse_url(proxy_url)
|
|
|
|
|
|
|
|
if username is None and password is None and parsed.auth is not None:
|
|
|
|
split = parsed.auth.split(':')
|
|
|
|
if len(split) == 2:
|
|
|
|
username, password = split
|
|
|
|
if parsed.scheme == 'socks5':
|
|
|
|
socks_version = socks.PROXY_TYPE_SOCKS5
|
|
|
|
rdns = False
|
|
|
|
elif parsed.scheme == 'socks5h':
|
|
|
|
socks_version = socks.PROXY_TYPE_SOCKS5
|
|
|
|
rdns = True
|
|
|
|
elif parsed.scheme == 'socks4':
|
|
|
|
socks_version = socks.PROXY_TYPE_SOCKS4
|
|
|
|
rdns = False
|
|
|
|
elif parsed.scheme == 'socks4a':
|
|
|
|
socks_version = socks.PROXY_TYPE_SOCKS4
|
|
|
|
rdns = True
|
|
|
|
else:
|
|
|
|
raise ValueError(
|
|
|
|
"Unable to determine SOCKS version from %s" % proxy_url
|
|
|
|
)
|
|
|
|
|
|
|
|
self.proxy_url = proxy_url
|
|
|
|
|
|
|
|
socks_options = {
|
|
|
|
'socks_version': socks_version,
|
|
|
|
'proxy_host': parsed.host,
|
|
|
|
'proxy_port': parsed.port,
|
|
|
|
'username': username,
|
|
|
|
'password': password,
|
|
|
|
'rdns': rdns
|
|
|
|
}
|
|
|
|
connection_pool_kw['_socks_options'] = socks_options
|
|
|
|
|
|
|
|
super(SOCKSProxyManager, self).__init__(
|
|
|
|
num_pools, headers, **connection_pool_kw
|
|
|
|
)
|
|
|
|
|
|
|
|
self.pool_classes_by_scheme = SOCKSProxyManager.pool_classes_by_scheme
|