# -*- coding: utf-8 -*- """ Copyright © 2004-2019, CherryPy Team (team@cherrypy.org) All rights reserved. * * * Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of CherryPy nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ from pkg_resources import parse_version # The SSL code in CherryPy 3.5.0 is buggy. It was fixed long ago, # but 3.5.0 is still shipping in major linux distributions # (Fedora 27, Ubuntu Xenial), so we must monkey patch it to get SSL working. def patch_http_connection_init(v): # It was fixed in 3.7.0. Exact lower bound version is probably earlier, # but 3.5.0 is what this monkey patch is tested on. if parse_version("3.5.0") <= v < parse_version("3.7.0"): from cherrypy.wsgiserver.wsgiserver2 import CP_fileobject, HTTPConnection def fixed_init(hc_self, server, sock, makefile=CP_fileobject): hc_self.server = server hc_self.socket = sock hc_self.rfile = makefile(sock, "rb", hc_self.rbufsize) hc_self.wfile = makefile(sock, "wb", hc_self.wbufsize) hc_self.requests_seen = 0 HTTPConnection.__init__ = fixed_init # When the CherryPy server in 3.2.2 (and later) starts it attempts to verify # that the ports its listening on are in fact bound. When using the any address # "::" it tries both ipv4 and ipv6, and in some environments (e.g. kubernetes) # ipv6 isn't yet configured / supported and CherryPy throws an uncaught # exception. def skip_wait_for_occupied_port(v): # the issue was fixed in 3.2.3. it's present in 3.2.2 (current version on # centos:7) and back to at least 3.0.0. if parse_version("3.1.2") <= v < parse_version("3.2.3"): # https://github.com/cherrypy/cherrypy/issues/1100 from cherrypy.process import servers servers.wait_for_occupied_port = lambda host, port: None # cherrypy.wsgiserver was extracted wsgiserver into cheroot in cherrypy v9.0.0 def patch_builtin_ssl_wrap(v, new_wrap): if v < parse_version("9.0.0"): from cherrypy.wsgiserver.ssl_builtin import BuiltinSSLAdapter as builtin_ssl else: from cheroot.ssl.builtin import BuiltinSSLAdapter as builtin_ssl # type: ignore builtin_ssl.wrap = new_wrap(builtin_ssl.wrap) def accept_exceptions_from_builtin_ssl(v): # the fix was included by cheroot v5.2.0, which was included by cherrypy # 10.2.0. if v < parse_version("10.2.0"): # see https://github.com/cherrypy/cheroot/pull/4 import ssl def accept_ssl_errors(func): def wrapper(self, sock): try: return func(self, sock) except ssl.SSLError as e: if e.errno == ssl.SSL_ERROR_SSL: # Check if it's one of the known errors # Errors that are caught by PyOpenSSL, but thrown by # built-in ssl _block_errors = ('unknown protocol', 'unknown ca', 'unknown_ca', 'unknown error', 'https proxy request', 'inappropriate fallback', 'wrong version number', 'no shared cipher', 'certificate unknown', 'ccs received early', 'certificate verify failed', # client cert w/o trusted CA 'version too low', # caused by SSL3 connections 'unsupported protocol', # caused by TLS1 connections 'sslv3 alert bad certificate') for error_text in _block_errors: if error_text in e.args[1].lower(): # Accepted error, let's pass return None, {} raise return wrapper patch_builtin_ssl_wrap(v, accept_ssl_errors) def accept_socket_error_0(v): # see https://github.com/cherrypy/cherrypy/issues/1618 try: import cheroot cheroot_version = parse_version(cheroot.__version__) except ImportError: pass if v < parse_version("9.0.0") or cheroot_version < parse_version("6.5.5"): generic_socket_error = OSError def accept_socket_error_0(func): def wrapper(self, sock): try: return func(self, sock) except generic_socket_error as e: """It is unclear why exactly this happens. It's reproducible only 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 """ import ssl is_error0 = e.args == (0, 'Error') IS_ABOVE_OPENSSL10 = ssl.OPENSSL_VERSION_INFO >= (1, 1) del ssl if is_error0 and IS_ABOVE_OPENSSL10: return None, {} raise return wrapper patch_builtin_ssl_wrap(v, accept_socket_error_0) def patch_request_unique_id(v): """ Older versions of cherrypy don't include request.unique_id field (a lazily calculated UUID4). Monkey-patching is preferred over alternatives as inheritance, as it'd break type checks (cherrypy/lib/cgtools.py: `isinstance(obj, _cprequest.Request)`) """ if v < parse_version('11.1.0'): import uuid from functools import update_wrapper from cherrypy._cprequest import Request class LazyUUID4(object): def __str__(self): """Return UUID4 and keep it for future calls.""" return str(self.uuid4) @property def uuid4(self): """Provide unique id on per-request basis using UUID4. It's evaluated lazily on render. """ try: self._uuid4 # type: ignore except AttributeError: # evaluate on first access self._uuid4 = uuid.uuid4() return self._uuid4 old_init = Request.__init__ def init_with_unique_id(self, *args, **kwargs): old_init(self, *args, **kwargs) self.unique_id = LazyUUID4() Request.__init__ = update_wrapper(init_with_unique_id, old_init) def patch_cherrypy(v): ver = parse_version(v) patch_http_connection_init(ver) skip_wait_for_occupied_port(ver) accept_exceptions_from_builtin_ssl(ver) accept_socket_error_0(ver) patch_request_unique_id(ver)