wake-up-neo.com

ssl/asyncio: traceback auch bei fehlerbehandlung

Versuch, JPEGs von URLs herunterzuladen und zu verarbeiten. Mein Problem ist nicht, dass die Zertifikatüberprüfung für einige URLs fehlschlägt, da diese URLs alt sind und möglicherweise nicht mehr vertrauenswürdig sind. Wenn ich jedoch den SSLCertVerificationError-Code try...except...e, erhalte ich immer noch den Traceback.

System: Linux 4.17.14-Arch1-1-Arch, Python 3.7.0-3, aiohttp 3.3.2

Minimales Beispiel:

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/']))

Ausgabe:

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

Das Traceback wird durch die Implementierung des SSL-Protokolls durch asyncio generiert, das den Ausnahmehandler der Ereignisschleife aufruft. Durch ein Labyrinth von Interaktionen zwischen dem Transport und der Streaming-Schnittstelle wird diese Ausnahme sowohl von der Ereignisschleife protokolliert als auch an den API-Benutzer weitergegeben. Die Art und Weise, wie es geschieht, ist wie folgt:

  • Während des SSL-Handshakes tritt eine Ausnahme auf.
  • SSLProtocol._on_handshake_complete empfängt Nicht-Keinen handshake_exc und behandelt ihn als "schwerwiegenden Fehler" (im Handshake-Kontext), d. h. ruft self._fatal_error auf und kehrt zurück.
  • _fatal_error ruft den Ausnahmehandler der Ereignisschleife auf, um den Fehler zu protokollieren. Der Handler wird normalerweise für Ausnahmen aufgerufen, die in Rückrufen in der Warteschlange auftreten und an die kein Anrufer mehr weitergegeben werden kann. Er protokolliert den Rückverfolgungsfehler lediglich, um sicherzustellen, dass die Ausnahme nicht unbemerkt weitergegeben wird. Jedoch...
  • _fatal_error ruft dann transport._force_close auf, wodurch connection_lost vom Protokoll zurückgerufen wird.
  • Der connection_lostImplementierung des Stream-Reader-Protokolls legt die Ausnahme als Ergebnis der Zukunft des Stream-Readers fest und gibt sie so an die Benutzer der Stream-API weiter, die darauf warten.

Es ist nicht offensichtlich, ob es sich um einen Fehler oder eine Funktion handelt, bei der dieselbe Ausnahme von der Ereignisschleife protokolliert und an connection_lost übergeben wird. Es könnte eine Problemumgehung sein, wenn BaseProtocol.connection_lostals No-Op definiert ist, sodass das zusätzliche Protokoll sicherstellt, dass ein Protokoll, das einfach von BaseProtocol erbt, die möglicherweise sensiblen Ausnahmen, die während des SSL-Handshakes auftreten, nicht stumm schaltet. Was auch immer der Grund ist, das aktuelle Verhalten führt zu dem Problem, das beim OP auftritt: Das Abfangen der Ausnahme reicht nicht aus, um sie zu unterdrücken, ein Traceback wird dennoch protokolliert.

Um das Problem zu umgehen, können Sie den Ausnahmehandler vorübergehend auf einen festlegen, der keine SSLCertVerificationError meldet:

@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)

Durch Hinzufügen von with suppress_ssl_exception_report() um den Code in fetch_url wird das unerwünschte Traceback unterdrückt.

4
user4815162342

Aus unbekannten Gründen (Fehler?) Druckt aiohttp die Fehlerausgabe auf die Konsole, noch bevor eine Ausnahme ausgelöst wird. Sie können die vorübergehende Umleitung der Fehlerausgabe mit contextlib.redirect_stderr vermeiden:

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. Ich denke, Sie können einen allgemeineren Ausnahmetyp verwenden, um Client- fehler abzufangen, zum Beispiel:

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