web-dev-qa-db-ja.com

ssl / asyncio:エラーが処理された場合でもトレースバック

URLからjpegをダウンロードして処理しようとしています。私の問題は、これらのURLが古く、信頼できない可能性があるため、証明書の検証が一部のURLで失敗することではありませんが、try...except...SSLCertVerificationError、私はまだトレースバックを取得します。

システム:Linux 4.17.14-Arch1-1-Arch、python 3.7.0-3、aiohttp 3.3.2

最小限の例:

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

出力:

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
11
deasmhumnha

トレースバックは、イベントループの 例外ハンドラー を呼び出すSSLプロトコルのasyncioの実装によって生成されます。トランスポートとストリーミングインターフェースの間の相互作用の迷路を通じて、この例外はイベントループによってログに記録され、APIユーザーに伝達されます。それが起こる方法は次のとおりです:

  • SSLハンドシェイク中に例外が発生します。
  • _SSLProtocol._on_handshake_complete_ は、Non __handshake_exc_を受信せず、それを(ハンドシェイクコンテキストで)「致命的なエラー」として扱います。つまり、_self._fatal_error_を呼び出して戻ります。
  • __fatal_error_ イベントループの例外ハンドラを呼び出してエラーを記録します。ハンドラーは通常、キューに入れられたコールバックで発生する例外に対して呼び出されます。例外は伝搬する呼び出し元がなくなったため、トレースバックを標準エラーに記録し、例外がサイレントに渡されないようにします。しかしながら...
  • __fatal_error_は、プロトコルで_transport._force_close_を呼び出す _connection_lost_ を呼び出します。
  • ストリームリーダープロトコルの_connection_lost_ implementation は、ストリームリーダーの将来の結果として例外を設定し、それを待っているストリームAPIのユーザーに伝播します。

同じ例外がイベントループによってログに記録され、_connection_lost_に渡されるのはバグか機能かは明らかではありません。これは、_BaseProtocol.connection_lost_が defined a no-op であることの回避策になる可能性があるため、追加のログにより、BaseProtocolから単純に継承するプロトコルが機密情報である可能性を沈黙させないことが保証されますSSLハンドシェイク中に発生する例外。理由が何であれ、現在の動作はOPが経験する問題につながります。例外をキャッチしてもそれを抑制するには不十分であり、トレースバックは引き続き記録されます。

この問題を回避するには、例外ハンドラーを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)
_

_fetch_url_のコードの周囲にwith suppress_ssl_exception_report()を追加すると、不要なトレースバックが抑制されます。

上記は機能しますが、根本的な問題の回避策のように強く感じられ、APIの正しい使用法ではないので、トラッカーに バグレポート を提出しました。

13
user4815162342

不明な理由(バグ?)のため、aiohttpは例外がスローされる前でもエラー出力をコンソールに出力します。 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。クライアントをキャッチするために、より一般的な例外タイプ errors を使用できると思います。次に例を示します。

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