mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-16 02:02:58 -07:00
Update cheroot-8.5.2
This commit is contained in:
parent
4ac151d7de
commit
182e5f553e
25 changed files with 2171 additions and 602 deletions
|
@ -3,15 +3,22 @@
|
|||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
import errno
|
||||
import socket
|
||||
import time
|
||||
import logging
|
||||
import traceback as traceback_
|
||||
from collections import namedtuple
|
||||
|
||||
from six.moves import range, http_client, urllib
|
||||
|
||||
import six
|
||||
import pytest
|
||||
from jaraco.text import trim, unwrap
|
||||
|
||||
from cheroot.test import helper, webtest
|
||||
from cheroot._compat import IS_CI, IS_PYPY, IS_WINDOWS
|
||||
import cheroot.server
|
||||
|
||||
|
||||
timeout = 1
|
||||
|
@ -26,7 +33,7 @@ class Controller(helper.Controller):
|
|||
return 'Hello, world!'
|
||||
|
||||
def pov(req, resp):
|
||||
"""Render pov value."""
|
||||
"""Render ``pov`` value."""
|
||||
return pov
|
||||
|
||||
def stream(req, resp):
|
||||
|
@ -43,8 +50,10 @@ class Controller(helper.Controller):
|
|||
def upload(req, resp):
|
||||
"""Process file upload and render thank."""
|
||||
if not req.environ['REQUEST_METHOD'] == 'POST':
|
||||
raise AssertionError("'POST' != request.method %r" %
|
||||
req.environ['REQUEST_METHOD'])
|
||||
raise AssertionError(
|
||||
"'POST' != request.method %r" %
|
||||
req.environ['REQUEST_METHOD'],
|
||||
)
|
||||
return "thanks for '%s'" % req.environ['wsgi.input'].read()
|
||||
|
||||
def custom_204(req, resp):
|
||||
|
@ -103,9 +112,33 @@ class Controller(helper.Controller):
|
|||
}
|
||||
|
||||
|
||||
class ErrorLogMonitor:
|
||||
"""Mock class to access the server error_log calls made by the server."""
|
||||
|
||||
ErrorLogCall = namedtuple('ErrorLogCall', ['msg', 'level', 'traceback'])
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the server error log monitor/interceptor.
|
||||
|
||||
If you need to ignore a particular error message use the property
|
||||
``ignored_msgs`` by appending to the list the expected error messages.
|
||||
"""
|
||||
self.calls = []
|
||||
# to be used the the teardown validation
|
||||
self.ignored_msgs = []
|
||||
|
||||
def __call__(self, msg='', level=logging.INFO, traceback=False):
|
||||
"""Intercept the call to the server error_log method."""
|
||||
if traceback:
|
||||
tblines = traceback_.format_exc()
|
||||
else:
|
||||
tblines = ''
|
||||
self.calls.append(ErrorLogMonitor.ErrorLogCall(msg, level, tblines))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def testing_server(wsgi_server_client):
|
||||
"""Attach a WSGI app to the given server and pre-configure it."""
|
||||
def raw_testing_server(wsgi_server_client):
|
||||
"""Attach a WSGI app to the given server and preconfigure it."""
|
||||
app = Controller()
|
||||
|
||||
def _timeout(req, resp):
|
||||
|
@ -117,9 +150,36 @@ def testing_server(wsgi_server_client):
|
|||
wsgi_server.timeout = timeout
|
||||
wsgi_server.server_client = wsgi_server_client
|
||||
wsgi_server.keep_alive_conn_limit = 2
|
||||
|
||||
return wsgi_server
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def testing_server(raw_testing_server, monkeypatch):
|
||||
"""Modify the "raw" base server to monitor the error_log messages.
|
||||
|
||||
If you need to ignore a particular error message use the property
|
||||
``testing_server.error_log.ignored_msgs`` by appending to the list
|
||||
the expected error messages.
|
||||
"""
|
||||
# patch the error_log calls of the server instance
|
||||
monkeypatch.setattr(raw_testing_server, 'error_log', ErrorLogMonitor())
|
||||
|
||||
yield raw_testing_server
|
||||
|
||||
# Teardown verification, in case that the server logged an
|
||||
# error that wasn't notified to the client or we just made a mistake.
|
||||
for c_msg, c_level, c_traceback in raw_testing_server.error_log.calls:
|
||||
if c_level <= logging.WARNING:
|
||||
continue
|
||||
|
||||
assert c_msg in raw_testing_server.error_log.ignored_msgs, (
|
||||
'Found error in the error log: '
|
||||
"message = '{c_msg}', level = '{c_level}'\n"
|
||||
'{c_traceback}'.format(**locals()),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_client(testing_server):
|
||||
"""Get and return a test client out of the given server."""
|
||||
|
@ -338,7 +398,14 @@ def test_streaming_10(test_client, set_cl):
|
|||
'http_server_protocol',
|
||||
(
|
||||
'HTTP/1.0',
|
||||
'HTTP/1.1',
|
||||
pytest.param(
|
||||
'HTTP/1.1',
|
||||
marks=pytest.mark.xfail(
|
||||
IS_PYPY and IS_CI,
|
||||
reason='Fails under PyPy in CI for unknown reason',
|
||||
strict=False,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_keepalive(test_client, http_server_protocol):
|
||||
|
@ -375,6 +442,11 @@ def test_keepalive(test_client, http_server_protocol):
|
|||
assert status_line[4:] == 'OK'
|
||||
assert actual_resp_body == pov.encode()
|
||||
assert header_has_value('Connection', 'Keep-Alive', actual_headers)
|
||||
assert header_has_value(
|
||||
'Keep-Alive',
|
||||
'timeout={test_client.server_instance.timeout}'.format(**locals()),
|
||||
actual_headers,
|
||||
)
|
||||
|
||||
# Remove the keep-alive header again.
|
||||
status_line, actual_headers, actual_resp_body = test_client.get(
|
||||
|
@ -386,6 +458,7 @@ def test_keepalive(test_client, http_server_protocol):
|
|||
assert status_line[4:] == 'OK'
|
||||
assert actual_resp_body == pov.encode()
|
||||
assert not header_exists('Connection', actual_headers)
|
||||
assert not header_exists('Keep-Alive', actual_headers)
|
||||
|
||||
test_client.server_instance.protocol = original_server_protocol
|
||||
|
||||
|
@ -401,7 +474,7 @@ def test_keepalive_conn_management(test_client):
|
|||
http_connection.connect()
|
||||
return http_connection
|
||||
|
||||
def request(conn):
|
||||
def request(conn, keepalive=True):
|
||||
status_line, actual_headers, actual_resp_body = test_client.get(
|
||||
'/page3', headers=[('Connection', 'Keep-Alive')],
|
||||
http_conn=conn, protocol='HTTP/1.0',
|
||||
|
@ -410,7 +483,28 @@ def test_keepalive_conn_management(test_client):
|
|||
assert actual_status == 200
|
||||
assert status_line[4:] == 'OK'
|
||||
assert actual_resp_body == pov.encode()
|
||||
assert header_has_value('Connection', 'Keep-Alive', actual_headers)
|
||||
if keepalive:
|
||||
assert header_has_value('Connection', 'Keep-Alive', actual_headers)
|
||||
assert header_has_value(
|
||||
'Keep-Alive',
|
||||
'timeout={test_client.server_instance.timeout}'.
|
||||
format(**locals()),
|
||||
actual_headers,
|
||||
)
|
||||
else:
|
||||
assert not header_exists('Connection', actual_headers)
|
||||
assert not header_exists('Keep-Alive', actual_headers)
|
||||
|
||||
def check_server_idle_conn_count(count, timeout=1.0):
|
||||
deadline = time.time() + timeout
|
||||
while True:
|
||||
n = test_client.server_instance._connections._num_connections
|
||||
if n == count:
|
||||
return
|
||||
assert time.time() <= deadline, (
|
||||
'idle conn count mismatch, wanted {count}, got {n}'.
|
||||
format(**locals()),
|
||||
)
|
||||
|
||||
disconnect_errors = (
|
||||
http_client.BadStatusLine,
|
||||
|
@ -421,50 +515,175 @@ def test_keepalive_conn_management(test_client):
|
|||
# Make a new connection.
|
||||
c1 = connection()
|
||||
request(c1)
|
||||
check_server_idle_conn_count(1)
|
||||
|
||||
# Make a second one.
|
||||
c2 = connection()
|
||||
request(c2)
|
||||
check_server_idle_conn_count(2)
|
||||
|
||||
# Reusing the first connection should still work.
|
||||
request(c1)
|
||||
check_server_idle_conn_count(2)
|
||||
|
||||
# Creating a new connection should still work.
|
||||
# Creating a new connection should still work, but we should
|
||||
# have run out of available connections to keep alive, so the
|
||||
# server should tell us to close.
|
||||
c3 = connection()
|
||||
request(c3)
|
||||
request(c3, keepalive=False)
|
||||
check_server_idle_conn_count(2)
|
||||
|
||||
# Allow a tick.
|
||||
time.sleep(0.2)
|
||||
|
||||
# That's three connections, we should expect the one used less recently
|
||||
# to be expired.
|
||||
# Show that the third connection was closed.
|
||||
with pytest.raises(disconnect_errors):
|
||||
request(c2)
|
||||
|
||||
# But the oldest created one should still be valid.
|
||||
# (As well as the newest one).
|
||||
request(c1)
|
||||
request(c3)
|
||||
request(c3)
|
||||
check_server_idle_conn_count(2)
|
||||
|
||||
# Wait for some of our timeout.
|
||||
time.sleep(1.0)
|
||||
time.sleep(1.2)
|
||||
|
||||
# Refresh the third connection.
|
||||
request(c3)
|
||||
# Refresh the second connection.
|
||||
request(c2)
|
||||
check_server_idle_conn_count(2)
|
||||
|
||||
# Wait for the remainder of our timeout, plus one tick.
|
||||
time.sleep(1.2)
|
||||
check_server_idle_conn_count(1)
|
||||
|
||||
# First connection should now be expired.
|
||||
with pytest.raises(disconnect_errors):
|
||||
request(c1)
|
||||
check_server_idle_conn_count(1)
|
||||
|
||||
# But the third one should still be valid.
|
||||
request(c3)
|
||||
# But the second one should still be valid.
|
||||
request(c2)
|
||||
check_server_idle_conn_count(1)
|
||||
|
||||
# Restore original timeout.
|
||||
test_client.server_instance.timeout = timeout
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('simulated_exception', 'error_number', 'exception_leaks'),
|
||||
(
|
||||
pytest.param(
|
||||
socket.error, errno.ECONNRESET, False,
|
||||
id='socket.error(ECONNRESET)',
|
||||
),
|
||||
pytest.param(
|
||||
socket.error, errno.EPIPE, False,
|
||||
id='socket.error(EPIPE)',
|
||||
),
|
||||
pytest.param(
|
||||
socket.error, errno.ENOTCONN, False,
|
||||
id='simulated socket.error(ENOTCONN)',
|
||||
),
|
||||
pytest.param(
|
||||
None, # <-- don't raise an artificial exception
|
||||
errno.ENOTCONN, False,
|
||||
id='real socket.error(ENOTCONN)',
|
||||
marks=pytest.mark.xfail(
|
||||
IS_WINDOWS,
|
||||
reason='Now reproducible this way on Windows',
|
||||
),
|
||||
),
|
||||
pytest.param(
|
||||
socket.error, errno.ESHUTDOWN, False,
|
||||
id='socket.error(ESHUTDOWN)',
|
||||
),
|
||||
pytest.param(RuntimeError, 666, True, id='RuntimeError(666)'),
|
||||
pytest.param(socket.error, -1, True, id='socket.error(-1)'),
|
||||
) + (
|
||||
() if six.PY2 else (
|
||||
pytest.param(
|
||||
ConnectionResetError, errno.ECONNRESET, False,
|
||||
id='ConnectionResetError(ECONNRESET)',
|
||||
),
|
||||
pytest.param(
|
||||
BrokenPipeError, errno.EPIPE, False,
|
||||
id='BrokenPipeError(EPIPE)',
|
||||
),
|
||||
pytest.param(
|
||||
BrokenPipeError, errno.ESHUTDOWN, False,
|
||||
id='BrokenPipeError(ESHUTDOWN)',
|
||||
),
|
||||
)
|
||||
),
|
||||
)
|
||||
def test_broken_connection_during_tcp_fin(
|
||||
error_number, exception_leaks,
|
||||
mocker, monkeypatch,
|
||||
simulated_exception, test_client,
|
||||
):
|
||||
"""Test there's no traceback on broken connection during close.
|
||||
|
||||
It artificially causes :py:data:`~errno.ECONNRESET` /
|
||||
:py:data:`~errno.EPIPE` / :py:data:`~errno.ESHUTDOWN` /
|
||||
:py:data:`~errno.ENOTCONN` as well as unrelated :py:exc:`RuntimeError`
|
||||
and :py:exc:`socket.error(-1) <socket.error>` on the server socket when
|
||||
:py:meth:`socket.shutdown() <socket.socket.shutdown>` is called. It's
|
||||
triggered by closing the client socket before the server had a chance
|
||||
to respond.
|
||||
|
||||
The expectation is that only :py:exc:`RuntimeError` and a
|
||||
:py:exc:`socket.error` with an unusual error code would leak.
|
||||
|
||||
With the :py:data:`None`-parameter, a real non-simulated
|
||||
:py:exc:`OSError(107, 'Transport endpoint is not connected')
|
||||
<OSError>` happens.
|
||||
"""
|
||||
exc_instance = (
|
||||
None if simulated_exception is None
|
||||
else simulated_exception(error_number, 'Simulated socket error')
|
||||
)
|
||||
old_close_kernel_socket = (
|
||||
test_client.server_instance.
|
||||
ConnectionClass._close_kernel_socket
|
||||
)
|
||||
|
||||
def _close_kernel_socket(self):
|
||||
monkeypatch.setattr( # `socket.shutdown` is read-only otherwise
|
||||
self, 'socket',
|
||||
mocker.mock_module.Mock(wraps=self.socket),
|
||||
)
|
||||
if exc_instance is not None:
|
||||
monkeypatch.setattr(
|
||||
self.socket, 'shutdown',
|
||||
mocker.mock_module.Mock(side_effect=exc_instance),
|
||||
)
|
||||
_close_kernel_socket.fin_spy = mocker.spy(self.socket, 'shutdown')
|
||||
_close_kernel_socket.exception_leaked = True
|
||||
old_close_kernel_socket(self)
|
||||
_close_kernel_socket.exception_leaked = False
|
||||
|
||||
monkeypatch.setattr(
|
||||
test_client.server_instance.ConnectionClass,
|
||||
'_close_kernel_socket',
|
||||
_close_kernel_socket,
|
||||
)
|
||||
|
||||
conn = test_client.get_connection()
|
||||
conn.auto_open = False
|
||||
conn.connect()
|
||||
conn.send(b'GET /hello HTTP/1.1')
|
||||
conn.send(('Host: %s' % conn.host).encode('ascii'))
|
||||
conn.close()
|
||||
|
||||
for _ in range(10): # Let the server attempt TCP shutdown
|
||||
time.sleep(0.1)
|
||||
if hasattr(_close_kernel_socket, 'exception_leaked'):
|
||||
break
|
||||
|
||||
if exc_instance is not None: # simulated by us
|
||||
assert _close_kernel_socket.fin_spy.spy_exception is exc_instance
|
||||
else: # real
|
||||
assert isinstance(
|
||||
_close_kernel_socket.fin_spy.spy_exception, socket.error,
|
||||
)
|
||||
assert _close_kernel_socket.fin_spy.spy_exception.errno == error_number
|
||||
|
||||
assert _close_kernel_socket.exception_leaked is exception_leaks
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'timeout_before_headers',
|
||||
(
|
||||
|
@ -475,7 +694,7 @@ def test_keepalive_conn_management(test_client):
|
|||
def test_HTTP11_Timeout(test_client, timeout_before_headers):
|
||||
"""Check timeout without sending any data.
|
||||
|
||||
The server will close the conn with a 408.
|
||||
The server will close the connection with a 408.
|
||||
"""
|
||||
conn = test_client.get_connection()
|
||||
conn.auto_open = False
|
||||
|
@ -594,7 +813,7 @@ def test_HTTP11_Timeout_after_request(test_client):
|
|||
def test_HTTP11_pipelining(test_client):
|
||||
"""Test HTTP/1.1 pipelining.
|
||||
|
||||
httplib doesn't support this directly.
|
||||
:py:mod:`http.client` doesn't support this directly.
|
||||
"""
|
||||
conn = test_client.get_connection()
|
||||
|
||||
|
@ -639,7 +858,7 @@ def test_100_Continue(test_client):
|
|||
conn = test_client.get_connection()
|
||||
|
||||
# Try a page without an Expect request header first.
|
||||
# Note that httplib's response.begin automatically ignores
|
||||
# Note that http.client's response.begin automatically ignores
|
||||
# 100 Continue responses, so we must manually check for it.
|
||||
conn.putrequest('POST', '/upload', skip_host=True)
|
||||
conn.putheader('Host', conn.host)
|
||||
|
@ -800,11 +1019,13 @@ def test_No_Message_Body(test_client):
|
|||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason='Server does not correctly read trailers/ending of the previous '
|
||||
'HTTP request, thus the second request fails as the server tries '
|
||||
r"to parse b'Content-Type: application/json\r\n' as a "
|
||||
'Request-Line. This results in HTTP status code 400, instead of 413'
|
||||
'Ref: https://github.com/cherrypy/cheroot/issues/69',
|
||||
reason=unwrap(
|
||||
trim("""
|
||||
Headers from earlier request leak into the request
|
||||
line for a subsequent request, resulting in 400
|
||||
instead of 413. See cherrypy/cheroot#69 for details.
|
||||
"""),
|
||||
),
|
||||
)
|
||||
def test_Chunked_Encoding(test_client):
|
||||
"""Test HTTP uploads with chunked transfer-encoding."""
|
||||
|
@ -837,7 +1058,7 @@ def test_Chunked_Encoding(test_client):
|
|||
|
||||
# Try a chunked request that exceeds server.max_request_body_size.
|
||||
# Note that the delimiters and trailer are included.
|
||||
body = b'3e3\r\n' + (b'x' * 995) + b'\r\n0\r\n\r\n'
|
||||
body = b'\r\n'.join((b'3e3', b'x' * 995, b'0', b'', b''))
|
||||
conn.putrequest('POST', '/upload', skip_host=True)
|
||||
conn.putheader('Host', conn.host)
|
||||
conn.putheader('Transfer-Encoding', 'chunked')
|
||||
|
@ -895,7 +1116,7 @@ def test_Content_Length_not_int(test_client):
|
|||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
'uri,expected_resp_status,expected_resp_body',
|
||||
('uri', 'expected_resp_status', 'expected_resp_body'),
|
||||
(
|
||||
(
|
||||
'/wrong_cl_buffered', 500,
|
||||
|
@ -929,6 +1150,16 @@ def test_Content_Length_out(
|
|||
|
||||
conn.close()
|
||||
|
||||
# the server logs the exception that we had verified from the
|
||||
# client perspective. Tell the error_log verification that
|
||||
# it can ignore that message.
|
||||
test_client.server_instance.error_log.ignored_msgs.extend((
|
||||
# Python 3.7+:
|
||||
"ValueError('Response body exceeds the declared Content-Length.')",
|
||||
# Python 2.7-3.6 (macOS?):
|
||||
"ValueError('Response body exceeds the declared Content-Length.',)",
|
||||
))
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
reason='Sometimes this test fails due to low timeout. '
|
||||
|
@ -970,11 +1201,94 @@ def test_No_CRLF(test_client, invalid_terminator):
|
|||
# Initialize a persistent HTTP connection
|
||||
conn = test_client.get_connection()
|
||||
|
||||
# (b'%s' % b'') is not supported in Python 3.4, so just use +
|
||||
conn.send(b'GET /hello HTTP/1.1' + invalid_terminator)
|
||||
# (b'%s' % b'') is not supported in Python 3.4, so just use bytes.join()
|
||||
conn.send(b''.join((b'GET /hello HTTP/1.1', invalid_terminator)))
|
||||
response = conn.response_class(conn.sock, method='GET')
|
||||
response.begin()
|
||||
actual_resp_body = response.read()
|
||||
expected_resp_body = b'HTTP requires CRLF terminators'
|
||||
assert actual_resp_body == expected_resp_body
|
||||
conn.close()
|
||||
|
||||
|
||||
class FaultySelect:
|
||||
"""Mock class to insert errors in the selector.select method."""
|
||||
|
||||
def __init__(self, original_select):
|
||||
"""Initilize helper class to wrap the selector.select method."""
|
||||
self.original_select = original_select
|
||||
self.request_served = False
|
||||
self.os_error_triggered = False
|
||||
|
||||
def __call__(self, timeout):
|
||||
"""Intercept the calls to selector.select."""
|
||||
if self.request_served:
|
||||
self.os_error_triggered = True
|
||||
raise OSError('Error while selecting the client socket.')
|
||||
|
||||
return self.original_select(timeout)
|
||||
|
||||
|
||||
class FaultyGetMap:
|
||||
"""Mock class to insert errors in the selector.get_map method."""
|
||||
|
||||
def __init__(self, original_get_map):
|
||||
"""Initilize helper class to wrap the selector.get_map method."""
|
||||
self.original_get_map = original_get_map
|
||||
self.sabotage_conn = False
|
||||
self.conn_closed = False
|
||||
|
||||
def __call__(self):
|
||||
"""Intercept the calls to selector.get_map."""
|
||||
sabotage_targets = (
|
||||
conn for _, (_, _, _, conn) in self.original_get_map().items()
|
||||
if isinstance(conn, cheroot.server.HTTPConnection)
|
||||
) if self.sabotage_conn and not self.conn_closed else ()
|
||||
|
||||
for conn in sabotage_targets:
|
||||
# close the socket to cause OSError
|
||||
conn.close()
|
||||
self.conn_closed = True
|
||||
|
||||
return self.original_get_map()
|
||||
|
||||
|
||||
def test_invalid_selected_connection(test_client, monkeypatch):
|
||||
"""Test the error handling segment of HTTP connection selection.
|
||||
|
||||
See :py:meth:`cheroot.connections.ConnectionManager.get_conn`.
|
||||
"""
|
||||
# patch the select method
|
||||
faux_select = FaultySelect(
|
||||
test_client.server_instance._connections._selector.select,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
test_client.server_instance._connections._selector,
|
||||
'select',
|
||||
faux_select,
|
||||
)
|
||||
|
||||
# patch the get_map method
|
||||
faux_get_map = FaultyGetMap(
|
||||
test_client.server_instance._connections._selector._selector.get_map,
|
||||
)
|
||||
|
||||
monkeypatch.setattr(
|
||||
test_client.server_instance._connections._selector._selector,
|
||||
'get_map',
|
||||
faux_get_map,
|
||||
)
|
||||
|
||||
# request a page with connection keep-alive to make sure
|
||||
# we'll have a connection to be modified.
|
||||
resp_status, resp_headers, resp_body = test_client.request(
|
||||
'/page1', headers=[('Connection', 'Keep-Alive')],
|
||||
)
|
||||
|
||||
assert resp_status == '200 OK'
|
||||
# trigger the internal errors
|
||||
faux_get_map.sabotage_conn = faux_select.request_served = True
|
||||
# give time to make sure the error gets handled
|
||||
time.sleep(test_client.server_instance.expiration_interval * 2)
|
||||
assert faux_select.os_error_triggered
|
||||
assert faux_get_map.conn_closed
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue