From 9bd310340b423f06f848ffdfa462b2f0d6b040f1 Mon Sep 17 00:00:00 2001 From: hellysmile Date: Thu, 28 Sep 2017 22:08:51 +0300 Subject: [PATCH 01/16] Initial ssl.SSLError support --- aiohttp/client_exceptions.py | 20 ++++++++++++++++++++ aiohttp/connector.py | 14 +++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/aiohttp/client_exceptions.py b/aiohttp/client_exceptions.py index 95fc1d8eb05..6e7ef3ff9ea 100644 --- a/aiohttp/client_exceptions.py +++ b/aiohttp/client_exceptions.py @@ -3,6 +3,12 @@ import asyncio +try: + import ssl +except ImportError: # pragma: no cover + ssl = None + + __all__ = ( 'ClientError', @@ -72,8 +78,13 @@ class ClientConnectorError(ClientOSError): """ def __init__(self, connection_key, os_error): self._conn_key = connection_key + self._os_error = os_error super().__init__(os_error.errno, os_error.strerror) + @property + def os_error(self): + return self._os_error + @property def host(self): return self._conn_key.host @@ -150,3 +161,12 @@ def url(self): def __repr__(self): return '<{} {}>'.format(self.__class__.__name__, self.url) + + +if ssl is not None: + class ClientConnectorSSLError(ClientConnectorError, ssl.SSLError): + """Response ssl error.""" +else: + class ClientConnectorSSLError(ClientConnectorError): + """Dummy wrapper for ClientConnectorSSLError + when ssl module is not available.""" diff --git a/aiohttp/connector.py b/aiohttp/connector.py index b0fe3f43f20..6cb9fe8718d 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -11,7 +11,7 @@ from . import hdrs, helpers from .client_exceptions import (ClientConnectionError, ClientConnectorError, - ClientHttpProxyError, + ClientConnectorSSLError, ClientHttpProxyError, ClientProxyConnectionError, ServerFingerprintMismatch) from .client_proto import ResponseHandler @@ -24,9 +24,13 @@ try: import ssl + + ssl_error = ssl.SSLError except ImportError: # pragma: no cover ssl = None + ssl_error = tuple() + __all__ = ('BaseConnector', 'TCPConnector', 'UnixConnector') @@ -390,6 +394,10 @@ def connect(self, req): if self._closed: proto.close() raise ClientConnectionError("Connector is closed.") + except ClientConnectorSSLError as exc: + raise ClientConnectorSSLError(key, exc.os_error) + except ClientConnectorError as exc: + raise ClientConnectorError(key, exc.os_error) except OSError as exc: raise ClientConnectorError(key, exc) from exc finally: @@ -810,6 +818,10 @@ def _create_direct_connection(self, req): except OSError as e: exc = e else: + # ssl.SSLError has OSError as __bases__ + if isinstance(exc, ssl_error): + raise ClientConnectorSSLError(req, exc) from exc + raise ClientConnectorError(req, exc) from exc @asyncio.coroutine From 1159075dbf81d4d89f94a12233ed3391e230b26b Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 08:29:05 +0300 Subject: [PATCH 02/16] Explicit exc re raise --- aiohttp/connector.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 6cb9fe8718d..2173f91dd24 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -395,9 +395,10 @@ def connect(self, req): proto.close() raise ClientConnectionError("Connector is closed.") except ClientConnectorSSLError as exc: - raise ClientConnectorSSLError(key, exc.os_error) + raise ClientConnectorSSLError( + key, exc.os_error) from exc.os_error except ClientConnectorError as exc: - raise ClientConnectorError(key, exc.os_error) + raise ClientConnectorError(key, exc.os_error) from exc.os_error except OSError as exc: raise ClientConnectorError(key, exc) from exc finally: From db834edb305565f7a88d89bc07aee8523bd97189 Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 08:33:26 +0300 Subject: [PATCH 03/16] Added tests for os_error property --- tests/test_connector.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/test_connector.py b/tests/test_connector.py index 18b1d07c7f1..4a691e5298d 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -586,6 +586,7 @@ def test_connect_connection_error(loop): assert ctx.value.host == req.host assert ctx.value.port == req.port assert ctx.value.ssl == req.ssl + assert ctx.value.os_error is err @asyncio.coroutine @@ -1206,18 +1207,47 @@ def tearDown(self): gc.collect() @asyncio.coroutine - def create_server(self, method, path, handler): + def create_server(self, method, path, handler, sslcontext=None): app = web.Application() app.router.add_route(method, path, handler) port = unused_port() self.handler = app.make_handler(loop=self.loop, tcp_keepalive=False) srv = yield from self.loop.create_server( - self.handler, '127.0.0.1', port) + self.handler, '127.0.0.1', port, ssl=sslcontext) url = "http://127.0.0.1:{}".format(port) + path self.addCleanup(srv.close) return app, srv, url + def test_tcp_connector_client_ssl_error(self): + @asyncio.coroutine + def handler(request): + return web.HTTPOk() + + sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + here = os.path.join(os.path.dirname(__file__), '..', 'tests') + keyfile = os.path.join(here, 'sample.key') + certfile = os.path.join(here, 'sample.crt') + sslcontext.load_cert_chain(certfile, keyfile) + + app, srv, url = self.loop.run_until_complete( + self.create_server('get', '/', handler, + sslcontext=sslcontext) + ) + + port = unused_port() + conn = aiohttp.TCPConnector(loop=self.loop, ssl_context=sslcontext, + local_addr=('127.0.0.1', port)) + + session = aiohttp.ClientSession(connector=conn) + + r = self.loop.run_until_complete( + session.request('get', url) + ) + + session.close() + conn.close() + @asyncio.coroutine def create_unix_server(self, method, path, handler): tmpdir = tempfile.mkdtemp() From 6ad5fb58e5159d8f7b580e9c0e937a65d7a19cee Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 08:39:30 +0300 Subject: [PATCH 04/16] Added basic tests for os_error re raise --- tests/test_connector.py | 59 +++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/tests/test_connector.py b/tests/test_connector.py index 4a691e5298d..c7f52343e95 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -574,8 +574,8 @@ def test_connect_connection_error(loop): conn = aiohttp.BaseConnector(loop=loop) conn._create_connection = mock.Mock() conn._create_connection.return_value = helpers.create_future(loop) - err = OSError(1, 'permission error') - conn._create_connection.return_value.set_exception(err) + os_error = OSError(1, 'permission error') + conn._create_connection.return_value.set_exception(os_error) with pytest.raises(aiohttp.ClientConnectorError) as ctx: req = mock.Mock() @@ -586,7 +586,31 @@ def test_connect_connection_error(loop): assert ctx.value.host == req.host assert ctx.value.port == req.port assert ctx.value.ssl == req.ssl - assert ctx.value.os_error is err + assert ctx.value.os_error is os_error + + +@asyncio.coroutine +def test_connect_connection_error_re_raise(loop): + conn = aiohttp.BaseConnector(loop=loop) + conn._create_connection = mock.Mock() + conn._create_connection.return_value = helpers.create_future(loop) + os_error = OSError(1, 'permission error') + + with pytest.raises(aiohttp.ClientConnectorError) as ctx: + raise aiohttp.ClientConnectorError(conn, os_error) from os_error + + conn._create_connection.return_value.set_exception(ctx.value) + + with pytest.raises(aiohttp.ClientConnectorError) as ctx: + req = mock.Mock() + yield from conn.connect(req) + assert 1 == ctx.value.errno + assert str(ctx.value).startswith('Cannot connect to') + assert str(ctx.value).endswith('[permission error]') + assert ctx.value.host == req.host + assert ctx.value.port == req.port + assert ctx.value.ssl == req.ssl + assert ctx.value.os_error is os_error @asyncio.coroutine @@ -1219,35 +1243,6 @@ def create_server(self, method, path, handler, sslcontext=None): self.addCleanup(srv.close) return app, srv, url - def test_tcp_connector_client_ssl_error(self): - @asyncio.coroutine - def handler(request): - return web.HTTPOk() - - sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - here = os.path.join(os.path.dirname(__file__), '..', 'tests') - keyfile = os.path.join(here, 'sample.key') - certfile = os.path.join(here, 'sample.crt') - sslcontext.load_cert_chain(certfile, keyfile) - - app, srv, url = self.loop.run_until_complete( - self.create_server('get', '/', handler, - sslcontext=sslcontext) - ) - - port = unused_port() - conn = aiohttp.TCPConnector(loop=self.loop, ssl_context=sslcontext, - local_addr=('127.0.0.1', port)) - - session = aiohttp.ClientSession(connector=conn) - - r = self.loop.run_until_complete( - session.request('get', url) - ) - - session.close() - conn.close() - @asyncio.coroutine def create_unix_server(self, method, path, handler): tmpdir = tempfile.mkdtemp() From 8845241d767cd62c43ac8b88a69a556c84b57fdd Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 11:37:59 +0300 Subject: [PATCH 05/16] Added ClientConnectorSSLError to __all__ --- aiohttp/client_exceptions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiohttp/client_exceptions.py b/aiohttp/client_exceptions.py index 6e7ef3ff9ea..c062011cf4e 100644 --- a/aiohttp/client_exceptions.py +++ b/aiohttp/client_exceptions.py @@ -14,6 +14,7 @@ 'ClientConnectionError', 'ClientOSError', 'ClientConnectorError', 'ClientProxyConnectionError', + 'ClientConnectorSSLError', 'ServerConnectionError', 'ServerTimeoutError', 'ServerDisconnectedError', 'ServerFingerprintMismatch', From 9a9c5b1ae919e7c28834fd3dd4a5e0d3799c06ff Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 11:38:13 +0300 Subject: [PATCH 06/16] Added tests for ClientConnectorSSLError --- tests/test_connector.py | 74 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/tests/test_connector.py b/tests/test_connector.py index c7f52343e95..beaf87a9430 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -1231,15 +1231,16 @@ def tearDown(self): gc.collect() @asyncio.coroutine - def create_server(self, method, path, handler, sslcontext=None): + def create_server(self, method, path, handler, ssl_context=None): app = web.Application() app.router.add_route(method, path, handler) port = unused_port() self.handler = app.make_handler(loop=self.loop, tcp_keepalive=False) srv = yield from self.loop.create_server( - self.handler, '127.0.0.1', port, ssl=sslcontext) - url = "http://127.0.0.1:{}".format(port) + path + self.handler, '127.0.0.1', port, ssl=ssl_context) + scheme = 's' if ssl is not None else '' + url = "http{}://127.0.0.1:{}".format(scheme, port) + path self.addCleanup(srv.close) return app, srv, url @@ -1259,13 +1260,48 @@ def create_unix_server(self, method, path, handler): self.addCleanup(srv.close) return app, srv, url, sock_path - def test_tcp_connector_uses_provided_local_addr(self): + def test_tcp_connector_raise_connector_ssl_error(self): @asyncio.coroutine def handler(request): return web.HTTPOk() + here = os.path.join(os.path.dirname(__file__), '..', 'tests') + keyfile = os.path.join(here, 'sample.key') + certfile = os.path.join(here, 'sample.crt') + sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + sslcontext.load_cert_chain(certfile, keyfile) + app, srv, url = self.loop.run_until_complete( - self.create_server('get', '/', handler) + self.create_server('get', '/', handler, ssl_context=sslcontext) + ) + + port = unused_port() + conn = aiohttp.TCPConnector(loop=self.loop, + local_addr=('127.0.0.1', port)) + + session = aiohttp.ClientSession(connector=conn) + + with pytest.raises(aiohttp.ClientConnectorSSLError) as ctx: + self.loop.run_until_complete(session.request('get', url)) + + self.assertIsInstance(ctx.value.os_error, ssl.SSLError) + + session.close() + conn.close() + + def test_tcp_connector_do_not_raise_connector_ssl_error(self): + @asyncio.coroutine + def handler(request): + return web.HTTPOk() + + here = os.path.join(os.path.dirname(__file__), '..', 'tests') + keyfile = os.path.join(here, 'sample.key') + certfile = os.path.join(here, 'sample.crt') + sslcontext = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + sslcontext.load_cert_chain(certfile, keyfile) + + app, srv, url = self.loop.run_until_complete( + self.create_server('get', '/', handler, ssl_context=sslcontext) ) port = unused_port() @@ -1274,13 +1310,39 @@ def handler(request): session = aiohttp.ClientSession(connector=conn) + r = self.loop.run_until_complete( + session.request('get', url, ssl_context=sslcontext)) + + r.release() + first_conn = next(iter(conn._conns.values()))[0][0] + self.assertIs( + first_conn.transport._sslcontext, sslcontext) + r.close() + + session.close() + conn.close() + + def test_tcp_connector_uses_provided_local_addr(self): + @asyncio.coroutine + def handler(request): + return web.HTTPOk() + + app, srv, url = self.loop.run_until_complete( + self.create_server('get', '/', handler) + ) + + port = unused_port() + conn = aiohttp.TCPConnector(loop=self.loop) + + session = aiohttp.ClientSession(connector=conn) + r = self.loop.run_until_complete( session.request('get', url) ) r.release() first_conn = next(iter(conn._conns.values()))[0][0] - self.assertEqual( + self.assertIs( first_conn.transport._sock.getsockname(), ('127.0.0.1', port)) r.close() session.close() From 97d92997c472fd333edafd1700a8d8cfd7773342 Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 11:42:35 +0300 Subject: [PATCH 07/16] Fix typo in test_tcp_connector_uses_provided_local_addr --- tests/test_connector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_connector.py b/tests/test_connector.py index beaf87a9430..d45a1721f2e 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -1342,7 +1342,7 @@ def handler(request): r.release() first_conn = next(iter(conn._conns.values()))[0][0] - self.assertIs( + self.assertEqual( first_conn.transport._sock.getsockname(), ('127.0.0.1', port)) r.close() session.close() From c7f4103072933ba2907f76471b74446a5ec97e97 Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 11:45:49 +0300 Subject: [PATCH 08/16] Added myself to CONTRIBUTORS.txt --- CONTRIBUTORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index edafc7fe7f4..ac891b789f0 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -178,6 +178,7 @@ Vaibhav Sagar Vamsi Krishna Avula Vasiliy Faronov Vasyl Baran +Victor Kovtun Vikas Kawadia Vitalik Verhovodov Vitaly Haritonsky From ba597ffb359360f93d180f299d203b6393d78460 Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 11:52:39 +0300 Subject: [PATCH 09/16] Added changes. --- changes/2294.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changes/2294.feature diff --git a/changes/2294.feature b/changes/2294.feature new file mode 100644 index 00000000000..c1cb49d2ece --- /dev/null +++ b/changes/2294.feature @@ -0,0 +1 @@ +Added `aiohttp.ClientConnectorSSLError` when connection fails due `ssl.SSLError` From 139c31ae739d4ba7b68d316c864e616329aafb88 Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 12:42:00 +0300 Subject: [PATCH 10/16] Fix test_tcp_connector_uses_provided_local_addr --- tests/test_connector.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_connector.py b/tests/test_connector.py index d45a1721f2e..a6354e84a3c 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -1239,7 +1239,7 @@ def create_server(self, method, path, handler, ssl_context=None): self.handler = app.make_handler(loop=self.loop, tcp_keepalive=False) srv = yield from self.loop.create_server( self.handler, '127.0.0.1', port, ssl=ssl_context) - scheme = 's' if ssl is not None else '' + scheme = 's' if ssl_context is not None else '' url = "http{}://127.0.0.1:{}".format(scheme, port) + path self.addCleanup(srv.close) return app, srv, url @@ -1332,7 +1332,8 @@ def handler(request): ) port = unused_port() - conn = aiohttp.TCPConnector(loop=self.loop) + conn = aiohttp.TCPConnector(loop=self.loop, + local_addr=('127.0.0.1', port)) session = aiohttp.ClientSession(connector=conn) From 071897af2debe679340863424e6dff2a12485153 Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 14:51:04 +0300 Subject: [PATCH 11/16] Fix Python versions capability. --- tests/test_connector.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_connector.py b/tests/test_connector.py index a6354e84a3c..89af98208bf 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -1315,8 +1315,13 @@ def handler(request): r.release() first_conn = next(iter(conn._conns.values()))[0][0] - self.assertIs( - first_conn.transport._sslcontext, sslcontext) + + try: + _sslcontext = first_conn.transport._ssl_protocol._sslcontext + except AttributeError: + _sslcontext = first_conn.transport._sslcontext + + self.assertIs(_sslcontext, sslcontext) r.close() session.close() From 2441d25f3c2c47a13456e739cc2c00575647476b Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 15:00:50 +0300 Subject: [PATCH 12/16] Added docs for aiohttp.ClientConnectorSSLError --- docs/client.rst | 9 ++++++++- docs/client_reference.rst | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/client.rst b/docs/client.rst index 06642eec2e4..eada28cca2f 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -542,6 +542,13 @@ same thing as the previous example, but add another call to '/path/to/client/private/device.jey') r = await session.get('https://example.com', ssl_context=sslcontext) +There is explicit error when ssl verification fails +:class:`aiohttp.ClientConnectorSSLError`:: + + try: + await session.get('https://expired.badssl.com/') + except aiohttp.ClientConnectorSSLError as e: + assert isinstance(e, ssl.SSLError) You may also verify certificates via *SHA256* fingerprint:: @@ -808,7 +815,7 @@ For a ``ClientSession`` with SSL, the application must wait a short duration bef # Wait 250 ms for the underlying SSL connections to close loop.run_until_complete(asyncio.sleep(0.250)) loop.close() - + Note that the appropriate amount of time to wait will vary from application to application. All if this will eventually become obsolete when the asyncio internals are changed so that aiohttp itself can wait on the underlying connection to close. Please follow issue `#1925 `_ for the progress on this. diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 55becacaf59..23f63ad6157 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -1642,6 +1642,12 @@ Connection errors Derived from :exc:`ClientOSError` +.. class:: ClientConnectorSSLError + + Response ssl error. + + Derived from :exc:`ClientConnectorError` and :exc:`ssl.SSLError` + .. class:: ClientProxyConnectionError Derived from :exc:`ClientConnectonError` @@ -1692,6 +1698,8 @@ Hierarchy of exceptions * :exc:`ClientConnectorError` + * :exc:`ClientConnectorSSLError` + * :exc:`ClientProxyConnectionError` * :exc:`ServerConnectionError` From 2f1b2dd507308330608ffae26cec0d8ddd5e266b Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 16:48:38 +0300 Subject: [PATCH 13/16] Refactor connectio_key usage. --- aiohttp/client_exceptions.py | 14 ++++++------ aiohttp/client_reqrep.py | 8 +++++++ aiohttp/connector.py | 18 ++++----------- tests/test_connector.py | 44 ------------------------------------ 4 files changed, 19 insertions(+), 65 deletions(-) diff --git a/aiohttp/client_exceptions.py b/aiohttp/client_exceptions.py index c062011cf4e..7c87f0dd62f 100644 --- a/aiohttp/client_exceptions.py +++ b/aiohttp/client_exceptions.py @@ -100,7 +100,7 @@ def ssl(self): def __str__(self): return ('Cannot connect to host {0.host}:{0.port} ssl:{0.ssl} [{1}]' - .format(self._conn_key, self.strerror)) + .format(self, self.strerror)) class ClientProxyConnectionError(ClientConnectorError): @@ -164,10 +164,10 @@ def __repr__(self): return '<{} {}>'.format(self.__class__.__name__, self.url) +ssl_error_bases = [ClientConnectorError] if ssl is not None: - class ClientConnectorSSLError(ClientConnectorError, ssl.SSLError): - """Response ssl error.""" -else: - class ClientConnectorSSLError(ClientConnectorError): - """Dummy wrapper for ClientConnectorSSLError - when ssl module is not available.""" + ssl_error_bases.append(ssl.SSLError) + + +class ClientConnectorSSLError(*ssl_error_bases): + """Response ssl error.""" diff --git a/aiohttp/client_reqrep.py b/aiohttp/client_reqrep.py index 0326c6f6c74..94ed0c6d667 100644 --- a/aiohttp/client_reqrep.py +++ b/aiohttp/client_reqrep.py @@ -6,6 +6,7 @@ import sys import traceback import warnings +from collections import namedtuple from hashlib import md5, sha1, sha256 from http.cookies import CookieError, Morsel @@ -46,6 +47,9 @@ _SSL_OP_NO_COMPRESSION = getattr(ssl, "OP_NO_COMPRESSION", 0) +ConnectionKey = namedtuple('ConnectionKey', ['host', 'port', 'ssl']) + + class ClientRequest: GET_METHODS = {hdrs.METH_GET, hdrs.METH_HEAD, hdrs.METH_OPTIONS} @@ -128,6 +132,10 @@ def __init__(self, method, url, *, self.update_transfer_encoding() self.update_expect_continue(expect100) + @property + def connection_key(self): + return ConnectionKey(self.host, self.port, self.ssl) + @property def host(self): return self.url.host diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 2173f91dd24..7e1a830e4c2 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -3,7 +3,7 @@ import sys import traceback import warnings -from collections import defaultdict, namedtuple +from collections import defaultdict from hashlib import md5, sha1, sha256 from itertools import cycle, islice from time import monotonic @@ -140,9 +140,6 @@ def close(self): pass -ConnectionKey = namedtuple('ConnectionKey', ['host', 'port', 'ssl']) - - class BaseConnector(object): """Base connector class. @@ -353,7 +350,7 @@ def closed(self): @asyncio.coroutine def connect(self, req): """Get from pool or create new connection.""" - key = ConnectionKey(req.host, req.port, req.ssl) + key = req.connection_key if self._limit: # total calc available connections @@ -394,13 +391,6 @@ def connect(self, req): if self._closed: proto.close() raise ClientConnectionError("Connector is closed.") - except ClientConnectorSSLError as exc: - raise ClientConnectorSSLError( - key, exc.os_error) from exc.os_error - except ClientConnectorError as exc: - raise ClientConnectorError(key, exc.os_error) from exc.os_error - except OSError as exc: - raise ClientConnectorError(key, exc) from exc finally: if not self._closed: self._acquired.remove(placeholder) @@ -821,9 +811,9 @@ def _create_direct_connection(self, req): else: # ssl.SSLError has OSError as __bases__ if isinstance(exc, ssl_error): - raise ClientConnectorSSLError(req, exc) from exc + raise ClientConnectorSSLError(req.connection_key, exc) from exc - raise ClientConnectorError(req, exc) from exc + raise ClientConnectorError(req.connection_key, exc) from exc @asyncio.coroutine def _create_proxy_connection(self, req): diff --git a/tests/test_connector.py b/tests/test_connector.py index 89af98208bf..1320c700a01 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -569,50 +569,6 @@ def test_connect(loop): connection.close() -@asyncio.coroutine -def test_connect_connection_error(loop): - conn = aiohttp.BaseConnector(loop=loop) - conn._create_connection = mock.Mock() - conn._create_connection.return_value = helpers.create_future(loop) - os_error = OSError(1, 'permission error') - conn._create_connection.return_value.set_exception(os_error) - - with pytest.raises(aiohttp.ClientConnectorError) as ctx: - req = mock.Mock() - yield from conn.connect(req) - assert 1 == ctx.value.errno - assert str(ctx.value).startswith('Cannot connect to') - assert str(ctx.value).endswith('[permission error]') - assert ctx.value.host == req.host - assert ctx.value.port == req.port - assert ctx.value.ssl == req.ssl - assert ctx.value.os_error is os_error - - -@asyncio.coroutine -def test_connect_connection_error_re_raise(loop): - conn = aiohttp.BaseConnector(loop=loop) - conn._create_connection = mock.Mock() - conn._create_connection.return_value = helpers.create_future(loop) - os_error = OSError(1, 'permission error') - - with pytest.raises(aiohttp.ClientConnectorError) as ctx: - raise aiohttp.ClientConnectorError(conn, os_error) from os_error - - conn._create_connection.return_value.set_exception(ctx.value) - - with pytest.raises(aiohttp.ClientConnectorError) as ctx: - req = mock.Mock() - yield from conn.connect(req) - assert 1 == ctx.value.errno - assert str(ctx.value).startswith('Cannot connect to') - assert str(ctx.value).endswith('[permission error]') - assert ctx.value.host == req.host - assert ctx.value.port == req.port - assert ctx.value.ssl == req.ssl - assert ctx.value.os_error is os_error - - @asyncio.coroutine def test_close_during_connect(loop): proto = mock.Mock() From d79b947a2510d7a03e750e01c8988b1c04ff130b Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 18:06:06 +0300 Subject: [PATCH 14/16] Added aiohttp.ClientConnectorCertificateError --- aiohttp/client_exceptions.py | 44 +++++++++++++++++++++++++++++++++--- aiohttp/connector.py | 24 ++++++++------------ docs/client.rst | 8 +++++++ docs/client_reference.rst | 17 +++++++++----- tests/test_connector.py | 20 ++++++++++++++++ 5 files changed, 90 insertions(+), 23 deletions(-) diff --git a/aiohttp/client_exceptions.py b/aiohttp/client_exceptions.py index 7c87f0dd62f..3bcd5be4431 100644 --- a/aiohttp/client_exceptions.py +++ b/aiohttp/client_exceptions.py @@ -14,7 +14,7 @@ 'ClientConnectionError', 'ClientOSError', 'ClientConnectorError', 'ClientProxyConnectionError', - 'ClientConnectorSSLError', + 'ClientConnectorSSLError', 'ClientConnectorCertificateError', 'ServerConnectionError', 'ServerTimeoutError', 'ServerDisconnectedError', 'ServerFingerprintMismatch', @@ -164,10 +164,48 @@ def __repr__(self): return '<{} {}>'.format(self.__class__.__name__, self.url) -ssl_error_bases = [ClientConnectorError] if ssl is not None: - ssl_error_bases.append(ssl.SSLError) + certificate_errors = (ssl.CertificateError,) + certificate_errors_bases = (ClientConnectorError, ssl.CertificateError,) + + ssl_errors = (ssl.SSLError,) + ssl_error_bases = (ClientConnectorError, ssl.SSLError) +else: + certificate_errors = tuple() + certificate_errors_bases = (ClientConnectorError, ValueError,) + + ssl_errors = tuple() + ssl_error_bases = (ClientConnectorError,) class ClientConnectorSSLError(*ssl_error_bases): """Response ssl error.""" + + +class ClientConnectorCertificateError(*certificate_errors_bases): + """Response certificate error.""" + + def __init__(self, connection_key, certificate_error): + self._conn_key = connection_key + self._certificate_error = certificate_error + + @property + def certificate_error(self): + return self._certificate_error + + @property + def host(self): + return self._conn_key.host + + @property + def port(self): + return self._conn_key.port + + @property + def ssl(self): + return self._conn_key.ssl + + def __str__(self): + return ('Cannot connect to host {0.host}:{0.port} ssl:{0.ssl} ' + '[{0.certificate_error.__class__.__name__}: ' + '{0.certificate_error.args}]'.format(self)) diff --git a/aiohttp/connector.py b/aiohttp/connector.py index 7e1a830e4c2..d85e659fd56 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -10,10 +10,12 @@ from types import MappingProxyType from . import hdrs, helpers -from .client_exceptions import (ClientConnectionError, ClientConnectorError, +from .client_exceptions import (ClientConnectorCertificateError, + ClientConnectionError, ClientConnectorError, ClientConnectorSSLError, ClientHttpProxyError, ClientProxyConnectionError, - ServerFingerprintMismatch) + ServerFingerprintMismatch, certificate_errors, + ssl_errors) from .client_proto import ResponseHandler from .client_reqrep import ClientRequest from .helpers import SimpleCookie, is_ip_address, noop, sentinel @@ -24,13 +26,9 @@ try: import ssl - - ssl_error = ssl.SSLError except ImportError: # pragma: no cover ssl = None - ssl_error = tuple() - __all__ = ('BaseConnector', 'TCPConnector', 'UnixConnector') @@ -774,7 +772,6 @@ def _create_direct_connection(self, req): fingerprint, hashfunc = self._get_fingerprint_and_hashfunc(req) hosts = yield from self._resolve_host(req.url.raw_host, req.port) - exc = None for hinfo in hosts: try: @@ -806,14 +803,13 @@ def _create_direct_connection(self, req): raise ServerFingerprintMismatch( expected, got, host, port) return transp, proto - except OSError as e: - exc = e - else: - # ssl.SSLError has OSError as __bases__ - if isinstance(exc, ssl_error): + except certificate_errors as exc: + raise ClientConnectorCertificateError( + req.connection_key, exc) from exc + except ssl_errors as exc: raise ClientConnectorSSLError(req.connection_key, exc) from exc - - raise ClientConnectorError(req.connection_key, exc) from exc + except OSError as exc: + raise ClientConnectorError(req.connection_key, exc) from exc @asyncio.coroutine def _create_proxy_connection(self, req): diff --git a/docs/client.rst b/docs/client.rst index eada28cca2f..93e528daf95 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -543,6 +543,7 @@ same thing as the previous example, but add another call to r = await session.get('https://example.com', ssl_context=sslcontext) There is explicit error when ssl verification fails + :class:`aiohttp.ClientConnectorSSLError`:: try: @@ -550,6 +551,13 @@ There is explicit error when ssl verification fails except aiohttp.ClientConnectorSSLError as e: assert isinstance(e, ssl.SSLError) +:class:`aiohttp.ClientConnectorCertificateError`:: + + try: + await session.get('https://wrong.host.badssl.com/') + except aiohttp.ClientConnectorCertificateError as e: + assert isinstance(e, ssl.CertificateError) + You may also verify certificates via *SHA256* fingerprint:: # Attempt to connect to https://www.python.org diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 23f63ad6157..251a1cd1d7c 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -1642,12 +1642,6 @@ Connection errors Derived from :exc:`ClientOSError` -.. class:: ClientConnectorSSLError - - Response ssl error. - - Derived from :exc:`ClientConnectorError` and :exc:`ssl.SSLError` - .. class:: ClientProxyConnectionError Derived from :exc:`ClientConnectonError` @@ -1656,6 +1650,17 @@ Connection errors Derived from :exc:`ClientConnectonError` +.. class:: ClientConnectorSSLError + + Response ssl error. + + Derived from :exc:`ClientConnectorError` and :exc:`ssl.SSLError` + +.. class:: ClientConnectorCertificateError + + Response certificate error. + + Derived from :exc:`ClientConnectorError` and :exc:`ssl.CertificateError` .. class:: ServerDisconnectedError diff --git a/tests/test_connector.py b/tests/test_connector.py index 1320c700a01..4878812003e 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -341,6 +341,26 @@ def test_release_close(loop): assert proto.close.called +@asyncio.coroutine +def test_tcp_connector_certificate_error(loop): + req = ClientRequest('GET', URL('https://127.0.0.1:443'), loop=loop) + + @asyncio.coroutine + def certificate_error(*args, **kwargs): + raise ssl.CertificateError + + conn = aiohttp.TCPConnector(loop=loop) + conn._loop.create_connection = certificate_error + + with pytest.raises(aiohttp.ClientConnectorCertificateError) as ctx: + yield from conn.connect(req) + + assert isinstance(ctx.value, ssl.CertificateError) + assert isinstance(ctx.value.certificate_error, ssl.CertificateError) + assert str(ctx.value) == ('Cannot connect to host 127.0.0.1:443 ssl:True ' + '[CertificateError: ()]') + + @asyncio.coroutine def test_tcp_connector_resolve_host(loop): conn = aiohttp.TCPConnector(loop=loop, use_dns_cache=True) From 4743679b06707716e4fc839ebfab997a98f13ae0 Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 18:19:54 +0300 Subject: [PATCH 15/16] Added base ClientSSLError --- aiohttp/client_exceptions.py | 12 +++++++++--- docs/client.rst | 16 +++++++++++++++- docs/client_reference.rst | 15 +++++++++++---- tests/test_connector.py | 2 ++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/aiohttp/client_exceptions.py b/aiohttp/client_exceptions.py index 3bcd5be4431..ef6abcad42e 100644 --- a/aiohttp/client_exceptions.py +++ b/aiohttp/client_exceptions.py @@ -14,6 +14,8 @@ 'ClientConnectionError', 'ClientOSError', 'ClientConnectorError', 'ClientProxyConnectionError', + + 'ClientSSLError', 'ClientConnectorSSLError', 'ClientConnectorCertificateError', 'ServerConnectionError', 'ServerTimeoutError', 'ServerDisconnectedError', @@ -164,15 +166,19 @@ def __repr__(self): return '<{} {}>'.format(self.__class__.__name__, self.url) +class ClientSSLError(ClientConnectorError): + """Base error for ssl.*Errors.""" + + if ssl is not None: certificate_errors = (ssl.CertificateError,) - certificate_errors_bases = (ClientConnectorError, ssl.CertificateError,) + certificate_errors_bases = (ClientSSLError, ssl.CertificateError,) ssl_errors = (ssl.SSLError,) ssl_error_bases = (ClientConnectorError, ssl.SSLError) -else: +else: # pragma: no cover certificate_errors = tuple() - certificate_errors_bases = (ClientConnectorError, ValueError,) + certificate_errors_bases = (ClientSSLError, ValueError,) ssl_errors = tuple() ssl_error_bases = (ClientConnectorError,) diff --git a/docs/client.rst b/docs/client.rst index 93e528daf95..508f55d57ab 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -542,7 +542,7 @@ same thing as the previous example, but add another call to '/path/to/client/private/device.jey') r = await session.get('https://example.com', ssl_context=sslcontext) -There is explicit error when ssl verification fails +There is explicit errors when ssl verification fails :class:`aiohttp.ClientConnectorSSLError`:: @@ -558,6 +558,20 @@ There is explicit error when ssl verification fails except aiohttp.ClientConnectorCertificateError as e: assert isinstance(e, ssl.CertificateError) +If you need to skip both ssl related errors + +:class:`aiohttp.ClientSSLError`:: + + try: + await session.get('https://expired.badssl.com/') + except aiohttp.ClientSSLError as e: + assert isinstance(e, ssl.SSLError) + + try: + await session.get('https://wrong.host.badssl.com/') + except aiohttp.ClientSSLError as e: + assert isinstance(e, ssl.CertificateError) + You may also verify certificates via *SHA256* fingerprint:: # Attempt to connect to https://www.python.org diff --git a/docs/client_reference.rst b/docs/client_reference.rst index 251a1cd1d7c..9b90f69c64c 100644 --- a/docs/client_reference.rst +++ b/docs/client_reference.rst @@ -1626,7 +1626,6 @@ Connection errors These exceptions related to low-level connection problems. - Derived from :exc:`ClientError` .. class:: ClientOSError @@ -1650,17 +1649,21 @@ Connection errors Derived from :exc:`ClientConnectonError` +.. class:: ClientSSLError + + Derived from :exc:`ClientConnectonError` + .. class:: ClientConnectorSSLError Response ssl error. - Derived from :exc:`ClientConnectorError` and :exc:`ssl.SSLError` + Derived from :exc:`ClientSSLError` and :exc:`ssl.SSLError` .. class:: ClientConnectorCertificateError Response certificate error. - Derived from :exc:`ClientConnectorError` and :exc:`ssl.CertificateError` + Derived from :exc:`ClientSSLError` and :exc:`ssl.CertificateError` .. class:: ServerDisconnectedError @@ -1703,7 +1706,11 @@ Hierarchy of exceptions * :exc:`ClientConnectorError` - * :exc:`ClientConnectorSSLError` + * :exc:`ClientSSLError` + + * :exc:`ClientConnectorCertificateError` + + * :exc:`ClientConnectorSSLError` * :exc:`ClientProxyConnectionError` diff --git a/tests/test_connector.py b/tests/test_connector.py index 4878812003e..2cae8b8d768 100644 --- a/tests/test_connector.py +++ b/tests/test_connector.py @@ -357,6 +357,7 @@ def certificate_error(*args, **kwargs): assert isinstance(ctx.value, ssl.CertificateError) assert isinstance(ctx.value.certificate_error, ssl.CertificateError) + assert isinstance(ctx.value, aiohttp.ClientSSLError) assert str(ctx.value) == ('Cannot connect to host 127.0.0.1:443 ssl:True ' '[CertificateError: ()]') @@ -1261,6 +1262,7 @@ def handler(request): self.loop.run_until_complete(session.request('get', url)) self.assertIsInstance(ctx.value.os_error, ssl.SSLError) + self.assertTrue(ctx.value, aiohttp.ClientSSLError) session.close() conn.close() From 586f04260a44b42a75591968ee66a3cbbd2308b7 Mon Sep 17 00:00:00 2001 From: hellysmile Date: Fri, 29 Sep 2017 18:21:00 +0300 Subject: [PATCH 16/16] Happy isort --- aiohttp/connector.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/aiohttp/connector.py b/aiohttp/connector.py index d85e659fd56..9ea367bb425 100644 --- a/aiohttp/connector.py +++ b/aiohttp/connector.py @@ -10,9 +10,10 @@ from types import MappingProxyType from . import hdrs, helpers -from .client_exceptions import (ClientConnectorCertificateError, - ClientConnectionError, ClientConnectorError, - ClientConnectorSSLError, ClientHttpProxyError, +from .client_exceptions import (ClientConnectionError, + ClientConnectorCertificateError, + ClientConnectorError, ClientConnectorSSLError, + ClientHttpProxyError, ClientProxyConnectionError, ServerFingerprintMismatch, certificate_errors, ssl_errors)