Skip to content

Commit d6fef33

Browse files
authored
Merge pull request #21 from nibrag/connector-fix
Connector fix
2 parents 15d4c06 + e4cfa0b commit d6fef33

File tree

7 files changed

+105
-129
lines changed

7 files changed

+105
-129
lines changed

.coveragerc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[run]
22
branch = True
33
source = aiosocks, tests
4-
omit = site-packages
4+
omit = site-packages,aiosocks/test_utils.py
55

66
[html]
77
directory = coverage

.travis.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ install:
1818
- pip install --upgrade pip wheel
1919
- pip install --upgrade setuptools
2020
- pip install pip
21-
- pip install flake8
21+
- pip install flake8==3.3.0
2222
- pip install pyflakes==1.1.0
2323
- pip install coverage
2424
- pip install pytest

README.rst

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ SOCKS proxy client for asyncio and aiohttp
1515
Dependencies
1616
------------
1717
python 3.5+
18-
aiohttp 2.1+
18+
aiohttp 2.3.2+
1919

2020
Features
2121
--------
@@ -175,8 +175,10 @@ aiohttp usage
175175
proxy_auth=ba) as resp:
176176
if resp.status == 200:
177177
print(await resp.text())
178-
except aiohttp.ProxyConnectionError:
178+
except aiohttp.ClientProxyConnectionError:
179179
# connection problem
180+
except aiohttp.ClientConnectorError:
181+
# ssl error, certificate error, etc
180182
except aiosocks.SocksError:
181183
# communication problem
182184
@@ -185,22 +187,3 @@ aiohttp usage
185187
loop = asyncio.get_event_loop()
186188
loop.run_until_complete(load_github_main())
187189
loop.close()
188-
189-
Proxy from environment
190-
^^^^^^^^^^^^^^^^^^^^^^
191-
192-
.. code-block:: python
193-
194-
import os
195-
from aiosocks.connector import ProxyConnector, ProxyClientRequest
196-
197-
os.environ['socks4_proxy'] = 'socks4://127.0.0.1:333'
198-
# or
199-
os.environ['socks5_proxy'] = 'socks5://127.0.0.1:444'
200-
201-
conn = ProxyConnector()
202-
203-
with aiohttp.ClientSession(connector=conn, request_class=ProxyClientRequest) as session:
204-
async with session.get('http://github.com/', proxy_from_env=True) as resp:
205-
if resp.status == 200:
206-
print(await resp.text())

aiosocks/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
)
99
from .protocols import Socks4Protocol, Socks5Protocol, DEFAULT_LIMIT
1010

11-
__version__ = '0.2.4'
11+
__version__ = '0.2.5'
1212

1313
__all__ = ('Socks4Protocol', 'Socks5Protocol', 'Socks4Auth',
1414
'Socks5Auth', 'Socks4Addr', 'Socks5Addr', 'SocksError',
@@ -79,7 +79,7 @@ def socks_factory():
7979

8080
try:
8181
await waiter
82-
except:
82+
except: # noqa
8383
transport.close()
8484
raise
8585

aiosocks/connector.py

Lines changed: 65 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,24 @@
11
try:
22
import aiohttp
33
from aiohttp.connector import sentinel
4+
from aiohttp.client_exceptions import certificate_errors, ssl_errors
45
except ImportError:
56
raise ImportError('aiosocks.SocksConnector require aiohttp library')
6-
7-
from yarl import URL
8-
from urllib.request import getproxies
9-
10-
from .errors import SocksError, SocksConnectionError
7+
from .errors import SocksConnectionError
118
from .helpers import Socks4Auth, Socks5Auth, Socks4Addr, Socks5Addr
129
from . import create_connection
1310

1411
__all__ = ('ProxyConnector', 'ProxyClientRequest')
1512

1613

17-
class ProxyClientRequest(aiohttp.ClientRequest):
18-
def update_proxy(self, proxy, proxy_auth, proxy_from_env):
19-
if proxy_from_env and not proxy:
20-
proxies = getproxies()
14+
from distutils.version import StrictVersion
2115

22-
proxy_url = proxies.get(self.original_url.scheme)
23-
if not proxy_url:
24-
proxy_url = proxies.get('socks4') or proxies.get('socks5')
16+
if StrictVersion(aiohttp.__version__) < StrictVersion('2.3.2'):
17+
raise RuntimeError('aiosocks.connector depends on aiohttp 2.3.2+')
2518

26-
proxy = URL(proxy_url) if proxy_url else None
2719

20+
class ProxyClientRequest(aiohttp.ClientRequest):
21+
def update_proxy(self, proxy, proxy_auth, proxy_headers):
2822
if proxy and proxy.scheme not in ['http', 'socks4', 'socks5']:
2923
raise ValueError(
3024
"Only http, socks4 and socks5 proxies are supported")
@@ -41,9 +35,9 @@ def update_proxy(self, proxy, proxy_auth, proxy_from_env):
4135
not isinstance(proxy_auth, Socks5Auth):
4236
raise ValueError("proxy_auth must be None or Socks5Auth() "
4337
"tuple for socks5 proxy")
44-
4538
self.proxy = proxy
4639
self.proxy_auth = proxy_auth
40+
self.proxy_headers = proxy_headers
4741

4842

4943
class ProxyConnector(aiohttp.TCPConnector):
@@ -69,20 +63,41 @@ async def _create_proxy_connection(self, req):
6963
else:
7064
return await self._create_socks_connection(req)
7165

66+
async def _wrap_create_socks_connection(self, *args, req, **kwargs):
67+
try:
68+
return await create_connection(*args, **kwargs)
69+
except certificate_errors as exc:
70+
raise aiohttp.ClientConnectorCertificateError(
71+
req.connection_key, exc) from exc
72+
except ssl_errors as exc:
73+
raise aiohttp.ClientConnectorSSLError(
74+
req.connection_key, exc) from exc
75+
except (OSError, SocksConnectionError) as exc:
76+
raise aiohttp.ClientProxyConnectionError(
77+
req.connection_key, exc) from exc
78+
7279
async def _create_socks_connection(self, req):
73-
if req.ssl:
74-
sslcontext = self.ssl_context
75-
else:
76-
sslcontext = None
80+
sslcontext = self._get_ssl_context(req)
81+
fingerprint, hashfunc = self._get_fingerprint_and_hashfunc(req)
7782

7883
if not self._remote_resolve:
79-
dst_hosts = list(await self._resolve_host(req.host, req.port))
80-
dst = dst_hosts[0]['host'], dst_hosts[0]['port']
84+
try:
85+
dst_hosts = list(await self._resolve_host(req.host, req.port))
86+
dst = dst_hosts[0]['host'], dst_hosts[0]['port']
87+
except OSError as exc:
88+
raise aiohttp.ClientConnectorError(
89+
req.connection_key, exc) from exc
8190
else:
8291
dst = req.host, req.port
8392

84-
proxy_hosts = await self._resolve_host(req.proxy.host, req.proxy.port)
85-
exc = None
93+
try:
94+
proxy_hosts = await self._resolve_host(
95+
req.proxy.host, req.proxy.port)
96+
except OSError as exc:
97+
raise aiohttp.ClientConnectorError(
98+
req.connection_key, exc) from exc
99+
100+
last_exc = None
86101

87102
for hinfo in proxy_hosts:
88103
if req.proxy.scheme == 'socks4':
@@ -91,45 +106,37 @@ async def _create_socks_connection(self, req):
91106
proxy = Socks5Addr(hinfo['host'], hinfo['port'])
92107

93108
try:
94-
transp, proto = await create_connection(
109+
transp, proto = await self._wrap_create_socks_connection(
95110
self._factory, proxy, req.proxy_auth, dst,
96111
loop=self._loop, remote_resolve=self._remote_resolve,
97112
ssl=sslcontext, family=hinfo['family'],
98113
proto=hinfo['proto'], flags=hinfo['flags'],
99-
local_addr=self._local_addr,
114+
local_addr=self._local_addr, req=req,
100115
server_hostname=req.host if sslcontext else None)
101-
102-
self._validate_ssl_fingerprint(transp, req.host, req.port)
103-
return transp, proto
104-
except (OSError, SocksError, SocksConnectionError) as e:
105-
exc = e
116+
except aiohttp.ClientConnectorError as exc:
117+
last_exc = exc
118+
continue
119+
120+
has_cert = transp.get_extra_info('sslcontext')
121+
if has_cert and fingerprint:
122+
sock = transp.get_extra_info('socket')
123+
if not hasattr(sock, 'getpeercert'):
124+
# Workaround for asyncio 3.5.0
125+
# Starting from 3.5.1 version
126+
# there is 'ssl_object' extra info in transport
127+
sock = transp._ssl_protocol._sslpipe.ssl_object
128+
# gives DER-encoded cert as a sequence of bytes (or None)
129+
cert = sock.getpeercert(binary_form=True)
130+
assert cert
131+
got = hashfunc(cert).digest()
132+
expected = fingerprint
133+
if got != expected:
134+
transp.close()
135+
if not self._cleanup_closed_disabled:
136+
self._cleanup_closed_transports.append(transp)
137+
last_exc = aiohttp.ServerFingerprintMismatch(
138+
expected, got, req.host, req.port)
139+
continue
140+
return transp, proto
106141
else:
107-
if isinstance(exc, SocksConnectionError):
108-
raise aiohttp.ClientProxyConnectionError(*exc.args)
109-
if isinstance(exc, SocksError):
110-
raise exc
111-
else:
112-
raise aiohttp.ClientOSError(
113-
exc.errno, 'Can not connect to %s:%s [%s]' %
114-
(req.host, req.port, exc.strerror)) from exc
115-
116-
def _validate_ssl_fingerprint(self, transp, host, port):
117-
has_cert = transp.get_extra_info('sslcontext')
118-
if has_cert and self._fingerprint:
119-
sock = transp.get_extra_info('socket')
120-
if not hasattr(sock, 'getpeercert'):
121-
# Workaround for asyncio 3.5.0
122-
# Starting from 3.5.1 version
123-
# there is 'ssl_object' extra info in transport
124-
sock = transp._ssl_protocol._sslpipe.ssl_object
125-
# gives DER-encoded cert as a sequence of bytes (or None)
126-
cert = sock.getpeercert(binary_form=True)
127-
assert cert
128-
got = self._hashfunc(cert).digest()
129-
expected = self._fingerprint
130-
if got != expected:
131-
transp.close()
132-
if not self._cleanup_closed_disabled:
133-
self._cleanup_closed_transports.append(transp)
134-
raise aiohttp.ServerFingerprintMismatch(
135-
expected, got, host, port)
142+
raise last_exc

aiosocks/test_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ async def retranslator(self, reader, writer):
113113
data.append(byte[0])
114114
writer.write(byte)
115115
await writer.drain()
116-
except:
116+
except: # noqa
117117
break
118118

119119
def factory():

tests/test_connector.py

Lines changed: 31 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import ssl
2+
13
import aiosocks
24
import aiohttp
35
import pytest
@@ -9,22 +11,21 @@
911
from aiosocks.helpers import Socks4Auth, Socks5Auth
1012

1113

12-
async def test_connect_proxy_ip():
14+
async def test_connect_proxy_ip(loop):
1315
tr, proto = mock.Mock(name='transport'), mock.Mock(name='protocol')
1416

1517
with mock.patch('aiosocks.connector.create_connection',
1618
make_mocked_coro((tr, proto))):
17-
loop_mock = mock.Mock()
18-
loop_mock.getaddrinfo = make_mocked_coro(
19-
[[0, 0, 0, 0, ['127.0.0.1', 1080]]])
19+
loop.getaddrinfo = make_mocked_coro(
20+
[[0, 0, 0, 0, ['127.0.0.1', 1080]]])
2021

2122
req = ProxyClientRequest(
22-
'GET', URL('http://python.org'), loop=loop_mock,
23+
'GET', URL('http://python.org'), loop=loop,
2324
proxy=URL('socks5://proxy.org'))
24-
connector = ProxyConnector(loop=loop_mock)
25+
connector = ProxyConnector(loop=loop)
2526
conn = await connector.connect(req)
2627

27-
assert loop_mock.getaddrinfo.called
28+
assert loop.getaddrinfo.called
2829
assert conn.protocol is proto
2930

3031
conn.close()
@@ -89,20 +90,40 @@ async def test_connect_locale_resolve(loop):
8990
conn.close()
9091

9192

92-
async def test_proxy_connect_fail(loop):
93+
@pytest.mark.parametrize('remote_resolve', [True, False])
94+
async def test_resolve_host_fail(loop, remote_resolve):
95+
tr, proto = mock.Mock(name='transport'), mock.Mock(name='protocol')
96+
97+
with mock.patch('aiosocks.connector.create_connection',
98+
make_mocked_coro((tr, proto))):
99+
req = ProxyClientRequest(
100+
'GET', URL('http://python.org'), loop=loop,
101+
proxy=URL('socks5://proxy.example'))
102+
connector = ProxyConnector(loop=loop, remote_resolve=remote_resolve)
103+
connector._resolve_host = make_mocked_coro(raise_exception=OSError())
104+
105+
with pytest.raises(aiohttp.ClientConnectorError):
106+
await connector.connect(req)
107+
108+
109+
@pytest.mark.parametrize('exc', [
110+
(ssl.CertificateError, aiohttp.ClientConnectorCertificateError),
111+
(ssl.SSLError, aiohttp.ClientConnectorSSLError),
112+
(aiosocks.SocksConnectionError, aiohttp.ClientProxyConnectionError)])
113+
async def test_proxy_connect_fail(loop, exc):
93114
loop_mock = mock.Mock()
94115
loop_mock.getaddrinfo = make_mocked_coro(
95116
[[0, 0, 0, 0, ['127.0.0.1', 1080]]])
96117
cc_coro = make_mocked_coro(
97-
raise_exception=aiosocks.SocksConnectionError())
118+
raise_exception=exc[0]())
98119

99120
with mock.patch('aiosocks.connector.create_connection', cc_coro):
100121
req = ProxyClientRequest(
101122
'GET', URL('http://python.org'), loop=loop,
102123
proxy=URL('socks5://127.0.0.1'))
103124
connector = ProxyConnector(loop=loop_mock)
104125

105-
with pytest.raises(aiohttp.ClientConnectionError):
126+
with pytest.raises(exc[1]):
106127
await connector.connect(req)
107128

108129

@@ -177,38 +198,3 @@ def test_proxy_client_request_invalid(loop):
177198
proxy=URL('socks5://proxy.org'), proxy_auth=Socks4Auth('l'))
178199
assert 'proxy_auth must be None or Socks5Auth() ' \
179200
'tuple for socks5 proxy' in str(cm)
180-
181-
182-
def test_proxy_from_env_http(loop):
183-
proxies = {'http': 'http://proxy.org'}
184-
185-
with mock.patch('aiosocks.connector.getproxies', return_value=proxies):
186-
req = ProxyClientRequest('GET', URL('http://python.org'), loop=loop)
187-
req.update_proxy(None, None, True)
188-
assert req.proxy == URL('http://proxy.org')
189-
190-
req.original_url = URL('https://python.org')
191-
req.update_proxy(None, None, True)
192-
assert req.proxy is None
193-
194-
proxies.update({'https': 'http://proxy.org',
195-
'socks4': 'socks4://127.0.0.1:33',
196-
'socks5': 'socks5://localhost:44'})
197-
req.update_proxy(None, None, True)
198-
assert req.proxy == URL('http://proxy.org')
199-
200-
201-
def test_proxy_from_env_socks(loop):
202-
proxies = {'socks4': 'socks4://127.0.0.1:33',
203-
'socks5': 'socks5://localhost:44'}
204-
205-
with mock.patch('aiosocks.connector.getproxies', return_value=proxies):
206-
req = ProxyClientRequest('GET', URL('http://python.org'), loop=loop)
207-
208-
req.update_proxy(None, None, True)
209-
assert req.proxy == URL('socks4://127.0.0.1:33')
210-
211-
del proxies['socks4']
212-
213-
req.update_proxy(None, None, True)
214-
assert req.proxy == URL('socks5://localhost:44')

0 commit comments

Comments
 (0)