web-dev-qa-db-fra.com

ssl/asyncio: traceback même lorsque l'erreur est gérée

Essayer de télécharger et de traiter des fichiers JPEG à partir d’URL. Mon problème n'est pas que la vérification du certificat échoue pour certaines URL, car ces URL sont anciennes et peuvent ne plus être fiables, mais que lorsque je try...except... la SSLCertVerificationError, j'obtiens toujours le suivi.

Système: Linux 4.17.14-Arch1-1-Arch, python 3.7.0-3, aiohttp 3.3.2

Exemple minimal:

import asyncio
import aiohttp
from ssl import SSLCertVerificationError

async def fetch_url(url, client):
    try:
        async with client.get(url) as resp:
            print(resp.status)
            print(await resp.read())
    except SSLCertVerificationError as e:
        print('Error handled')

async def main(urls):
    tasks = []
    async with aiohttp.ClientSession(loop=loop) as client:
        for url in urls:
            task = asyncio.ensure_future(fetch_url(url, client))
            tasks.append(task)
        return await asyncio.gather(*tasks)

loop = asyncio.get_event_loop()
loop.run_until_complete(main(['https://images.photos.com/']))

Sortie:

SSL handshake failed on verifying the certificate
protocol: <asyncio.sslproto.SSLProtocol object at 0x7ffbecad8ac8>
transport: <_SelectorSocketTransport fd=6 read=polling write=<idle, bufsize=0>>
Traceback (most recent call last):
  File "/usr/lib/python3.7/asyncio/sslproto.py", line 625, in _on_handshake_complete
    raise handshake_exc
  File "/usr/lib/python3.7/asyncio/sslproto.py", line 189, in feed_ssldata
    self._sslobj.do_handshake()
  File "/usr/lib/python3.7/ssl.py", line 763, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'images.photos.com'. (_ssl.c:1045)
SSL error in data received
protocol: <asyncio.sslproto.SSLProtocol object at 0x7ffbecad8ac8>
transport: <_SelectorSocketTransport closing fd=6 read=idle write=<idle, bufsize=0>>
Traceback (most recent call last):
  File "/usr/lib/python3.7/asyncio/sslproto.py", line 526, in data_received
    ssldata, appdata = self._sslpipe.feed_ssldata(data)
  File "/usr/lib/python3.7/asyncio/sslproto.py", line 189, in feed_ssldata
    self._sslobj.do_handshake()
  File "/usr/lib/python3.7/ssl.py", line 763, in do_handshake
    self._sslobj.do_handshake()
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'images.photos.com'. (_ssl.c:1045)
Error handled
6
deasmhumnha

Le suivi est généré par l'implémentation asyncio du protocole SSL, qui appelle le gestionnaire d'exception de la boucle d'événements . À travers un labyrinthe d'interactions entre le transport et l'interface de transmission en continu, il se trouve que cette exception est à la fois consignée par la boucle d'événement et propagée à l'utilisateur de l'API. La façon dont cela se passe est la suivante:

  • Une exception se produit lors de l'établissement de la liaison SSL.
  • SSLProtocol._on_handshake_complete reçoit un handshake_exc non néant et le traite comme une "erreur fatale" (dans le contexte de l'établissement de liaison), c'est-à-dire qu'il appelle self._fatal_error et le renvoie.
  • _fatal_error appelle le gestionnaire d'exceptions de la boucle d'événements pour consigner l'erreur. Le gestionnaire est normalement appelé pour les exceptions qui se produisent dans les rappels en file d'attente où il n'y a plus d'appelant à qui les propager, il enregistre donc le suivi en erreur standard pour s'assurer que l'exception ne se transmet pas en silence. Toutefois...
  • _fatal_error continue à appeler transport._force_close , qui appelle connection_lost sur le protocole.
  • connection_lostimplementation du protocole de lecteur de flux définit l’exception comme le résultat de l’avenir du lecteur de flux, ce qui la transmet aux utilisateurs de l’API de flux qui l’attend.

Il n'est pas évident s'il s'agit d'un bogue ou d'une fonctionnalité que la même exception soit à la fois enregistrée par la boucle d'événements et transmise à connection_lost. Cela pourrait être une solution de contournement pour que BaseProtocol.connection_lost soit défini comme un no-op , de sorte que le journal supplémentaire garantit qu'un protocole qui hérite simplement de BaseProtocol ne met pas au silence les exceptions éventuellement sensibles se produisant lors de la négociation SSL. Quelle que soit la raison, le comportement actuel entraîne le problème rencontré par l'OP: attraper l'exception ne suffit pas pour la supprimer, une trace est toujours enregistrée.

Pour contourner le problème, vous pouvez définir temporairement le gestionnaire d'exceptions sur un gestionnaire qui ne signale pas SSLCertVerificationError:

@contextlib.contextmanager
def suppress_ssl_exception_report():
    loop = asyncio.get_event_loop()
    old_handler = loop.get_exception_handler()
    old_handler_fn = old_handler or lambda _loop, ctx: loop.default_exception_handler(ctx)
    def ignore_exc(_loop, ctx):
        exc = ctx.get('exception')
        if isinstance(exc, SSLCertVerificationError):
            return
        old_handler_fn(loop, ctx)
    loop.set_exception_handler(ignore_exc)
    try:
        yield
    finally:
        loop.set_exception_handler(old_handler)

L'ajout de with suppress_ssl_exception_report() autour du code dans fetch_url supprime le suivi non souhaité.

4
user4815162342

Pour une raison inconnue (bug?), Aiohttp affiche le résultat de l'erreur sur la console avant même la levée d'une exception. Vous pouvez éviter la sortie d’erreurs de redirection temporaire avec contextlib.redirect_stderr :

import asyncio
import aiohttp
from ssl import SSLCertVerificationError

import os
from contextlib import redirect_stderr


async def fetch_url(url, client):
    try:

        f = open(os.devnull, 'w')
        with redirect_stderr(f):  # ignore any error output inside context

            async with client.get(url) as resp:
                print(resp.status)
                print(await resp.read())
    except SSLCertVerificationError as e:
        print('Error handled')

# ...

P.S. Je pense que vous pouvez utiliser un type d'exception plus commun pour intercepter le client errors , par exemple:

except aiohttp.ClientConnectionError as e:
    print('Error handled')
1
Mikhail Gerasimov