| @@ -1,7 +1,7 @@ | |||
| [run] | |||
| branch = True | |||
| source = aiosocks, tests | |||
| omit = site-packages | |||
| omit = site-packages,aiosocks/test_utils.py | |||
| [html] | |||
| directory = coverage | |||
| @@ -18,7 +18,7 @@ install: | |||
| - pip install --upgrade pip wheel | |||
| - pip install --upgrade setuptools | |||
| - pip install pip | |||
| - pip install flake8 | |||
| - pip install flake8==3.3.0 | |||
| - pip install pyflakes==1.1.0 | |||
| - pip install coverage | |||
| - pip install pytest | |||
| @@ -15,7 +15,7 @@ SOCKS proxy client for asyncio and aiohttp | |||
| Dependencies | |||
| ------------ | |||
| python 3.5+ | |||
| aiohttp 2.1+ | |||
| aiohttp 2.3.2+ | |||
| Features | |||
| -------- | |||
| @@ -175,8 +175,10 @@ aiohttp usage | |||
| proxy_auth=ba) as resp: | |||
| if resp.status == 200: | |||
| print(await resp.text()) | |||
| except aiohttp.ProxyConnectionError: | |||
| except aiohttp.ClientProxyConnectionError: | |||
| # connection problem | |||
| except aiohttp.ClientConnectorError: | |||
| # ssl error, certificate error, etc | |||
| except aiosocks.SocksError: | |||
| # communication problem | |||
| @@ -185,22 +187,3 @@ aiohttp usage | |||
| loop = asyncio.get_event_loop() | |||
| loop.run_until_complete(load_github_main()) | |||
| loop.close() | |||
| Proxy from environment | |||
| ^^^^^^^^^^^^^^^^^^^^^^ | |||
| .. code-block:: python | |||
| import os | |||
| from aiosocks.connector import ProxyConnector, ProxyClientRequest | |||
| os.environ['socks4_proxy'] = 'socks4://127.0.0.1:333' | |||
| # or | |||
| os.environ['socks5_proxy'] = 'socks5://127.0.0.1:444' | |||
| conn = ProxyConnector() | |||
| with aiohttp.ClientSession(connector=conn, request_class=ProxyClientRequest) as session: | |||
| async with session.get('http://github.com/', proxy_from_env=True) as resp: | |||
| if resp.status == 200: | |||
| print(await resp.text()) | |||
| @@ -8,7 +8,7 @@ from .helpers import ( | |||
| ) | |||
| from .protocols import Socks4Protocol, Socks5Protocol, DEFAULT_LIMIT | |||
| __version__ = '0.2.4' | |||
| __version__ = '0.2.5' | |||
| __all__ = ('Socks4Protocol', 'Socks5Protocol', 'Socks4Auth', | |||
| 'Socks5Auth', 'Socks4Addr', 'Socks5Addr', 'SocksError', | |||
| @@ -79,7 +79,7 @@ async def create_connection(protocol_factory, proxy, proxy_auth, dst, *, | |||
| try: | |||
| await waiter | |||
| except: | |||
| except: # noqa | |||
| transport.close() | |||
| raise | |||
| @@ -1,30 +1,24 @@ | |||
| try: | |||
| import aiohttp | |||
| from aiohttp.connector import sentinel | |||
| from aiohttp.client_exceptions import certificate_errors, ssl_errors | |||
| except ImportError: | |||
| raise ImportError('aiosocks.SocksConnector require aiohttp library') | |||
| from yarl import URL | |||
| from urllib.request import getproxies | |||
| from .errors import SocksError, SocksConnectionError | |||
| from .errors import SocksConnectionError | |||
| from .helpers import Socks4Auth, Socks5Auth, Socks4Addr, Socks5Addr | |||
| from . import create_connection | |||
| __all__ = ('ProxyConnector', 'ProxyClientRequest') | |||
| class ProxyClientRequest(aiohttp.ClientRequest): | |||
| def update_proxy(self, proxy, proxy_auth, proxy_from_env): | |||
| if proxy_from_env and not proxy: | |||
| proxies = getproxies() | |||
| from distutils.version import StrictVersion | |||
| proxy_url = proxies.get(self.original_url.scheme) | |||
| if not proxy_url: | |||
| proxy_url = proxies.get('socks4') or proxies.get('socks5') | |||
| if StrictVersion(aiohttp.__version__) < StrictVersion('2.3.2'): | |||
| raise RuntimeError('aiosocks.connector depends on aiohttp 2.3.2+') | |||
| proxy = URL(proxy_url) if proxy_url else None | |||
| class ProxyClientRequest(aiohttp.ClientRequest): | |||
| def update_proxy(self, proxy, proxy_auth, proxy_headers): | |||
| if proxy and proxy.scheme not in ['http', 'socks4', 'socks5']: | |||
| raise ValueError( | |||
| "Only http, socks4 and socks5 proxies are supported") | |||
| @@ -41,9 +35,9 @@ class ProxyClientRequest(aiohttp.ClientRequest): | |||
| not isinstance(proxy_auth, Socks5Auth): | |||
| raise ValueError("proxy_auth must be None or Socks5Auth() " | |||
| "tuple for socks5 proxy") | |||
| self.proxy = proxy | |||
| self.proxy_auth = proxy_auth | |||
| self.proxy_headers = proxy_headers | |||
| class ProxyConnector(aiohttp.TCPConnector): | |||
| @@ -69,20 +63,41 @@ class ProxyConnector(aiohttp.TCPConnector): | |||
| else: | |||
| return await self._create_socks_connection(req) | |||
| async def _wrap_create_socks_connection(self, *args, req, **kwargs): | |||
| try: | |||
| return await create_connection(*args, **kwargs) | |||
| except certificate_errors as exc: | |||
| raise aiohttp.ClientConnectorCertificateError( | |||
| req.connection_key, exc) from exc | |||
| except ssl_errors as exc: | |||
| raise aiohttp.ClientConnectorSSLError( | |||
| req.connection_key, exc) from exc | |||
| except (OSError, SocksConnectionError) as exc: | |||
| raise aiohttp.ClientProxyConnectionError( | |||
| req.connection_key, exc) from exc | |||
| async def _create_socks_connection(self, req): | |||
| if req.ssl: | |||
| sslcontext = self.ssl_context | |||
| else: | |||
| sslcontext = None | |||
| sslcontext = self._get_ssl_context(req) | |||
| fingerprint, hashfunc = self._get_fingerprint_and_hashfunc(req) | |||
| if not self._remote_resolve: | |||
| dst_hosts = list(await self._resolve_host(req.host, req.port)) | |||
| dst = dst_hosts[0]['host'], dst_hosts[0]['port'] | |||
| try: | |||
| dst_hosts = list(await self._resolve_host(req.host, req.port)) | |||
| dst = dst_hosts[0]['host'], dst_hosts[0]['port'] | |||
| except OSError as exc: | |||
| raise aiohttp.ClientConnectorError( | |||
| req.connection_key, exc) from exc | |||
| else: | |||
| dst = req.host, req.port | |||
| proxy_hosts = await self._resolve_host(req.proxy.host, req.proxy.port) | |||
| exc = None | |||
| try: | |||
| proxy_hosts = await self._resolve_host( | |||
| req.proxy.host, req.proxy.port) | |||
| except OSError as exc: | |||
| raise aiohttp.ClientConnectorError( | |||
| req.connection_key, exc) from exc | |||
| last_exc = None | |||
| for hinfo in proxy_hosts: | |||
| if req.proxy.scheme == 'socks4': | |||
| @@ -91,45 +106,37 @@ class ProxyConnector(aiohttp.TCPConnector): | |||
| proxy = Socks5Addr(hinfo['host'], hinfo['port']) | |||
| try: | |||
| transp, proto = await create_connection( | |||
| transp, proto = await self._wrap_create_socks_connection( | |||
| self._factory, proxy, req.proxy_auth, dst, | |||
| loop=self._loop, remote_resolve=self._remote_resolve, | |||
| ssl=sslcontext, family=hinfo['family'], | |||
| proto=hinfo['proto'], flags=hinfo['flags'], | |||
| local_addr=self._local_addr, | |||
| local_addr=self._local_addr, req=req, | |||
| server_hostname=req.host if sslcontext else None) | |||
| self._validate_ssl_fingerprint(transp, req.host, req.port) | |||
| return transp, proto | |||
| except (OSError, SocksError, SocksConnectionError) as e: | |||
| exc = e | |||
| except aiohttp.ClientConnectorError as exc: | |||
| last_exc = exc | |||
| continue | |||
| has_cert = transp.get_extra_info('sslcontext') | |||
| if has_cert and fingerprint: | |||
| sock = transp.get_extra_info('socket') | |||
| if not hasattr(sock, 'getpeercert'): | |||
| # Workaround for asyncio 3.5.0 | |||
| # Starting from 3.5.1 version | |||
| # there is 'ssl_object' extra info in transport | |||
| sock = transp._ssl_protocol._sslpipe.ssl_object | |||
| # gives DER-encoded cert as a sequence of bytes (or None) | |||
| cert = sock.getpeercert(binary_form=True) | |||
| assert cert | |||
| got = hashfunc(cert).digest() | |||
| expected = fingerprint | |||
| if got != expected: | |||
| transp.close() | |||
| if not self._cleanup_closed_disabled: | |||
| self._cleanup_closed_transports.append(transp) | |||
| last_exc = aiohttp.ServerFingerprintMismatch( | |||
| expected, got, req.host, req.port) | |||
| continue | |||
| return transp, proto | |||
| else: | |||
| if isinstance(exc, SocksConnectionError): | |||
| raise aiohttp.ClientProxyConnectionError(*exc.args) | |||
| if isinstance(exc, SocksError): | |||
| raise exc | |||
| else: | |||
| raise aiohttp.ClientOSError( | |||
| exc.errno, 'Can not connect to %s:%s [%s]' % | |||
| (req.host, req.port, exc.strerror)) from exc | |||
| def _validate_ssl_fingerprint(self, transp, host, port): | |||
| has_cert = transp.get_extra_info('sslcontext') | |||
| if has_cert and self._fingerprint: | |||
| sock = transp.get_extra_info('socket') | |||
| if not hasattr(sock, 'getpeercert'): | |||
| # Workaround for asyncio 3.5.0 | |||
| # Starting from 3.5.1 version | |||
| # there is 'ssl_object' extra info in transport | |||
| sock = transp._ssl_protocol._sslpipe.ssl_object | |||
| # gives DER-encoded cert as a sequence of bytes (or None) | |||
| cert = sock.getpeercert(binary_form=True) | |||
| assert cert | |||
| got = self._hashfunc(cert).digest() | |||
| expected = self._fingerprint | |||
| if got != expected: | |||
| transp.close() | |||
| if not self._cleanup_closed_disabled: | |||
| self._cleanup_closed_transports.append(transp) | |||
| raise aiohttp.ServerFingerprintMismatch( | |||
| expected, got, host, port) | |||
| raise last_exc | |||
| @@ -113,7 +113,7 @@ class FakeSocks4Srv: | |||
| data.append(byte[0]) | |||
| writer.write(byte) | |||
| await writer.drain() | |||
| except: | |||
| except: # noqa | |||
| break | |||
| def factory(): | |||
| @@ -1,3 +1,5 @@ | |||
| import ssl | |||
| import aiosocks | |||
| import aiohttp | |||
| import pytest | |||
| @@ -9,22 +11,21 @@ from aiosocks.connector import ProxyConnector, ProxyClientRequest | |||
| from aiosocks.helpers import Socks4Auth, Socks5Auth | |||
| async def test_connect_proxy_ip(): | |||
| async def test_connect_proxy_ip(loop): | |||
| tr, proto = mock.Mock(name='transport'), mock.Mock(name='protocol') | |||
| with mock.patch('aiosocks.connector.create_connection', | |||
| make_mocked_coro((tr, proto))): | |||
| loop_mock = mock.Mock() | |||
| loop_mock.getaddrinfo = make_mocked_coro( | |||
| [[0, 0, 0, 0, ['127.0.0.1', 1080]]]) | |||
| loop.getaddrinfo = make_mocked_coro( | |||
| [[0, 0, 0, 0, ['127.0.0.1', 1080]]]) | |||
| req = ProxyClientRequest( | |||
| 'GET', URL('http://python.org'), loop=loop_mock, | |||
| 'GET', URL('http://python.org'), loop=loop, | |||
| proxy=URL('socks5://proxy.org')) | |||
| connector = ProxyConnector(loop=loop_mock) | |||
| connector = ProxyConnector(loop=loop) | |||
| conn = await connector.connect(req) | |||
| assert loop_mock.getaddrinfo.called | |||
| assert loop.getaddrinfo.called | |||
| assert conn.protocol is proto | |||
| conn.close() | |||
| @@ -89,12 +90,32 @@ async def test_connect_locale_resolve(loop): | |||
| conn.close() | |||
| async def test_proxy_connect_fail(loop): | |||
| @pytest.mark.parametrize('remote_resolve', [True, False]) | |||
| async def test_resolve_host_fail(loop, remote_resolve): | |||
| tr, proto = mock.Mock(name='transport'), mock.Mock(name='protocol') | |||
| with mock.patch('aiosocks.connector.create_connection', | |||
| make_mocked_coro((tr, proto))): | |||
| req = ProxyClientRequest( | |||
| 'GET', URL('http://python.org'), loop=loop, | |||
| proxy=URL('socks5://proxy.example')) | |||
| connector = ProxyConnector(loop=loop, remote_resolve=remote_resolve) | |||
| connector._resolve_host = make_mocked_coro(raise_exception=OSError()) | |||
| with pytest.raises(aiohttp.ClientConnectorError): | |||
| await connector.connect(req) | |||
| @pytest.mark.parametrize('exc', [ | |||
| (ssl.CertificateError, aiohttp.ClientConnectorCertificateError), | |||
| (ssl.SSLError, aiohttp.ClientConnectorSSLError), | |||
| (aiosocks.SocksConnectionError, aiohttp.ClientProxyConnectionError)]) | |||
| async def test_proxy_connect_fail(loop, exc): | |||
| loop_mock = mock.Mock() | |||
| loop_mock.getaddrinfo = make_mocked_coro( | |||
| [[0, 0, 0, 0, ['127.0.0.1', 1080]]]) | |||
| cc_coro = make_mocked_coro( | |||
| raise_exception=aiosocks.SocksConnectionError()) | |||
| raise_exception=exc[0]()) | |||
| with mock.patch('aiosocks.connector.create_connection', cc_coro): | |||
| req = ProxyClientRequest( | |||
| @@ -102,7 +123,7 @@ async def test_proxy_connect_fail(loop): | |||
| proxy=URL('socks5://127.0.0.1')) | |||
| connector = ProxyConnector(loop=loop_mock) | |||
| with pytest.raises(aiohttp.ClientConnectionError): | |||
| with pytest.raises(exc[1]): | |||
| await connector.connect(req) | |||
| @@ -177,38 +198,3 @@ def test_proxy_client_request_invalid(loop): | |||
| proxy=URL('socks5://proxy.org'), proxy_auth=Socks4Auth('l')) | |||
| assert 'proxy_auth must be None or Socks5Auth() ' \ | |||
| 'tuple for socks5 proxy' in str(cm) | |||
| def test_proxy_from_env_http(loop): | |||
| proxies = {'http': 'http://proxy.org'} | |||
| with mock.patch('aiosocks.connector.getproxies', return_value=proxies): | |||
| req = ProxyClientRequest('GET', URL('http://python.org'), loop=loop) | |||
| req.update_proxy(None, None, True) | |||
| assert req.proxy == URL('http://proxy.org') | |||
| req.original_url = URL('https://python.org') | |||
| req.update_proxy(None, None, True) | |||
| assert req.proxy is None | |||
| proxies.update({'https': 'http://proxy.org', | |||
| 'socks4': 'socks4://127.0.0.1:33', | |||
| 'socks5': 'socks5://localhost:44'}) | |||
| req.update_proxy(None, None, True) | |||
| assert req.proxy == URL('http://proxy.org') | |||
| def test_proxy_from_env_socks(loop): | |||
| proxies = {'socks4': 'socks4://127.0.0.1:33', | |||
| 'socks5': 'socks5://localhost:44'} | |||
| with mock.patch('aiosocks.connector.getproxies', return_value=proxies): | |||
| req = ProxyClientRequest('GET', URL('http://python.org'), loop=loop) | |||
| req.update_proxy(None, None, True) | |||
| assert req.proxy == URL('socks4://127.0.0.1:33') | |||
| del proxies['socks4'] | |||
| req.update_proxy(None, None, True) | |||
| assert req.proxy == URL('socks5://localhost:44') | |||