Commit c0a66e8c authored by Julien Puydt's avatar Julien Puydt

New upstream version 6.5.3+ds

parent be613fd9
ref-names: HEAD -> master, tag: v6.5.2
ref-names: tag: v6.5.3, refs/reviewable/pr151/r1, refs/pull/151/head
......@@ -6,3 +6,7 @@ repos:
args:
- --msg-template={path}:L {line:3d}:({symbol}) {msg} (C {column:2d}), ::{msg_id}
- --output-format=colorized
- repo: git://github.com/asottile/add-trailing-comma
rev: v0.7.0
hooks:
- id: add-trailing-comma
conditions: v1
dist: trusty
sudo: false
dist: xenial
language: python
python:
......@@ -9,10 +8,9 @@ python:
- 3.4
- 3.5
- 3.6
- &pypy2 pypy2.7-5.10.0
- &pypy3 pypy3.5-5.10.1
- &pypy2 pypy2.7-6.0
- &pypy3 pypy3.5-6.0
group: edge
_base_envs:
- &stage_lint
stage: &stage_lint_name lint
......@@ -65,9 +63,6 @@ _base_envs:
- brew --cache
- &python_3_7_mixture
python: &mainstream_python 3.7
dist: xenial
group:
sudo: true
- &pure_python_base
<<: *stage_test
<<: *python_3_7_mixture
......
v6.5.3
======
- :pr:`149`: Make ``SCRIPT_NAME`` optional per PEP 333.
v6.5.2
======
- :issue:`6` via :pr:`109`: Fix import of
......
......@@ -42,10 +42,11 @@ else:
# escapes, but without having to prefix it with u'' for Python 2,
# but no prefix for Python 3.
if encoding == 'escape':
return six.u(
re.sub(r'\\u([0-9a-zA-Z]{4})',
lambda m: six.unichr(int(m.group(1), 16)),
n.decode('ISO-8859-1')))
return re.sub(
r'\\u([0-9a-zA-Z]{4})',
lambda m: six.unichr(int(m.group(1), 16)),
n.decode('ISO-8859-1'),
)
# Assume it's already in the given encoding, which for ISO-8859-1
# is almost always what was intended.
return n.decode(encoding)
......@@ -64,3 +65,25 @@ def assert_native(n):
"""
if not isinstance(n, str):
raise TypeError('n must be a native str (got %s)' % type(n).__name__)
if six.PY3:
"""Python 3 has memoryview builtin."""
# Python 2.7 has it backported, but socket.write() does
# str(memoryview(b'0' * 100)) -> <memory at 0x7fb6913a5588>
# instead of accessing it correctly.
memoryview = memoryview
else:
"""Link memoryview to buffer under Python 2."""
memoryview = buffer # noqa: F821
def extract_bytes(mv):
"""Retrieve bytes out of memoryview/buffer or bytes."""
if isinstance(mv, memoryview):
return mv.tobytes() if six.PY3 else bytes(mv)
if isinstance(mv, bytes):
return mv
return ValueError
......@@ -29,10 +29,9 @@ def plat_specific_errors(*errnames):
the specific platform (OS). This function will return the list of
numeric values for a given list of potential names.
"""
errno_names = dir(errno)
nums = [getattr(errno, k) for k in errnames if k in errno_names]
# de-dupe the list
return list(dict.fromkeys(nums).keys())
missing_attr = set([None, ])
unique_nums = set(getattr(errno, k, None) for k in errnames)
return list(unique_nums - missing_attr)
socket_error_eintr = plat_specific_errors('EINTR', 'WSAEINTR')
......
......@@ -15,6 +15,11 @@ except ImportError:
import six
from . import errors
from ._compat import extract_bytes, memoryview
# Write only 16K at a time to sockets
SOCK_WRITE_BLOCKSIZE = 16384
class BufferedWriter(io.BufferedWriter):
......@@ -64,17 +69,21 @@ class MakeFile_PY2(getattr(socket, '_fileobject', object)):
def write(self, data):
"""Sendall for non-blocking sockets."""
while data:
bytes_sent = 0
data_mv = memoryview(data)
payload_size = len(data_mv)
while bytes_sent < payload_size:
try:
bytes_sent = self.send(data)
data = data[bytes_sent:]
bytes_sent += self.send(
data_mv[bytes_sent:bytes_sent + SOCK_WRITE_BLOCKSIZE]
)
except socket.error as e:
if e.args[0] not in errors.socket_errors_nonblocking:
raise
def send(self, data):
"""Send some part of message to the socket."""
bytes_sent = self._sock.send(data)
bytes_sent = self._sock.send(extract_bytes(data))
self.bytes_written += bytes_sent
return bytes_sent
......
......@@ -96,11 +96,34 @@ __all__ = ('HTTPRequest', 'HTTPConnection', 'HTTPServer',
IS_WINDOWS = platform.system() == 'Windows'
"""Flag indicating whether the app is running under Windows."""
if not IS_WINDOWS:
import grp
import pwd
IS_GAE = os.getenv('SERVER_SOFTWARE', '').startswith('Google App Engine/')
"""Flag indicating whether the app is running in GAE env.
Ref:
https://cloud.google.com/appengine/docs/standard/python/tools
/using-local-server#detecting_application_runtime_environment
"""
IS_UID_GID_RESOLVABLE = not IS_WINDOWS and not IS_GAE
"""Indicates whether UID/GID resolution's available under current platform."""
if IS_UID_GID_RESOLVABLE:
try:
import grp
import pwd
except ImportError:
"""Unavailable in the current env.
This shouldn't be happening normally.
All of the known cases are excluded via the if clause.
"""
IS_UID_GID_RESOLVABLE = False
grp, pwd = None, None
import struct
......@@ -1371,9 +1394,11 @@ class HTTPConnection:
RuntimeError: in case of UID/GID lookup unsupported or disabled
"""
if IS_WINDOWS:
if not IS_UID_GID_RESOLVABLE:
raise NotImplementedError(
'UID/GID lookup can only be done under UNIX-like OS'
'UID/GID lookup is unavailable under current platform. '
'It can only be done under UNIX-like OS '
'but not under the Google App Engine'
)
elif not self.peercreds_resolve_enabled:
raise RuntimeError(
......@@ -1837,10 +1862,21 @@ class HTTPServer:
"""Create and prepare the socket object."""
sock = socket.socket(family, type, proto)
prevent_socket_inheritance(sock)
if not IS_WINDOWS:
# Windows has different semantics for SO_REUSEADDR,
# so don't set it.
# https://msdn.microsoft.com/en-us/library/ms740621(v=vs.85).aspx
host, port = bind_addr[:2]
IS_EPHEMERAL_PORT = port == 0
if not (IS_WINDOWS or IS_EPHEMERAL_PORT):
"""Enable SO_REUSEADDR for the current socket.
Skip for Windows (has different semantics)
or ephemeral ports (can steal ports from others).
Refs:
* https://msdn.microsoft.com/en-us/library/ms740621(v=vs.85).aspx
* https://github.com/cherrypy/cheroot/issues/114
* https://gavv.github.io/blog/ephemeral-port-reuse/
"""
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
if nodelay and not isinstance(bind_addr, str):
sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
......@@ -1848,8 +1884,6 @@ class HTTPServer:
if ssl_adapter is not None:
sock = ssl_adapter.bind(sock)
host, port = bind_addr[:2]
# If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
# activate dual-stack. See
# https://github.com/cherrypy/cherrypy/issues/871.
......
......@@ -10,10 +10,13 @@ To use this module, set ``HTTPServer.ssl_adapter`` to an instance of
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import sys
try:
import ssl
IS_ABOVE_OPENSSL10 = ssl.OPENSSL_VERSION_INFO >= (1, 1)
except ImportError:
ssl = None
ssl = IS_ABOVE_OPENSSL10 = None
try:
from _pyio import DEFAULT_BUFFER_SIZE
......@@ -37,6 +40,9 @@ else:
del socket
IS_BELOW_PY37 = sys.version_info[:2] < (3, 7)
def _assert_ssl_exc_contains(exc, *msgs):
"""Check whether SSL exception contains either of messages provided."""
if len(msgs) < 1:
......@@ -44,7 +50,7 @@ def _assert_ssl_exc_contains(exc, *msgs):
'_assert_ssl_exc_contains() requires '
'at least one message to be passed.'
)
err_msg_lower = exc.args[1].lower()
err_msg_lower = str(exc).lower()
return any(m.lower() in err_msg_lower for m in msgs)
......@@ -144,15 +150,19 @@ class BuiltinSSLAdapter(Adapter):
except generic_socket_error as exc:
"""It is unclear why exactly this happens.
It's reproducible only under Python 2 with openssl>1.0 and stdlib
``ssl`` wrapper, and only with CherryPy.
So it looks like some healthcheck tries to connect to this socket
during startup (from the same process).
It's reproducible only under Python<=3.6 with openssl>1.0
and stdlib ``ssl`` wrapper.
In CherryPy it's triggered by Checker plugin, which connects
to the app listening to the socket port in TLS mode via plain
HTTP during startup (from the same process).
Ref: https://github.com/cherrypy/cherrypy/issues/1618
"""
if six.PY2 and exc.args == (0, 'Error'):
is_error0 = exc.args == (0, 'Error')
ssl_doesnt_handle_error0 = IS_ABOVE_OPENSSL10 and IS_BELOW_PY37
if is_error0 and ssl_doesnt_handle_error0:
return EMPTY_RESULT
raise
return s, self.get_environ(s)
......
......@@ -156,8 +156,8 @@ class Controller:
resp.status = '404 Not Found'
else:
output = handler(req, resp)
if (output is not None and
not any(resp.status.startswith(status_code)
if (output is not None
and not any(resp.status.startswith(status_code)
for status_code in ('204', '304'))):
resp.body = output
try:
......
......@@ -37,13 +37,8 @@ def test_compat_functions_negative_nonnative(func):
func(non_native_test_str, encoding='utf-8')
@pytest.mark.skip(reason='This test does not work now')
@pytest.mark.skipif(
six.PY3,
reason='This code path only appears in Python 2 version.',
)
def test_ntou_escape():
"""Check that ntou supports escape-encoding under Python 2."""
expected = u''
actual = ntou('hi'.encode('ISO-8859-1'), encoding='escape')
expected = u'hišřії'
actual = ntou('hi\u0161\u0159\u0456\u0457', encoding='escape')
assert actual == expected
"""Tests for the HTTP server."""
# -*- coding: utf-8 -*-
# vim: set fileencoding=utf-8 :
from __future__ import absolute_import, division, print_function
from cheroot.wsgi import PathInfoDispatcher
def wsgi_invoke(app, environ):
"""Serve 1 requeset from a WSGI application."""
response = {}
def start_response(status, headers):
response.update({
'status': status,
'headers': headers,
})
response['body'] = b''.join(
app(environ, start_response)
)
return response
def test_dispatch_no_script_name():
"""Despatch despite lack of SCRIPT_NAME in environ."""
# Bare bones WSGI hello world app (from PEP 333).
def app(environ, start_response):
start_response('200 OK', [
('Content-Type', 'text/plain; charset=utf-8'),
])
return [u'Hello, world!'.encode('utf-8')]
# Build a dispatch table.
d = PathInfoDispatcher([
('/', app),
])
# Dispatch a request without `SCRIPT_NAME`.
response = wsgi_invoke(d, {
'PATH_INFO': '/foo',
})
assert response == {
'status': '200 OK',
'headers': [
('Content-Type', 'text/plain; charset=utf-8'),
],
'body': b'Hello, world!',
}
"""Test suite for ``cheroot.errors``."""
import platform
import pytest
from cheroot import errors
SYS_PLATFORM = platform.system()
IS_WINDOWS = SYS_PLATFORM == 'Windows'
IS_LINUX = SYS_PLATFORM == 'Linux'
IS_MACOS = SYS_PLATFORM == 'Darwin'
@pytest.mark.parametrize(
'err_names,err_nums',
(
(('', 'some-nonsense-name'), []),
(
('EPROTOTYPE', 'EAGAIN', 'EWOULDBLOCK',
'WSAEWOULDBLOCK', 'EPIPE'),
(91, 11, 32) if IS_LINUX else
(32, 35, 41) if IS_MACOS else
(32, 10041, 11, 10035) if IS_WINDOWS else
()
),
),
)
def test_plat_specific_errors(err_names, err_nums):
"""Test that plat_specific_errors retrieves correct err num list."""
actual_err_nums = errors.plat_specific_errors(*err_names)
assert len(actual_err_nums) == len(err_nums)
assert sorted(actual_err_nums) == sorted(err_nums)
......@@ -3,13 +3,18 @@
# vim: set fileencoding=utf-8 :
import os
import pytest
import platform
import ssl
import ddt
import pytest
from cheroot import wsgi
from cheroot.ssl.builtin import BuiltinSSLAdapter
from cheroot.test import helper
import cheroot
import ddt
IS_PYPY = platform.python_implementation() == 'PyPy'
def create_wsgi_server(**conf):
......@@ -19,7 +24,7 @@ def create_wsgi_server(**conf):
private_key=conf.pop('private_key'),
certificate_chain=conf.pop('certificate_chain'))
ssl_adapter.context.verify_mode = conf.pop('verify_mode', ssl.CERT_NONE)
server = cheroot.wsgi.Server(**conf)
server = wsgi.Server(**conf)
server.ssl_adapter = ssl_adapter
return server
......@@ -106,7 +111,11 @@ class ClientCertRequiredTests(HTTPSTestBase, helper.CherootWebCase):
def test_reject_wrong_ca(self):
"""Test that the given client cert is not allowed to connect."""
context = self.assert_reject('client_wrong_ca')
assert 'tlsv1 alert unknown ca' in str(context.value)
expected_substring = (
'TLSV1_ALERT_UNKNOWN_CA' if IS_PYPY
else 'tlsv1 alert unknown ca'
)
assert expected_substring in str(context.value)
@ddt.ddt
......
......@@ -14,7 +14,7 @@ import time
import pytest
from .._compat import bton
from ..server import Gateway, HTTPServer
from ..server import IS_UID_GID_RESOLVABLE, Gateway, HTTPServer
from ..testing import (
ANY_INTERFACE_IPV4,
ANY_INTERFACE_IPV6,
......@@ -44,6 +44,11 @@ non_windows_sock_test = pytest.mark.skipif(
)
http_over_unix_socket = pytest.mark.skip(
reason='Test HTTP client is not able to work through UNIX socket currently'
)
@pytest.fixture
def http_server():
"""Provision a server creator as a fixture."""
......@@ -169,24 +174,39 @@ class _TestGateway(Gateway):
return super(_TestGateway, self).respond()
@pytest.mark.skip(
reason='Test HTTP client is not able to work through UNIX socket currently'
)
@non_windows_sock_test
def test_peercreds_unix_sock(http_server, unix_sock_file):
"""Check that peercred lookup and resolution work when enabled."""
@pytest.fixture
def peercreds_enabled_server_and_client(http_server, unix_sock_file):
"""Construct a test server with `peercreds_enabled`."""
httpserver = http_server.send(unix_sock_file)
httpserver.gateway = _TestGateway
httpserver.peercreds_enabled = True
return httpserver, get_server_client(httpserver)
testclient = get_server_client(httpserver)
@http_over_unix_socket
@non_windows_sock_test
def test_peercreds_unix_sock(peercreds_enabled_server_and_client):
"""Check that peercred lookup works when enabled."""
httpserver, testclient = peercreds_enabled_server_and_client
expected_peercreds = os.getpid(), os.getuid(), os.getgid()
expected_peercreds = '|'.join(map(str, expected_peercreds))
assert testclient.get(PEERCRED_IDS_URI) == expected_peercreds
assert 'RuntimeError' in testclient.get(PEERCRED_TEXTS_URI)
@pytest.mark.skipif(
not IS_UID_GID_RESOLVABLE,
reason='Modules `grp` and `pwd` are not available '
'under the current platform',
)
@http_over_unix_socket
@non_windows_sock_test
def test_peercreds_unix_sock_with_lookup(peercreds_enabled_server_and_client):
"""Check that peercred resolution works when enabled."""
httpserver, testclient = peercreds_enabled_server_and_client
httpserver.peercreds_resolve_enabled = True
import grp
expected_textcreds = os.getlogin(), grp.getgrgid(os.getgid()).gr_name
expected_textcreds = '!'.join(map(str, expected_textcreds))
......
......@@ -66,24 +66,24 @@ class WorkerThread(threading.Thread):
self.work_time = 0
self.stats = {
'Requests': lambda s: self.requests_seen + (
(self.start_time is None) and
trueyzero or
self.conn.requests_seen
self.start_time is None
and trueyzero
or self.conn.requests_seen
),
'Bytes Read': lambda s: self.bytes_read + (
(self.start_time is None) and
trueyzero or
self.conn.rfile.bytes_read
self.start_time is None
and trueyzero
or self.conn.rfile.bytes_read
),
'Bytes Written': lambda s: self.bytes_written + (
(self.start_time is None) and
trueyzero or
self.conn.wfile.bytes_written
self.start_time is None
and trueyzero
or self.conn.wfile.bytes_written
),
'Work Time': lambda s: self.work_time + (
(self.start_time is None) and
trueyzero or
time.time() - self.start_time
self.start_time is None
and trueyzero
or time.time() - self.start_time
),
'Read Throughput': lambda s: s['Bytes Read'](s) / (
s['Work Time'](s) or 1e-6),
......
......@@ -404,7 +404,7 @@ class PathInfoDispatcher:
# The apps list should be sorted by length, descending.
if path.startswith(p + '/') or path == p:
environ = environ.copy()
environ['SCRIPT_NAME'] = environ['SCRIPT_NAME'] + p
environ['SCRIPT_NAME'] = environ.get('SCRIPT_NAME', '') + p
environ['PATH_INFO'] = path[len(p):]
return app(environ, start_response)
......
``cheroot.ssl.builtin`` module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: cheroot.ssl.builtin
:members:
:undoc-members:
:show-inheritance:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
``cheroot.ssl.pyopenssl`` module
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: cheroot.ssl.pyopenssl
:members:
:undoc-members:
:show-inheritance:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
``cheroot.ssl`` module
~~~~~~~~~~~~~~~~~~~~~~
.. automodule:: cheroot.ssl
:members:
:undoc-members:
:show-inheritance:
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
[pytest]
norecursedirs=dist docs build .tox .eggs
addopts=-v -rxs --testmon --doctest-modules --ignore cheroot/ssl/pyopenssl.py --junitxml=junit-test-results.xml --cov=cheroot --cov-report term-missing:skip-covered --cov-report xml
doctest_optionflags=ALLOW_UNICODE ELLIPSIS
norecursedirs = dist docs build .tox .eggs
addopts = -v -rxXs --testmon --doctest-modules --ignore cheroot/ssl/pyopenssl.py --junitxml=junit-test-results.xml --cov=cheroot --cov-report term-missing:skip-covered --cov-report xml
doctest_optionflags = ALLOW_UNICODE ELLIPSIS
junit_suite_name = cheroot_test_suite
testpaths = cheroot/test/
......@@ -5,5 +5,100 @@ dists = clean --all sdist bdist_wheel
universal = 1
[metadata]
license_file = LICENSE.md
name = cheroot
url = https://cheroot.cherrypy.org
project_urls =
CI: AppVeyor = https://ci.appveyor.com/project/cherrypy/cheroot
CI: Travis = https://travis-ci.org/cherrypy/cheroot
CI: Circle = https://circleci.com/gh/cherrypy/cheroot
Docs: RTD = https://cheroot.cherrypy.org
GitHub: issues = https://github.com/cherrypy/cheroot/issues
GitHub: repo = https://github.com/cherrypy/cheroot
description = Highly-optimized, pure-python HTTP server
long_description = file: README.rst
author = CherryPy Team
author_email = team@cherrypy.org
license = BSD 3-Clause License
license_file = LICENSE.md
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Web Environment
Intended Audience :: Developers
Operating System :: OS Independent
Framework :: CherryPy
License :: OSI Approved :: BSD License
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: Implementation
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: Jython
Programming Language :: Python :: Implementation :: PyPy
Topic :: Internet :: WWW/HTTP
Topic :: Internet :: WWW/HTTP :: HTTP Servers
Topic :: Internet :: WWW/HTTP :: WSGI
Topic :: Internet :: WWW/HTTP :: WSGI :: Server
keywords =
http
server
ssl
wsgi
[options]
use_scm_version = True
python_requires = >=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*
packages = find:
include_package_data = True
# These are required during `setup.py` run:
setup_requires =
setuptools_scm>=1.15.0
setuptools_scm_git_archive>=1.0
# These are required in actual runtime:
install_requires =
backports.functools_lru_cache
six>=1.11.0
more_itertools>=2.6
[options.extras_require]
docs =
# upstream
sphinx>=1.8.2
rst.linker>=1.9
jaraco.packaging>=3.2
# local
docutils
alabaster
# needed for setup-check tox env
collective.checkdocs
testing =
ddt
pytest>=2.8
pytest-sugar>=0.9.1
pytest-testmon>=0.9.7
pytest-watch
# measure test coverage
coverage
# send test coverage to codecov.io
codecov
pytest-cov
backports.unittest_mock
# TLS
trustme>=0.4.0
pyopenssl
[options.entry_points]
console_scripts =
cheroot = cheroot.cli:main
#! /usr/bin/env python
"""Cheroot package setuptools installer."""
# Project skeleton maintained at https://github.com/jaraco/skeleton
import setuptools
import io
import setuptools
try:
from setuptools.config import read_configuration, ConfigOptionsHandler
import setuptools.config
import setuptools.dist
# Set default value for 'use_scm_version'
setattr(setuptools.dist.Distribution, 'use_scm_version', False)
# Attach bool parser to 'use_scm_version' option
class ShimConfigOptionsHandler(ConfigOptionsHandler):
"""Extension class for ConfigOptionsHandler."""
@property
def parsers(self):