ssl / asyncio: трассировка даже при обработке ошибки - PullRequest
0 голосов
/ 25 августа 2018

Попытка загрузить и обработать JPEG с URL.Моя проблема не в том, что проверка сертификата завершается неудачно для некоторых 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

Ответы [ 2 ]

0 голосов
/ 26 августа 2018

Трассировка генерируется реализацией протокола asyncio протокола SSL, который вызывает обработчик исключений цикла событий . Через лабиринт взаимодействий между транспортным и потоковым интерфейсом случается так, что это исключение одновременно регистрируется в цикле событий и передается пользователю API. Вот как это происходит:

  • Исключение возникает при рукопожатии SSL.
  • SSLProtocol._on_handshake_complete получает не None handshake_exc и обрабатывает его как "фатальную ошибку" (в контексте рукопожатия), то есть вызывает self._fatal_error и возвращает.
  • _fatal_error вызывает обработчик исключений цикла событий для регистрации ошибки. Обычно обработчик вызывается для исключений, возникающих в обратных вызовах, поставленных в очередь, когда вызывающий абонент больше не передает их, поэтому он просто регистрирует трассировку до стандартной ошибки, чтобы исключить тихое прохождение исключения. Однако ...
  • _fatal_error продолжает звонить transport._force_close, что вызывает connection_lost обратно по протоколу.
  • Реализация connection_lost протокола потокового чтения устанавливает исключение как результат будущего потокового чтения, передавая его тем пользователям ожидающего его потокового API.

Не очевидно, если это ошибка или функция, одно и то же исключение регистрируется в цикле событий и передается connection_lost. Это может быть обходной путь для BaseProtocol.connection_lost, поскольку определяет 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)

Добавление with suppress_ssl_exception_report() вокруг кода в fetch_url подавляет нежелательный возврат.

Вышеописанное работает, но это сильно похоже на обход основной проблемы, а не на правильное использование API, поэтому я подал отчет об ошибке в трекер.

0 голосов
/ 25 августа 2018

По неизвестной причине (ошибка?) 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')

# ...

PS Я думаю, вы можете использовать более распространенный тип исключения для перехвата клиента ошибки , например:

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