mirror of
https://github.com/rembo10/headphones.git
synced 2026-05-23 03:47:45 +01:00
Initial python3 changes
Mostly just updating libraries, removing string encoding/decoding, fixing some edge cases. No new functionality was added in this commit.
This commit is contained in:
@@ -0,0 +1,763 @@
|
||||
"""Tests for TLS support."""
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim: set fileencoding=utf-8 :
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import functools
|
||||
import json
|
||||
import os
|
||||
import ssl
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import OpenSSL.SSL
|
||||
import pytest
|
||||
import requests
|
||||
import six
|
||||
import trustme
|
||||
|
||||
from .._compat import bton, ntob, ntou
|
||||
from .._compat import IS_ABOVE_OPENSSL10, IS_CI, IS_PYPY
|
||||
from .._compat import IS_LINUX, IS_MACOS, IS_WINDOWS
|
||||
from ..server import HTTPServer, get_ssl_adapter_class
|
||||
from ..testing import (
|
||||
ANY_INTERFACE_IPV4,
|
||||
ANY_INTERFACE_IPV6,
|
||||
EPHEMERAL_PORT,
|
||||
# get_server_client,
|
||||
_get_conn_data,
|
||||
_probe_ipv6_sock,
|
||||
)
|
||||
from ..wsgi import Gateway_10
|
||||
|
||||
|
||||
IS_GITHUB_ACTIONS_WORKFLOW = bool(os.getenv('GITHUB_WORKFLOW'))
|
||||
IS_WIN2016 = (
|
||||
IS_WINDOWS
|
||||
# pylint: disable=unsupported-membership-test
|
||||
and b'Microsoft Windows Server 2016 Datacenter' in subprocess.check_output(
|
||||
('systeminfo',),
|
||||
)
|
||||
)
|
||||
IS_LIBRESSL_BACKEND = ssl.OPENSSL_VERSION.startswith('LibreSSL')
|
||||
IS_PYOPENSSL_SSL_VERSION_1_0 = (
|
||||
OpenSSL.SSL.SSLeay_version(OpenSSL.SSL.SSLEAY_VERSION).
|
||||
startswith(b'OpenSSL 1.0.')
|
||||
)
|
||||
PY27 = sys.version_info[:2] == (2, 7)
|
||||
PY34 = sys.version_info[:2] == (3, 4)
|
||||
PY3 = not six.PY2
|
||||
PY310_PLUS = sys.version_info[:2] >= (3, 10)
|
||||
|
||||
|
||||
_stdlib_to_openssl_verify = {
|
||||
ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE,
|
||||
ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER,
|
||||
ssl.CERT_REQUIRED:
|
||||
OpenSSL.SSL.VERIFY_PEER + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT,
|
||||
}
|
||||
|
||||
|
||||
fails_under_py3 = pytest.mark.xfail(
|
||||
not six.PY2,
|
||||
reason='Fails under Python 3+',
|
||||
)
|
||||
|
||||
|
||||
fails_under_py3_in_pypy = pytest.mark.xfail(
|
||||
not six.PY2 and IS_PYPY,
|
||||
reason='Fails under PyPy3',
|
||||
)
|
||||
|
||||
|
||||
missing_ipv6 = pytest.mark.skipif(
|
||||
not _probe_ipv6_sock('::1'),
|
||||
reason=''
|
||||
'IPv6 is disabled '
|
||||
'(for example, under Travis CI '
|
||||
'which runs under GCE supporting only IPv4)',
|
||||
)
|
||||
|
||||
|
||||
class HelloWorldGateway(Gateway_10):
|
||||
"""Gateway responding with Hello World to root URI."""
|
||||
|
||||
def respond(self):
|
||||
"""Respond with dummy content via HTTP."""
|
||||
req = self.req
|
||||
req_uri = bton(req.uri)
|
||||
if req_uri == '/':
|
||||
req.status = b'200 OK'
|
||||
req.ensure_headers_sent()
|
||||
req.write(b'Hello world!')
|
||||
return
|
||||
if req_uri == '/env':
|
||||
req.status = b'200 OK'
|
||||
req.ensure_headers_sent()
|
||||
env = self.get_environ()
|
||||
# drop files so that it can be json dumped
|
||||
env.pop('wsgi.errors')
|
||||
env.pop('wsgi.input')
|
||||
print(env)
|
||||
req.write(json.dumps(env).encode('utf-8'))
|
||||
return
|
||||
return super(HelloWorldGateway, self).respond()
|
||||
|
||||
|
||||
def make_tls_http_server(bind_addr, ssl_adapter, request):
|
||||
"""Create and start an HTTP server bound to ``bind_addr``."""
|
||||
httpserver = HTTPServer(
|
||||
bind_addr=bind_addr,
|
||||
gateway=HelloWorldGateway,
|
||||
)
|
||||
# httpserver.gateway = HelloWorldGateway
|
||||
httpserver.ssl_adapter = ssl_adapter
|
||||
|
||||
threading.Thread(target=httpserver.safe_start).start()
|
||||
|
||||
while not httpserver.ready:
|
||||
time.sleep(0.1)
|
||||
|
||||
request.addfinalizer(httpserver.stop)
|
||||
|
||||
return httpserver
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_http_server(request):
|
||||
"""Provision a server creator as a fixture."""
|
||||
return functools.partial(make_tls_http_server, request=request)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ca():
|
||||
"""Provide a certificate authority via fixture."""
|
||||
return trustme.CA()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_ca_certificate_pem_path(ca):
|
||||
"""Provide a certificate authority certificate file via fixture."""
|
||||
with ca.cert_pem.tempfile() as ca_cert_pem:
|
||||
yield ca_cert_pem
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_certificate(ca):
|
||||
"""Provide a leaf certificate via fixture."""
|
||||
interface, _host, _port = _get_conn_data(ANY_INTERFACE_IPV4)
|
||||
return ca.issue_cert(ntou(interface))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_certificate_chain_pem_path(tls_certificate):
|
||||
"""Provide a certificate chain PEM file path via fixture."""
|
||||
with tls_certificate.private_key_and_cert_chain_pem.tempfile() as cert_pem:
|
||||
yield cert_pem
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tls_certificate_private_key_pem_path(tls_certificate):
|
||||
"""Provide a certificate private key PEM file path via fixture."""
|
||||
with tls_certificate.private_key_pem.tempfile() as cert_key_pem:
|
||||
yield cert_key_pem
|
||||
|
||||
|
||||
def _thread_except_hook(exceptions, args):
|
||||
"""Append uncaught exception ``args`` in threads to ``exceptions``."""
|
||||
if issubclass(args.exc_type, SystemExit):
|
||||
return
|
||||
# cannot store the exception, it references the thread's stack
|
||||
exceptions.append((
|
||||
args.exc_type,
|
||||
str(args.exc_value),
|
||||
''.join(
|
||||
traceback.format_exception(
|
||||
args.exc_type, args.exc_value, args.exc_traceback,
|
||||
),
|
||||
),
|
||||
))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def thread_exceptions():
|
||||
"""Provide a list of uncaught exceptions from threads via a fixture.
|
||||
|
||||
Only catches exceptions on Python 3.8+.
|
||||
The list contains: ``(type, str(value), str(traceback))``
|
||||
"""
|
||||
exceptions = []
|
||||
# Python 3.8+
|
||||
orig_hook = getattr(threading, 'excepthook', None)
|
||||
if orig_hook is not None:
|
||||
threading.excepthook = functools.partial(
|
||||
_thread_except_hook, exceptions,
|
||||
)
|
||||
try:
|
||||
yield exceptions
|
||||
finally:
|
||||
if orig_hook is not None:
|
||||
threading.excepthook = orig_hook
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'adapter_type',
|
||||
(
|
||||
'builtin',
|
||||
'pyopenssl',
|
||||
),
|
||||
)
|
||||
def test_ssl_adapters(
|
||||
tls_http_server, adapter_type,
|
||||
tls_certificate,
|
||||
tls_certificate_chain_pem_path,
|
||||
tls_certificate_private_key_pem_path,
|
||||
tls_ca_certificate_pem_path,
|
||||
):
|
||||
"""Test ability to connect to server via HTTPS using adapters."""
|
||||
interface, _host, port = _get_conn_data(ANY_INTERFACE_IPV4)
|
||||
tls_adapter_cls = get_ssl_adapter_class(name=adapter_type)
|
||||
tls_adapter = tls_adapter_cls(
|
||||
tls_certificate_chain_pem_path, tls_certificate_private_key_pem_path,
|
||||
)
|
||||
if adapter_type == 'pyopenssl':
|
||||
tls_adapter.context = tls_adapter.get_context()
|
||||
|
||||
tls_certificate.configure_cert(tls_adapter.context)
|
||||
|
||||
tlshttpserver = tls_http_server((interface, port), tls_adapter)
|
||||
|
||||
# testclient = get_server_client(tlshttpserver)
|
||||
# testclient.get('/')
|
||||
|
||||
interface, _host, port = _get_conn_data(
|
||||
tlshttpserver.bind_addr,
|
||||
)
|
||||
|
||||
resp = requests.get(
|
||||
'https://{host!s}:{port!s}/'.format(host=interface, port=port),
|
||||
verify=tls_ca_certificate_pem_path,
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.text == 'Hello world!'
|
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: C901 # FIXME
|
||||
'adapter_type',
|
||||
(
|
||||
'builtin',
|
||||
'pyopenssl',
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
('is_trusted_cert', 'tls_client_identity'),
|
||||
(
|
||||
(True, 'localhost'), (True, '127.0.0.1'),
|
||||
(True, '*.localhost'), (True, 'not_localhost'),
|
||||
(False, 'localhost'),
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
'tls_verify_mode',
|
||||
(
|
||||
ssl.CERT_NONE, # server shouldn't validate client cert
|
||||
ssl.CERT_OPTIONAL, # same as CERT_REQUIRED in client mode, don't use
|
||||
ssl.CERT_REQUIRED, # server should validate if client cert CA is OK
|
||||
),
|
||||
)
|
||||
@pytest.mark.xfail(
|
||||
IS_PYPY and IS_CI,
|
||||
reason='Fails under PyPy in CI for unknown reason',
|
||||
strict=False,
|
||||
)
|
||||
def test_tls_client_auth( # noqa: C901 # FIXME
|
||||
# FIXME: remove twisted logic, separate tests
|
||||
mocker,
|
||||
tls_http_server, adapter_type,
|
||||
ca,
|
||||
tls_certificate,
|
||||
tls_certificate_chain_pem_path,
|
||||
tls_certificate_private_key_pem_path,
|
||||
tls_ca_certificate_pem_path,
|
||||
is_trusted_cert, tls_client_identity,
|
||||
tls_verify_mode,
|
||||
):
|
||||
"""Verify that client TLS certificate auth works correctly."""
|
||||
test_cert_rejection = (
|
||||
tls_verify_mode != ssl.CERT_NONE
|
||||
and not is_trusted_cert
|
||||
)
|
||||
interface, _host, port = _get_conn_data(ANY_INTERFACE_IPV4)
|
||||
|
||||
client_cert_root_ca = ca if is_trusted_cert else trustme.CA()
|
||||
with mocker.mock_module.patch(
|
||||
'idna.core.ulabel',
|
||||
return_value=ntob(tls_client_identity),
|
||||
):
|
||||
client_cert = client_cert_root_ca.issue_cert(
|
||||
ntou(tls_client_identity),
|
||||
)
|
||||
del client_cert_root_ca
|
||||
|
||||
with client_cert.private_key_and_cert_chain_pem.tempfile() as cl_pem:
|
||||
tls_adapter_cls = get_ssl_adapter_class(name=adapter_type)
|
||||
tls_adapter = tls_adapter_cls(
|
||||
tls_certificate_chain_pem_path,
|
||||
tls_certificate_private_key_pem_path,
|
||||
)
|
||||
if adapter_type == 'pyopenssl':
|
||||
tls_adapter.context = tls_adapter.get_context()
|
||||
tls_adapter.context.set_verify(
|
||||
_stdlib_to_openssl_verify[tls_verify_mode],
|
||||
lambda conn, cert, errno, depth, preverify_ok: preverify_ok,
|
||||
)
|
||||
else:
|
||||
tls_adapter.context.verify_mode = tls_verify_mode
|
||||
|
||||
ca.configure_trust(tls_adapter.context)
|
||||
tls_certificate.configure_cert(tls_adapter.context)
|
||||
|
||||
tlshttpserver = tls_http_server((interface, port), tls_adapter)
|
||||
|
||||
interface, _host, port = _get_conn_data(tlshttpserver.bind_addr)
|
||||
|
||||
make_https_request = functools.partial(
|
||||
requests.get,
|
||||
'https://{host!s}:{port!s}/'.format(host=interface, port=port),
|
||||
|
||||
# Server TLS certificate verification:
|
||||
verify=tls_ca_certificate_pem_path,
|
||||
|
||||
# Client TLS certificate verification:
|
||||
cert=cl_pem,
|
||||
)
|
||||
|
||||
if not test_cert_rejection:
|
||||
resp = make_https_request()
|
||||
is_req_successful = resp.status_code == 200
|
||||
if (
|
||||
not is_req_successful
|
||||
and IS_PYOPENSSL_SSL_VERSION_1_0
|
||||
and adapter_type == 'builtin'
|
||||
and tls_verify_mode == ssl.CERT_REQUIRED
|
||||
and tls_client_identity == 'localhost'
|
||||
and is_trusted_cert
|
||||
) or PY34:
|
||||
pytest.xfail(
|
||||
'OpenSSL 1.0 has problems with verifying client certs',
|
||||
)
|
||||
assert is_req_successful
|
||||
assert resp.text == 'Hello world!'
|
||||
return
|
||||
|
||||
# xfail some flaky tests
|
||||
# https://github.com/cherrypy/cheroot/issues/237
|
||||
issue_237 = (
|
||||
IS_MACOS
|
||||
and adapter_type == 'builtin'
|
||||
and tls_verify_mode != ssl.CERT_NONE
|
||||
)
|
||||
if issue_237:
|
||||
pytest.xfail('Test sometimes fails')
|
||||
|
||||
expected_ssl_errors = (
|
||||
requests.exceptions.SSLError,
|
||||
OpenSSL.SSL.Error,
|
||||
) if PY34 else (
|
||||
requests.exceptions.SSLError,
|
||||
)
|
||||
if IS_WINDOWS or IS_GITHUB_ACTIONS_WORKFLOW:
|
||||
expected_ssl_errors += requests.exceptions.ConnectionError,
|
||||
with pytest.raises(expected_ssl_errors) as ssl_err:
|
||||
make_https_request()
|
||||
|
||||
if PY34 and isinstance(ssl_err, OpenSSL.SSL.Error):
|
||||
pytest.xfail(
|
||||
'OpenSSL behaves wierdly under Python 3.4 '
|
||||
'because of an outdated urllib3',
|
||||
)
|
||||
|
||||
try:
|
||||
err_text = ssl_err.value.args[0].reason.args[0].args[0]
|
||||
except AttributeError:
|
||||
if PY34:
|
||||
pytest.xfail('OpenSSL behaves wierdly under Python 3.4')
|
||||
elif IS_WINDOWS or IS_GITHUB_ACTIONS_WORKFLOW:
|
||||
err_text = str(ssl_err.value)
|
||||
else:
|
||||
raise
|
||||
|
||||
if isinstance(err_text, int):
|
||||
err_text = str(ssl_err.value)
|
||||
|
||||
expected_substrings = (
|
||||
'sslv3 alert bad certificate' if IS_LIBRESSL_BACKEND
|
||||
else 'tlsv1 alert unknown ca',
|
||||
)
|
||||
if not six.PY2:
|
||||
if IS_MACOS and IS_PYPY and adapter_type == 'pyopenssl':
|
||||
expected_substrings = ('tlsv1 alert unknown ca',)
|
||||
if (
|
||||
tls_verify_mode in (
|
||||
ssl.CERT_REQUIRED,
|
||||
ssl.CERT_OPTIONAL,
|
||||
)
|
||||
and not is_trusted_cert
|
||||
and tls_client_identity == 'localhost'
|
||||
):
|
||||
expected_substrings += (
|
||||
'bad handshake: '
|
||||
"SysCallError(10054, 'WSAECONNRESET')",
|
||||
"('Connection aborted.', "
|
||||
'OSError("(10054, \'WSAECONNRESET\')"))',
|
||||
"('Connection aborted.', "
|
||||
'OSError("(10054, \'WSAECONNRESET\')",))',
|
||||
"('Connection aborted.', "
|
||||
'error("(10054, \'WSAECONNRESET\')",))',
|
||||
"('Connection aborted.', "
|
||||
'ConnectionResetError(10054, '
|
||||
"'An existing connection was forcibly closed "
|
||||
"by the remote host', None, 10054, None))",
|
||||
"('Connection aborted.', "
|
||||
'error(10054, '
|
||||
"'An existing connection was forcibly closed "
|
||||
"by the remote host'))",
|
||||
) if IS_WINDOWS else (
|
||||
"('Connection aborted.', "
|
||||
'OSError("(104, \'ECONNRESET\')"))',
|
||||
"('Connection aborted.', "
|
||||
'OSError("(104, \'ECONNRESET\')",))',
|
||||
"('Connection aborted.', "
|
||||
'error("(104, \'ECONNRESET\')",))',
|
||||
"('Connection aborted.', "
|
||||
"ConnectionResetError(104, 'Connection reset by peer'))",
|
||||
"('Connection aborted.', "
|
||||
"error(104, 'Connection reset by peer'))",
|
||||
) if (
|
||||
IS_GITHUB_ACTIONS_WORKFLOW
|
||||
and IS_LINUX
|
||||
) else (
|
||||
"('Connection aborted.', "
|
||||
"BrokenPipeError(32, 'Broken pipe'))",
|
||||
)
|
||||
|
||||
if PY310_PLUS:
|
||||
# FIXME: Figure out what's happening and correct the problem
|
||||
expected_substrings += (
|
||||
'SSLError(SSLEOFError(8, '
|
||||
"'EOF occurred in violation of protocol (_ssl.c:",
|
||||
)
|
||||
if IS_GITHUB_ACTIONS_WORKFLOW and IS_WINDOWS and PY310_PLUS:
|
||||
expected_substrings += (
|
||||
"('Connection aborted.', "
|
||||
'RemoteDisconnected('
|
||||
"'Remote end closed connection without response'))",
|
||||
)
|
||||
|
||||
assert any(e in err_text for e in expected_substrings)
|
||||
|
||||
|
||||
@pytest.mark.parametrize( # noqa: C901 # FIXME
|
||||
'adapter_type',
|
||||
(
|
||||
pytest.param(
|
||||
'builtin',
|
||||
marks=pytest.mark.xfail(
|
||||
IS_GITHUB_ACTIONS_WORKFLOW and IS_MACOS and PY310_PLUS,
|
||||
reason='Unclosed TLS resource warnings happen on macOS '
|
||||
'under Python 3.10',
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
'pyopenssl',
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
('tls_verify_mode', 'use_client_cert'),
|
||||
(
|
||||
(ssl.CERT_NONE, False),
|
||||
(ssl.CERT_NONE, True),
|
||||
(ssl.CERT_OPTIONAL, False),
|
||||
(ssl.CERT_OPTIONAL, True),
|
||||
(ssl.CERT_REQUIRED, True),
|
||||
),
|
||||
)
|
||||
def test_ssl_env( # noqa: C901 # FIXME
|
||||
thread_exceptions,
|
||||
recwarn,
|
||||
mocker,
|
||||
tls_http_server, adapter_type,
|
||||
ca, tls_verify_mode, tls_certificate,
|
||||
tls_certificate_chain_pem_path,
|
||||
tls_certificate_private_key_pem_path,
|
||||
tls_ca_certificate_pem_path,
|
||||
use_client_cert,
|
||||
):
|
||||
"""Test the SSL environment generated by the SSL adapters."""
|
||||
interface, _host, port = _get_conn_data(ANY_INTERFACE_IPV4)
|
||||
|
||||
with mocker.mock_module.patch(
|
||||
'idna.core.ulabel',
|
||||
return_value=ntob('127.0.0.1'),
|
||||
):
|
||||
client_cert = ca.issue_cert(ntou('127.0.0.1'))
|
||||
|
||||
with client_cert.private_key_and_cert_chain_pem.tempfile() as cl_pem:
|
||||
tls_adapter_cls = get_ssl_adapter_class(name=adapter_type)
|
||||
tls_adapter = tls_adapter_cls(
|
||||
tls_certificate_chain_pem_path,
|
||||
tls_certificate_private_key_pem_path,
|
||||
)
|
||||
if adapter_type == 'pyopenssl':
|
||||
tls_adapter.context = tls_adapter.get_context()
|
||||
tls_adapter.context.set_verify(
|
||||
_stdlib_to_openssl_verify[tls_verify_mode],
|
||||
lambda conn, cert, errno, depth, preverify_ok: preverify_ok,
|
||||
)
|
||||
else:
|
||||
tls_adapter.context.verify_mode = tls_verify_mode
|
||||
|
||||
ca.configure_trust(tls_adapter.context)
|
||||
tls_certificate.configure_cert(tls_adapter.context)
|
||||
|
||||
tlswsgiserver = tls_http_server((interface, port), tls_adapter)
|
||||
|
||||
interface, _host, port = _get_conn_data(tlswsgiserver.bind_addr)
|
||||
|
||||
resp = requests.get(
|
||||
'https://' + interface + ':' + str(port) + '/env',
|
||||
verify=tls_ca_certificate_pem_path,
|
||||
cert=cl_pem if use_client_cert else None,
|
||||
)
|
||||
if PY34 and resp.status_code != 200:
|
||||
pytest.xfail(
|
||||
'Python 3.4 has problems with verifying client certs',
|
||||
)
|
||||
|
||||
env = json.loads(resp.content.decode('utf-8'))
|
||||
|
||||
# hard coded env
|
||||
assert env['wsgi.url_scheme'] == 'https'
|
||||
assert env['HTTPS'] == 'on'
|
||||
|
||||
# ensure these are present
|
||||
for key in {'SSL_VERSION_INTERFACE', 'SSL_VERSION_LIBRARY'}:
|
||||
assert key in env
|
||||
|
||||
# pyOpenSSL generates the env before the handshake completes
|
||||
if adapter_type == 'pyopenssl':
|
||||
return
|
||||
|
||||
for key in {'SSL_PROTOCOL', 'SSL_CIPHER'}:
|
||||
assert key in env
|
||||
|
||||
# client certificate env
|
||||
if tls_verify_mode == ssl.CERT_NONE or not use_client_cert:
|
||||
assert env['SSL_CLIENT_VERIFY'] == 'NONE'
|
||||
else:
|
||||
assert env['SSL_CLIENT_VERIFY'] == 'SUCCESS'
|
||||
|
||||
with open(cl_pem, 'rt') as f:
|
||||
assert env['SSL_CLIENT_CERT'] in f.read()
|
||||
|
||||
for key in {
|
||||
'SSL_CLIENT_M_VERSION', 'SSL_CLIENT_M_SERIAL',
|
||||
'SSL_CLIENT_I_DN', 'SSL_CLIENT_S_DN',
|
||||
}:
|
||||
assert key in env
|
||||
|
||||
# builtin ssl environment generation may use a loopback socket
|
||||
# ensure no ResourceWarning was raised during the test
|
||||
# NOTE: python 2.7 does not emit ResourceWarning for ssl sockets
|
||||
if IS_PYPY:
|
||||
# NOTE: PyPy doesn't have ResourceWarning
|
||||
# Ref: https://doc.pypy.org/en/latest/cpython_differences.html
|
||||
return
|
||||
for warn in recwarn:
|
||||
if not issubclass(warn.category, ResourceWarning):
|
||||
continue
|
||||
|
||||
# the tests can sporadically generate resource warnings
|
||||
# due to timing issues
|
||||
# all of these sporadic warnings appear to be about socket.socket
|
||||
# and have been observed to come from requests connection pool
|
||||
msg = str(warn.message)
|
||||
if 'socket.socket' in msg:
|
||||
pytest.xfail(
|
||||
'\n'.join((
|
||||
'Sometimes this test fails due to '
|
||||
'a socket.socket ResourceWarning:',
|
||||
msg,
|
||||
)),
|
||||
)
|
||||
pytest.fail(msg)
|
||||
|
||||
# to perform the ssl handshake over that loopback socket,
|
||||
# the builtin ssl environment generation uses a thread
|
||||
for _, _, trace in thread_exceptions:
|
||||
print(trace, file=sys.stderr)
|
||||
assert not thread_exceptions, ': '.join((
|
||||
thread_exceptions[0][0].__name__,
|
||||
thread_exceptions[0][1],
|
||||
))
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'ip_addr',
|
||||
(
|
||||
ANY_INTERFACE_IPV4,
|
||||
ANY_INTERFACE_IPV6,
|
||||
),
|
||||
)
|
||||
def test_https_over_http_error(http_server, ip_addr):
|
||||
"""Ensure that connecting over HTTPS to HTTP port is handled."""
|
||||
httpserver = http_server.send((ip_addr, EPHEMERAL_PORT))
|
||||
interface, _host, port = _get_conn_data(httpserver.bind_addr)
|
||||
with pytest.raises(ssl.SSLError) as ssl_err:
|
||||
six.moves.http_client.HTTPSConnection(
|
||||
'{interface}:{port}'.format(
|
||||
interface=interface,
|
||||
port=port,
|
||||
),
|
||||
).request('GET', '/')
|
||||
expected_substring = (
|
||||
'wrong version number' if IS_ABOVE_OPENSSL10
|
||||
else 'unknown protocol'
|
||||
)
|
||||
assert expected_substring in ssl_err.value.args[-1]
|
||||
|
||||
|
||||
http_over_https_error_builtin_marks = []
|
||||
if IS_WINDOWS and six.PY2:
|
||||
http_over_https_error_builtin_marks.append(
|
||||
pytest.mark.flaky(reruns=5, reruns_delay=2),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'adapter_type',
|
||||
(
|
||||
pytest.param(
|
||||
'builtin',
|
||||
marks=http_over_https_error_builtin_marks,
|
||||
),
|
||||
'pyopenssl',
|
||||
),
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
'ip_addr',
|
||||
(
|
||||
ANY_INTERFACE_IPV4,
|
||||
pytest.param(ANY_INTERFACE_IPV6, marks=missing_ipv6),
|
||||
),
|
||||
)
|
||||
def test_http_over_https_error(
|
||||
tls_http_server, adapter_type,
|
||||
ca, ip_addr,
|
||||
tls_certificate,
|
||||
tls_certificate_chain_pem_path,
|
||||
tls_certificate_private_key_pem_path,
|
||||
):
|
||||
"""Ensure that connecting over HTTP to HTTPS port is handled."""
|
||||
# disable some flaky tests
|
||||
# https://github.com/cherrypy/cheroot/issues/225
|
||||
issue_225 = (
|
||||
IS_MACOS
|
||||
and adapter_type == 'builtin'
|
||||
)
|
||||
if issue_225:
|
||||
pytest.xfail('Test fails in Travis-CI')
|
||||
|
||||
tls_adapter_cls = get_ssl_adapter_class(name=adapter_type)
|
||||
tls_adapter = tls_adapter_cls(
|
||||
tls_certificate_chain_pem_path, tls_certificate_private_key_pem_path,
|
||||
)
|
||||
if adapter_type == 'pyopenssl':
|
||||
tls_adapter.context = tls_adapter.get_context()
|
||||
|
||||
tls_certificate.configure_cert(tls_adapter.context)
|
||||
|
||||
interface, _host, port = _get_conn_data(ip_addr)
|
||||
tlshttpserver = tls_http_server((interface, port), tls_adapter)
|
||||
|
||||
interface, _host, port = _get_conn_data(
|
||||
tlshttpserver.bind_addr,
|
||||
)
|
||||
|
||||
fqdn = interface
|
||||
if ip_addr is ANY_INTERFACE_IPV6:
|
||||
fqdn = '[{fqdn}]'.format(**locals())
|
||||
|
||||
expect_fallback_response_over_plain_http = (
|
||||
(
|
||||
adapter_type == 'pyopenssl'
|
||||
and (IS_ABOVE_OPENSSL10 or not six.PY2)
|
||||
)
|
||||
or PY27
|
||||
) or (
|
||||
IS_GITHUB_ACTIONS_WORKFLOW
|
||||
and IS_WINDOWS
|
||||
and six.PY2
|
||||
and not IS_WIN2016
|
||||
)
|
||||
if (
|
||||
IS_GITHUB_ACTIONS_WORKFLOW
|
||||
and IS_WINDOWS
|
||||
and six.PY2
|
||||
and IS_WIN2016
|
||||
and adapter_type == 'builtin'
|
||||
and ip_addr is ANY_INTERFACE_IPV6
|
||||
):
|
||||
expect_fallback_response_over_plain_http = True
|
||||
if (
|
||||
IS_GITHUB_ACTIONS_WORKFLOW
|
||||
and IS_WINDOWS
|
||||
and six.PY2
|
||||
and not IS_WIN2016
|
||||
and adapter_type == 'builtin'
|
||||
and ip_addr is not ANY_INTERFACE_IPV6
|
||||
):
|
||||
expect_fallback_response_over_plain_http = False
|
||||
if expect_fallback_response_over_plain_http:
|
||||
resp = requests.get(
|
||||
'http://{host!s}:{port!s}/'.format(host=fqdn, port=port),
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
assert resp.text == (
|
||||
'The client sent a plain HTTP request, '
|
||||
'but this server only speaks HTTPS on this port.'
|
||||
)
|
||||
return
|
||||
|
||||
with pytest.raises(requests.exceptions.ConnectionError) as ssl_err:
|
||||
requests.get( # FIXME: make stdlib ssl behave like PyOpenSSL
|
||||
'http://{host!s}:{port!s}/'.format(host=fqdn, port=port),
|
||||
)
|
||||
|
||||
if IS_LINUX:
|
||||
expected_error_code, expected_error_text = (
|
||||
104, 'Connection reset by peer',
|
||||
)
|
||||
if IS_MACOS:
|
||||
expected_error_code, expected_error_text = (
|
||||
54, 'Connection reset by peer',
|
||||
)
|
||||
if IS_WINDOWS:
|
||||
expected_error_code, expected_error_text = (
|
||||
10054,
|
||||
'An existing connection was forcibly closed by the remote host',
|
||||
)
|
||||
|
||||
underlying_error = ssl_err.value.args[0].args[-1]
|
||||
err_text = str(underlying_error)
|
||||
assert underlying_error.errno == expected_error_code, (
|
||||
'The underlying error is {underlying_error!r}'.
|
||||
format(**locals())
|
||||
)
|
||||
assert expected_error_text in err_text
|
||||
Reference in New Issue
Block a user