Skip to content

Commit

Permalink
ssl.SSLError explicit support (#2297)
Browse files Browse the repository at this point in the history
* Initial ssl.SSLError support

* Explicit exc re raise

* Added tests for os_error property

* Added basic tests for os_error re raise

* Added ClientConnectorSSLError to __all__

* Added tests for ClientConnectorSSLError

* Fix typo in test_tcp_connector_uses_provided_local_addr

* Added myself to CONTRIBUTORS.txt

* Added changes.

* Fix test_tcp_connector_uses_provided_local_addr

* Fix Python versions capability.

* Added docs for aiohttp.ClientConnectorSSLError

* Refactor connectio_key usage.

* Added aiohttp.ClientConnectorCertificateError

* Added base ClientSSLError

* Happy isort
  • Loading branch information
hellysmile authored and asvetlov committed Oct 4, 2017
1 parent 540da48 commit 803510b
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 39 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ Vaibhav Sagar
Vamsi Krishna Avula
Vasiliy Faronov
Vasyl Baran
Victor Kovtun
Vikas Kawadia
Vitalik Verhovodov
Vitaly Haritonsky
Expand Down
67 changes: 66 additions & 1 deletion aiohttp/client_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,21 @@
import asyncio


try:
import ssl
except ImportError: # pragma: no cover
ssl = None


__all__ = (
'ClientError',

'ClientConnectionError',
'ClientOSError', 'ClientConnectorError', 'ClientProxyConnectionError',

'ClientSSLError',
'ClientConnectorSSLError', 'ClientConnectorCertificateError',

'ServerConnectionError', 'ServerTimeoutError', 'ServerDisconnectedError',
'ServerFingerprintMismatch',

Expand Down Expand Up @@ -72,8 +81,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
Expand All @@ -88,7 +102,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):
Expand Down Expand Up @@ -150,3 +164,54 @@ def url(self):

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 = (ClientSSLError, ssl.CertificateError,)

ssl_errors = (ssl.SSLError,)
ssl_error_bases = (ClientConnectorError, ssl.SSLError)
else: # pragma: no cover
certificate_errors = tuple()
certificate_errors_bases = (ClientSSLError, 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))
8 changes: 8 additions & 0 deletions aiohttp/client_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -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
Expand Down
28 changes: 14 additions & 14 deletions aiohttp/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
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
from types import MappingProxyType

from . import hdrs, helpers
from .client_exceptions import (ClientConnectionError, ClientConnectorError,
from .client_exceptions import (ClientConnectionError,
ClientConnectorCertificateError,
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
Expand Down Expand Up @@ -136,9 +139,6 @@ def close(self):
pass


ConnectionKey = namedtuple('ConnectionKey', ['host', 'port', 'ssl'])


class BaseConnector(object):
"""Base connector class.
Expand Down Expand Up @@ -349,7 +349,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
Expand Down Expand Up @@ -390,8 +390,6 @@ def connect(self, req):
if self._closed:
proto.close()
raise ClientConnectionError("Connector is closed.")
except OSError as exc:
raise ClientConnectorError(key, exc) from exc
finally:
if not self._closed:
self._acquired.remove(placeholder)
Expand Down Expand Up @@ -775,7 +773,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:
Expand Down Expand Up @@ -807,10 +804,13 @@ def _create_direct_connection(self, req):
raise ServerFingerprintMismatch(
expected, got, host, port)
return transp, proto
except OSError as e:
exc = e
else:
raise ClientConnectorError(req, exc) from exc
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
except OSError as exc:
raise ClientConnectorError(req.connection_key, exc) from exc

@asyncio.coroutine
def _create_proxy_connection(self, req):
Expand Down
1 change: 1 addition & 0 deletions changes/2294.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added `aiohttp.ClientConnectorSSLError` when connection fails due `ssl.SSLError`
31 changes: 30 additions & 1 deletion docs/client.rst
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,35 @@ 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 errors 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)

:class:`aiohttp.ClientConnectorCertificateError`::

try:
await session.get('https://wrong.host.badssl.com/')
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::

Expand Down Expand Up @@ -808,7 +837,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 <https://github.com/aio-libs/aiohttp/issues/1925>`_ for the progress on this.
22 changes: 21 additions & 1 deletion docs/client_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1626,7 +1626,6 @@ Connection errors

These exceptions related to low-level connection problems.


Derived from :exc:`ClientError`

.. class:: ClientOSError
Expand All @@ -1650,6 +1649,21 @@ Connection errors

Derived from :exc:`ClientConnectonError`

.. class:: ClientSSLError

Derived from :exc:`ClientConnectonError`

.. class:: ClientConnectorSSLError

Response ssl error.

Derived from :exc:`ClientSSLError` and :exc:`ssl.SSLError`

.. class:: ClientConnectorCertificateError

Response certificate error.

Derived from :exc:`ClientSSLError` and :exc:`ssl.CertificateError`

.. class:: ServerDisconnectedError

Expand Down Expand Up @@ -1692,6 +1706,12 @@ Hierarchy of exceptions

* :exc:`ClientConnectorError`

* :exc:`ClientSSLError`

* :exc:`ClientConnectorCertificateError`

* :exc:`ClientConnectorSSLError`

* :exc:`ClientProxyConnectionError`

* :exc:`ServerConnectionError`
Expand Down
Loading

0 comments on commit 803510b

Please sign in to comment.