Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ssl.SSLError explicit support #2297

Merged
merged 20 commits into from
Oct 4, 2017
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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