Update cherrpy to 17.4.2

This commit is contained in:
JonnyWong16 2019-11-23 18:55:19 -08:00
parent f28e741ad7
commit 4d6279a626
131 changed files with 15864 additions and 10389 deletions

View file

@ -1,6 +1,5 @@
"""CherryPy is a pythonic, object-oriented HTTP framework. """CherryPy is a pythonic, object-oriented HTTP framework.
CherryPy consists of not one, but four separate API layers. CherryPy consists of not one, but four separate API layers.
The APPLICATION LAYER is the simplest. CherryPy applications are written as The APPLICATION LAYER is the simplest. CherryPy applications are written as
@ -53,69 +52,72 @@ with customized or extended components. The core API's are:
* Server API * Server API
* WSGI API * WSGI API
These API's are described in the `CherryPy specification <https://bitbucket.org/cherrypy/cherrypy/wiki/CherryPySpec>`_. These API's are described in the `CherryPy specification
<https://github.com/cherrypy/cherrypy/wiki/CherryPySpec>`_.
""" """
__version__ = "5.1.0"
from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
from cherrypy._cpcompat import basestring, unicodestr
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError
from cherrypy import _cpdispatch as dispatch
from cherrypy import _cptools
tools = _cptools.default_toolbox
Tool = _cptools.Tool
from cherrypy import _cprequest
from cherrypy.lib import httputil as _httputil
from cherrypy import _cptree
tree = _cptree.Tree()
from cherrypy._cptree import Application
from cherrypy import _cpwsgi as wsgi
from cherrypy import process
try: try:
from cherrypy.process import win32 import pkg_resources
except ImportError:
pass
from threading import local as _local
from ._cperror import (
HTTPError, HTTPRedirect, InternalRedirect,
NotFound, CherryPyException,
)
from . import _cpdispatch as dispatch
from ._cptools import default_toolbox as tools, Tool
from ._helper import expose, popargs, url
from . import _cprequest, _cpserver, _cptree, _cplogging, _cpconfig
import cherrypy.lib.httputil as _httputil
from ._cptree import Application
from . import _cpwsgi as wsgi
from . import process
try:
from .process import win32
engine = win32.Win32Bus() engine = win32.Win32Bus()
engine.console_control_handler = win32.ConsoleCtrlHandler(engine) engine.console_control_handler = win32.ConsoleCtrlHandler(engine)
del win32 del win32
except ImportError: except ImportError:
engine = process.bus engine = process.bus
from . import _cpchecker
__all__ = (
'HTTPError', 'HTTPRedirect', 'InternalRedirect',
'NotFound', 'CherryPyException',
'dispatch', 'tools', 'Tool', 'Application',
'wsgi', 'process', 'tree', 'engine',
'quickstart', 'serving', 'request', 'response', 'thread_data',
'log', 'expose', 'popargs', 'url', 'config',
)
__import__('cherrypy._cptools')
__import__('cherrypy._cprequest')
tree = _cptree.Tree()
try:
__version__ = pkg_resources.require('cherrypy')[0].version
except Exception:
__version__ = 'unknown'
# Timeout monitor. We add two channels to the engine
# to which cherrypy.Application will publish.
engine.listeners['before_request'] = set() engine.listeners['before_request'] = set()
engine.listeners['after_request'] = set() engine.listeners['after_request'] = set()
class _TimeoutMonitor(process.plugins.Monitor):
def __init__(self, bus):
self.servings = []
process.plugins.Monitor.__init__(self, bus, self.run)
def before_request(self):
self.servings.append((serving.request, serving.response))
def after_request(self):
try:
self.servings.remove((serving.request, serving.response))
except ValueError:
pass
def run(self):
"""Check timeout on all responses. (Internal)"""
for req, resp in self.servings:
resp.check_timeout()
engine.timeout_monitor = _TimeoutMonitor(engine)
engine.timeout_monitor.subscribe()
engine.autoreload = process.plugins.Autoreloader(engine) engine.autoreload = process.plugins.Autoreloader(engine)
engine.autoreload.subscribe() engine.autoreload.subscribe()
@ -126,29 +128,30 @@ engine.signal_handler = process.plugins.SignalHandler(engine)
class _HandleSignalsPlugin(object): class _HandleSignalsPlugin(object):
"""Handle signals from other processes.
"""Handle signals from other processes based on the configured Based on the configured platform handlers above.
platform handlers above.""" """
def __init__(self, bus): def __init__(self, bus):
self.bus = bus self.bus = bus
def subscribe(self): def subscribe(self):
"""Add the handlers based on the platform""" """Add the handlers based on the platform."""
if hasattr(self.bus, "signal_handler"): if hasattr(self.bus, 'signal_handler'):
self.bus.signal_handler.subscribe() self.bus.signal_handler.subscribe()
if hasattr(self.bus, "console_control_handler"): if hasattr(self.bus, 'console_control_handler'):
self.bus.console_control_handler.subscribe() self.bus.console_control_handler.subscribe()
engine.signals = _HandleSignalsPlugin(engine) engine.signals = _HandleSignalsPlugin(engine)
from cherrypy import _cpserver
server = _cpserver.Server() server = _cpserver.Server()
server.subscribe() server.subscribe()
def quickstart(root=None, script_name="", config=None): def quickstart(root=None, script_name='', config=None):
"""Mount the given root, start the builtin server (and engine), then block. """Mount the given root, start the builtin server (and engine), then block.
root: an instance of a "controller class" (a collection of page handler root: an instance of a "controller class" (a collection of page handler
@ -175,11 +178,7 @@ def quickstart(root=None, script_name="", config=None):
engine.block() engine.block()
from cherrypy._cpcompat import threadlocal as _local
class _Serving(_local): class _Serving(_local):
"""An interface for registering request and response objects. """An interface for registering request and response objects.
Rather than have a separate "thread local" object for the request and Rather than have a separate "thread local" object for the request and
@ -190,8 +189,8 @@ class _Serving(_local):
thread-safe way. thread-safe way.
""" """
request = _cprequest.Request(_httputil.Host("127.0.0.1", 80), request = _cprequest.Request(_httputil.Host('127.0.0.1', 80),
_httputil.Host("127.0.0.1", 1111)) _httputil.Host('127.0.0.1', 1111))
""" """
The request object for the current thread. In the main thread, The request object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None.""" and any threads which are not receiving HTTP requests, this is None."""
@ -209,6 +208,7 @@ class _Serving(_local):
"""Remove all attributes of self.""" """Remove all attributes of self."""
self.__dict__.clear() self.__dict__.clear()
serving = _Serving() serving = _Serving()
@ -224,7 +224,7 @@ class _ThreadLocalProxy(object):
return getattr(child, name) return getattr(child, name)
def __setattr__(self, name, value): def __setattr__(self, name, value):
if name in ("__attrname__", ): if name in ('__attrname__', ):
object.__setattr__(self, name, value) object.__setattr__(self, name, value)
else: else:
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
@ -234,12 +234,12 @@ class _ThreadLocalProxy(object):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
delattr(child, name) delattr(child, name)
def _get_dict(self): @property
def __dict__(self):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
d = child.__class__.__dict__.copy() d = child.__class__.__dict__.copy()
d.update(child.__dict__) d.update(child.__dict__)
return d return d
__dict__ = property(_get_dict)
def __getitem__(self, key): def __getitem__(self, key):
child = getattr(serving, self.__attrname__) child = getattr(serving, self.__attrname__)
@ -267,6 +267,7 @@ class _ThreadLocalProxy(object):
# Python 3 # Python 3
__bool__ = __nonzero__ __bool__ = __nonzero__
# Create request and response object (the same objects will be used # Create request and response object (the same objects will be used
# throughout the entire life of the webserver, but will redirect # throughout the entire life of the webserver, but will redirect
# to the "serving" object) # to the "serving" object)
@ -277,8 +278,9 @@ response = _ThreadLocalProxy('response')
class _ThreadData(_local): class _ThreadData(_local):
"""A container for thread-specific data.""" """A container for thread-specific data."""
thread_data = _ThreadData() thread_data = _ThreadData()
@ -292,6 +294,7 @@ def _cherrypy_pydoc_resolve(thing, forceload=0):
thing = getattr(serving, thing.__attrname__) thing = getattr(serving, thing.__attrname__)
return _pydoc._builtin_resolve(thing, forceload) return _pydoc._builtin_resolve(thing, forceload)
try: try:
import pydoc as _pydoc import pydoc as _pydoc
_pydoc._builtin_resolve = _pydoc.resolve _pydoc._builtin_resolve = _pydoc.resolve
@ -300,11 +303,7 @@ except ImportError:
pass pass
from cherrypy import _cplogging
class _GlobalLogManager(_cplogging.LogManager): class _GlobalLogManager(_cplogging.LogManager):
"""A site-wide LogManager; routes to app.log or global log as appropriate. """A site-wide LogManager; routes to app.log or global log as appropriate.
This :class:`LogManager<cherrypy._cplogging.LogManager>` implements This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
@ -315,10 +314,13 @@ class _GlobalLogManager(_cplogging.LogManager):
""" """
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
"""Log the given message to the app.log or global log as appropriate. """Log the given message to the app.log or global log.
Log the given message to the app.log or global
log as appropriate.
""" """
# Do NOT use try/except here. See # Do NOT use try/except here. See
# https://bitbucket.org/cherrypy/cherrypy/issue/945 # https://github.com/cherrypy/cherrypy/issues/945
if hasattr(request, 'app') and hasattr(request.app, 'log'): if hasattr(request, 'app') and hasattr(request.app, 'log'):
log = request.app.log log = request.app.log
else: else:
@ -326,7 +328,10 @@ class _GlobalLogManager(_cplogging.LogManager):
return log.error(*args, **kwargs) return log.error(*args, **kwargs)
def access(self): def access(self):
"""Log an access message to the app.log or global log as appropriate. """Log an access message to the app.log or global log.
Log the given message to the app.log or global
log as appropriate.
""" """
try: try:
return request.app.log.access() return request.app.log.access()
@ -342,297 +347,11 @@ log.error_file = ''
log.access_file = '' log.access_file = ''
@engine.subscribe('log')
def _buslog(msg, level): def _buslog(msg, level):
log.error(msg, 'ENGINE', severity=level) log.error(msg, 'ENGINE', severity=level)
engine.subscribe('log', _buslog)
# Helper functions for CP apps #
def expose(func=None, alias=None):
"""Expose the function, optionally providing an alias or set of aliases."""
def expose_(func):
func.exposed = True
if alias is not None:
if isinstance(alias, basestring):
parents[alias.replace(".", "_")] = func
else:
for a in alias:
parents[a.replace(".", "_")] = func
return func
import sys
import types
if isinstance(func, (types.FunctionType, types.MethodType)):
if alias is None:
# @expose
func.exposed = True
return func
else:
# func = expose(func, alias)
parents = sys._getframe(1).f_locals
return expose_(func)
elif func is None:
if alias is None:
# @expose()
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose(alias="alias") or
# @expose(alias=["alias1", "alias2"])
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose("alias") or
# @expose(["alias1", "alias2"])
parents = sys._getframe(1).f_locals
alias = func
return expose_
def popargs(*args, **kwargs):
"""A decorator for _cp_dispatch
(cherrypy.dispatch.Dispatcher.dispatch_method_name).
Optional keyword argument: handler=(Object or Function)
Provides a _cp_dispatch function that pops off path segments into
cherrypy.request.params under the names specified. The dispatch
is then forwarded on to the next vpath element.
Note that any existing (and exposed) member function of the class that
popargs is applied to will override that value of the argument. For
instance, if you have a method named "list" on the class decorated with
popargs, then accessing "/list" will call that function instead of popping
it off as the requested parameter. This restriction applies to all
_cp_dispatch functions. The only way around this restriction is to create
a "blank class" whose only function is to provide _cp_dispatch.
If there are path elements after the arguments, or more arguments
are requested than are available in the vpath, then the 'handler'
keyword argument specifies the next object to handle the parameterized
request. If handler is not specified or is None, then self is used.
If handler is a function rather than an instance, then that function
will be called with the args specified and the return value from that
function used as the next object INSTEAD of adding the parameters to
cherrypy.request.args.
This decorator may be used in one of two ways:
As a class decorator:
@cherrypy.popargs('year', 'month', 'day')
class Blog:
def index(self, year=None, month=None, day=None):
#Process the parameters here; any url like
#/, /2009, /2009/12, or /2009/12/31
#will fill in the appropriate parameters.
def create(self):
#This link will still be available at /create. Defined functions
#take precedence over arguments.
Or as a member of a class:
class Blog:
_cp_dispatch = cherrypy.popargs('year', 'month', 'day')
#...
The handler argument may be used to mix arguments with built in functions.
For instance, the following setup allows different activities at the
day, month, and year level:
class DayHandler:
def index(self, year, month, day):
#Do something with this day; probably list entries
def delete(self, year, month, day):
#Delete all entries for this day
@cherrypy.popargs('day', handler=DayHandler())
class MonthHandler:
def index(self, year, month):
#Do something with this month; probably list entries
def delete(self, year, month):
#Delete all entries for this month
@cherrypy.popargs('month', handler=MonthHandler())
class YearHandler:
def index(self, year):
#Do something with this year
#...
@cherrypy.popargs('year', handler=YearHandler())
class Root:
def index(self):
#...
"""
# Since keyword arg comes after *args, we have to process it ourselves
# for lower versions of python.
handler = None
handler_call = False
for k, v in kwargs.items():
if k == 'handler':
handler = v
else:
raise TypeError(
"cherrypy.popargs() got an unexpected keyword argument '{0}'"
.format(k)
)
import inspect
if handler is not None \
and (hasattr(handler, '__call__') or inspect.isclass(handler)):
handler_call = True
def decorated(cls_or_self=None, vpath=None):
if inspect.isclass(cls_or_self):
# cherrypy.popargs is a class decorator
cls = cls_or_self
setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated)
return cls
# We're in the actual function
self = cls_or_self
parms = {}
for arg in args:
if not vpath:
break
parms[arg] = vpath.pop(0)
if handler is not None:
if handler_call:
return handler(**parms)
else:
request.params.update(parms)
return handler
request.params.update(parms)
# If we are the ultimate handler, then to prevent our _cp_dispatch
# from being called again, we will resolve remaining elements through
# getattr() directly.
if vpath:
return getattr(self, vpath.pop(0), None)
else:
return self
return decorated
def url(path="", qs="", script_name=None, base=None, relative=None):
"""Create an absolute URL for the given path.
If 'path' starts with a slash ('/'), this will return
(base + script_name + path + qs).
If it does not start with a slash, this returns
(base + script_name [+ request.path_info] + path + qs).
If script_name is None, cherrypy.request will be used
to find a script_name, if available.
If base is None, cherrypy.request.base will be used (if available).
Note that you can use cherrypy.tools.proxy to change this.
Finally, note that this function can be used to obtain an absolute URL
for the current request path (minus the querystring) by passing no args.
If you call url(qs=cherrypy.request.query_string), you should get the
original browser URL (assuming no internal redirections).
If relative is None or not provided, request.app.relative_urls will
be used (if available, else False). If False, the output will be an
absolute URL (including the scheme, host, vhost, and script_name).
If True, the output will instead be a URL that is relative to the
current request path, perhaps including '..' atoms. If relative is
the string 'server', the output will instead be a URL that is
relative to the server root; i.e., it will start with a slash.
"""
if isinstance(qs, (tuple, list, dict)):
qs = _urlencode(qs)
if qs:
qs = '?' + qs
if request.app:
if not path.startswith("/"):
# Append/remove trailing slash from path_info as needed
# (this is to support mistyped URL's without redirecting;
# if you want to redirect, use tools.trailing_slash).
pi = request.path_info
if request.is_index is True:
if not pi.endswith('/'):
pi = pi + '/'
elif request.is_index is False:
if pi.endswith('/') and pi != '/':
pi = pi[:-1]
if path == "":
path = pi
else:
path = _urljoin(pi, path)
if script_name is None:
script_name = request.script_name
if base is None:
base = request.base
newurl = base + script_name + path + qs
else:
# No request.app (we're being called outside a request).
# We'll have to guess the base from server.* attributes.
# This will produce very different results from the above
# if you're using vhosts or tools.proxy.
if base is None:
base = server.base()
path = (script_name or "") + path
newurl = base + path + qs
if './' in newurl:
# Normalize the URL by removing ./ and ../
atoms = []
for atom in newurl.split('/'):
if atom == '.':
pass
elif atom == '..':
atoms.pop()
else:
atoms.append(atom)
newurl = '/'.join(atoms)
# At this point, we should have a fully-qualified absolute URL.
if relative is None:
relative = getattr(request.app, "relative_urls", False)
# See http://www.ietf.org/rfc/rfc2396.txt
if relative == 'server':
# "A relative reference beginning with a single slash character is
# termed an absolute-path reference, as defined by <abs_path>..."
# This is also sometimes called "server-relative".
newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
elif relative:
# "A relative reference that does not begin with a scheme name
# or a slash character is termed a relative-path reference."
old = url(relative=False).split('/')[:-1]
new = newurl.split('/')
while old and new:
a, b = old[0], new[0]
if a != b:
break
old.pop(0)
new.pop(0)
new = (['..'] * len(old)) + new
newurl = '/'.join(new)
return newurl
# import _cpconfig last so it can reference other top-level objects
from cherrypy import _cpconfig
# Use _global_conf_alias so quickstart can use 'config' as an arg # Use _global_conf_alias so quickstart can use 'config' as an arg
# without shadowing cherrypy.config. # without shadowing cherrypy.config.
config = _global_conf_alias = _cpconfig.Config() config = _global_conf_alias = _cpconfig.Config()
@ -642,11 +361,10 @@ config.defaults = {
'tools.trailing_slash.on': True, 'tools.trailing_slash.on': True,
'tools.encode.on': True 'tools.encode.on': True
} }
config.namespaces["log"] = lambda k, v: setattr(log, k, v) config.namespaces['log'] = lambda k, v: setattr(log, k, v)
config.namespaces["checker"] = lambda k, v: setattr(checker, k, v) config.namespaces['checker'] = lambda k, v: setattr(checker, k, v)
# Must reset to get our defaults applied. # Must reset to get our defaults applied.
config.reset() config.reset()
from cherrypy import _cpchecker
checker = _cpchecker.Checker() checker = _cpchecker.Checker()
engine.subscribe('start', checker) engine.subscribe('start', checker)

View file

@ -1,4 +1,5 @@
import cherrypy.daemon """CherryPy'd cherryd daemon runner."""
from cherrypy.daemon import run
if __name__ == '__main__':
cherrypy.daemon.run() __name__ == '__main__' and run()

View file

@ -1,12 +1,14 @@
"""Checker for CherryPy sites and mounted apps."""
import os import os
import warnings import warnings
import six
from six.moves import builtins
import cherrypy import cherrypy
from cherrypy._cpcompat import iteritems, copykeys, builtins
class Checker(object): class Checker(object):
"""A checker for CherryPy sites and their mounted applications. """A checker for CherryPy sites and their mounted applications.
When this object is called at engine startup, it executes each When this object is called at engine startup, it executes each
@ -24,6 +26,7 @@ class Checker(object):
"""If True (the default), run all checks; if False, turn off all checks.""" """If True (the default), run all checks; if False, turn off all checks."""
def __init__(self): def __init__(self):
"""Initialize Checker instance."""
self._populate_known_types() self._populate_known_types()
def __call__(self): def __call__(self):
@ -33,7 +36,7 @@ class Checker(object):
warnings.formatwarning = self.formatwarning warnings.formatwarning = self.formatwarning
try: try:
for name in dir(self): for name in dir(self):
if name.startswith("check_"): if name.startswith('check_'):
method = getattr(self, name) method = getattr(self, name)
if method and hasattr(method, '__call__'): if method and hasattr(method, '__call__'):
method() method()
@ -41,15 +44,14 @@ class Checker(object):
warnings.formatwarning = oldformatwarning warnings.formatwarning = oldformatwarning
def formatwarning(self, message, category, filename, lineno, line=None): def formatwarning(self, message, category, filename, lineno, line=None):
"""Function to format a warning.""" """Format a warning."""
return "CherryPy Checker:\n%s\n\n" % message return 'CherryPy Checker:\n%s\n\n' % message
# This value should be set inside _cpconfig. # This value should be set inside _cpconfig.
global_config_contained_paths = False global_config_contained_paths = False
def check_app_config_entries_dont_start_with_script_name(self): def check_app_config_entries_dont_start_with_script_name(self):
"""Check for Application config with sections that repeat script_name. """Check for App config with sections that repeat script_name."""
"""
for sn, app in cherrypy.tree.apps.items(): for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application): if not isinstance(app, cherrypy.Application):
continue continue
@ -57,36 +59,36 @@ class Checker(object):
continue continue
if sn == '': if sn == '':
continue continue
sn_atoms = sn.strip("/").split("/") sn_atoms = sn.strip('/').split('/')
for key in app.config.keys(): for key in app.config.keys():
key_atoms = key.strip("/").split("/") key_atoms = key.strip('/').split('/')
if key_atoms[:len(sn_atoms)] == sn_atoms: if key_atoms[:len(sn_atoms)] == sn_atoms:
warnings.warn( warnings.warn(
"The application mounted at %r has config " 'The application mounted at %r has config '
"entries that start with its script name: %r" % (sn, 'entries that start with its script name: %r' % (sn,
key)) key))
def check_site_config_entries_in_app_config(self): def check_site_config_entries_in_app_config(self):
"""Check for mounted Applications that have site-scoped config.""" """Check for mounted Applications that have site-scoped config."""
for sn, app in iteritems(cherrypy.tree.apps): for sn, app in six.iteritems(cherrypy.tree.apps):
if not isinstance(app, cherrypy.Application): if not isinstance(app, cherrypy.Application):
continue continue
msg = [] msg = []
for section, entries in iteritems(app.config): for section, entries in six.iteritems(app.config):
if section.startswith('/'): if section.startswith('/'):
for key, value in iteritems(entries): for key, value in six.iteritems(entries):
for n in ("engine.", "server.", "tree.", "checker."): for n in ('engine.', 'server.', 'tree.', 'checker.'):
if key.startswith(n): if key.startswith(n):
msg.append("[%s] %s = %s" % msg.append('[%s] %s = %s' %
(section, key, value)) (section, key, value))
if msg: if msg:
msg.insert(0, msg.insert(0,
"The application mounted at %r contains the " 'The application mounted at %r contains the '
"following config entries, which are only allowed " 'following config entries, which are only allowed '
"in site-wide config. Move them to a [global] " 'in site-wide config. Move them to a [global] '
"section and pass them to cherrypy.config.update() " 'section and pass them to cherrypy.config.update() '
"instead of tree.mount()." % sn) 'instead of tree.mount().' % sn)
warnings.warn(os.linesep.join(msg)) warnings.warn(os.linesep.join(msg))
def check_skipped_app_config(self): def check_skipped_app_config(self):
@ -95,32 +97,30 @@ class Checker(object):
if not isinstance(app, cherrypy.Application): if not isinstance(app, cherrypy.Application):
continue continue
if not app.config: if not app.config:
msg = "The Application mounted at %r has an empty config." % sn msg = 'The Application mounted at %r has an empty config.' % sn
if self.global_config_contained_paths: if self.global_config_contained_paths:
msg += (" It looks like the config you passed to " msg += (' It looks like the config you passed to '
"cherrypy.config.update() contains application-" 'cherrypy.config.update() contains application-'
"specific sections. You must explicitly pass " 'specific sections. You must explicitly pass '
"application config via " 'application config via '
"cherrypy.tree.mount(..., config=app_config)") 'cherrypy.tree.mount(..., config=app_config)')
warnings.warn(msg) warnings.warn(msg)
return return
def check_app_config_brackets(self): def check_app_config_brackets(self):
"""Check for Application config with extraneous brackets in section """Check for App config with extraneous brackets in section names."""
names.
"""
for sn, app in cherrypy.tree.apps.items(): for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application): if not isinstance(app, cherrypy.Application):
continue continue
if not app.config: if not app.config:
continue continue
for key in app.config.keys(): for key in app.config.keys():
if key.startswith("[") or key.endswith("]"): if key.startswith('[') or key.endswith(']'):
warnings.warn( warnings.warn(
"The application mounted at %r has config " 'The application mounted at %r has config '
"section names with extraneous brackets: %r. " 'section names with extraneous brackets: %r. '
"Config *files* need brackets; config *dicts* " 'Config *files* need brackets; config *dicts* '
"(e.g. passed to tree.mount) do not." % (sn, key)) '(e.g. passed to tree.mount) do not.' % (sn, key))
def check_static_paths(self): def check_static_paths(self):
"""Check Application config for incorrect static paths.""" """Check Application config for incorrect static paths."""
@ -132,47 +132,47 @@ class Checker(object):
request.app = app request.app = app
for section in app.config: for section in app.config:
# get_resource will populate request.config # get_resource will populate request.config
request.get_resource(section + "/dummy.html") request.get_resource(section + '/dummy.html')
conf = request.config.get conf = request.config.get
if conf("tools.staticdir.on", False): if conf('tools.staticdir.on', False):
msg = "" msg = ''
root = conf("tools.staticdir.root") root = conf('tools.staticdir.root')
dir = conf("tools.staticdir.dir") dir = conf('tools.staticdir.dir')
if dir is None: if dir is None:
msg = "tools.staticdir.dir is not set." msg = 'tools.staticdir.dir is not set.'
else: else:
fulldir = "" fulldir = ''
if os.path.isabs(dir): if os.path.isabs(dir):
fulldir = dir fulldir = dir
if root: if root:
msg = ("dir is an absolute path, even " msg = ('dir is an absolute path, even '
"though a root is provided.") 'though a root is provided.')
testdir = os.path.join(root, dir[1:]) testdir = os.path.join(root, dir[1:])
if os.path.exists(testdir): if os.path.exists(testdir):
msg += ( msg += (
"\nIf you meant to serve the " '\nIf you meant to serve the '
"filesystem folder at %r, remove the " 'filesystem folder at %r, remove the '
"leading slash from dir." % (testdir,)) 'leading slash from dir.' % (testdir,))
else: else:
if not root: if not root:
msg = ( msg = (
"dir is a relative path and " 'dir is a relative path and '
"no root provided.") 'no root provided.')
else: else:
fulldir = os.path.join(root, dir) fulldir = os.path.join(root, dir)
if not os.path.isabs(fulldir): if not os.path.isabs(fulldir):
msg = ("%r is not an absolute path." % ( msg = ('%r is not an absolute path.' % (
fulldir,)) fulldir,))
if fulldir and not os.path.exists(fulldir): if fulldir and not os.path.exists(fulldir):
if msg: if msg:
msg += "\n" msg += '\n'
msg += ("%r (root + dir) is not an existing " msg += ('%r (root + dir) is not an existing '
"filesystem path." % fulldir) 'filesystem path.' % fulldir)
if msg: if msg:
warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r" warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r'
% (msg, section, root, dir)) % (msg, section, root, dir))
# -------------------------- Compatibility -------------------------- # # -------------------------- Compatibility -------------------------- #
@ -196,21 +196,21 @@ class Checker(object):
"""Process config and warn on each obsolete or deprecated entry.""" """Process config and warn on each obsolete or deprecated entry."""
for section, conf in config.items(): for section, conf in config.items():
if isinstance(conf, dict): if isinstance(conf, dict):
for k, v in conf.items(): for k in conf:
if k in self.obsolete: if k in self.obsolete:
warnings.warn("%r is obsolete. Use %r instead.\n" warnings.warn('%r is obsolete. Use %r instead.\n'
"section: [%s]" % 'section: [%s]' %
(k, self.obsolete[k], section)) (k, self.obsolete[k], section))
elif k in self.deprecated: elif k in self.deprecated:
warnings.warn("%r is deprecated. Use %r instead.\n" warnings.warn('%r is deprecated. Use %r instead.\n'
"section: [%s]" % 'section: [%s]' %
(k, self.deprecated[k], section)) (k, self.deprecated[k], section))
else: else:
if section in self.obsolete: if section in self.obsolete:
warnings.warn("%r is obsolete. Use %r instead." warnings.warn('%r is obsolete. Use %r instead.'
% (section, self.obsolete[section])) % (section, self.obsolete[section]))
elif section in self.deprecated: elif section in self.deprecated:
warnings.warn("%r is deprecated. Use %r instead." warnings.warn('%r is deprecated. Use %r instead.'
% (section, self.deprecated[section])) % (section, self.deprecated[section]))
def check_compatibility(self): def check_compatibility(self):
@ -225,40 +225,40 @@ class Checker(object):
extra_config_namespaces = [] extra_config_namespaces = []
def _known_ns(self, app): def _known_ns(self, app):
ns = ["wsgi"] ns = ['wsgi']
ns.extend(copykeys(app.toolboxes)) ns.extend(app.toolboxes)
ns.extend(copykeys(app.namespaces)) ns.extend(app.namespaces)
ns.extend(copykeys(app.request_class.namespaces)) ns.extend(app.request_class.namespaces)
ns.extend(copykeys(cherrypy.config.namespaces)) ns.extend(cherrypy.config.namespaces)
ns += self.extra_config_namespaces ns += self.extra_config_namespaces
for section, conf in app.config.items(): for section, conf in app.config.items():
is_path_section = section.startswith("/") is_path_section = section.startswith('/')
if is_path_section and isinstance(conf, dict): if is_path_section and isinstance(conf, dict):
for k, v in conf.items(): for k in conf:
atoms = k.split(".") atoms = k.split('.')
if len(atoms) > 1: if len(atoms) > 1:
if atoms[0] not in ns: if atoms[0] not in ns:
# Spit out a special warning if a known # Spit out a special warning if a known
# namespace is preceded by "cherrypy." # namespace is preceded by "cherrypy."
if atoms[0] == "cherrypy" and atoms[1] in ns: if atoms[0] == 'cherrypy' and atoms[1] in ns:
msg = ( msg = (
"The config entry %r is invalid; " 'The config entry %r is invalid; '
"try %r instead.\nsection: [%s]" 'try %r instead.\nsection: [%s]'
% (k, ".".join(atoms[1:]), section)) % (k, '.'.join(atoms[1:]), section))
else: else:
msg = ( msg = (
"The config entry %r is invalid, " 'The config entry %r is invalid, '
"because the %r config namespace " 'because the %r config namespace '
"is unknown.\n" 'is unknown.\n'
"section: [%s]" % (k, atoms[0], section)) 'section: [%s]' % (k, atoms[0], section))
warnings.warn(msg) warnings.warn(msg)
elif atoms[0] == "tools": elif atoms[0] == 'tools':
if atoms[1] not in dir(cherrypy.tools): if atoms[1] not in dir(cherrypy.tools):
msg = ( msg = (
"The config entry %r may be invalid, " 'The config entry %r may be invalid, '
"because the %r tool was not found.\n" 'because the %r tool was not found.\n'
"section: [%s]" % (k, atoms[1], section)) 'section: [%s]' % (k, atoms[1], section))
warnings.warn(msg) warnings.warn(msg)
def check_config_namespaces(self): def check_config_namespaces(self):
@ -282,29 +282,22 @@ class Checker(object):
continue continue
vtype = type(getattr(obj, name, None)) vtype = type(getattr(obj, name, None))
if vtype in b: if vtype in b:
self.known_config_types[namespace + "." + name] = vtype self.known_config_types[namespace + '.' + name] = vtype
traverse(cherrypy.request, "request") traverse(cherrypy.request, 'request')
traverse(cherrypy.response, "response") traverse(cherrypy.response, 'response')
traverse(cherrypy.server, "server") traverse(cherrypy.server, 'server')
traverse(cherrypy.engine, "engine") traverse(cherrypy.engine, 'engine')
traverse(cherrypy.log, "log") traverse(cherrypy.log, 'log')
def _known_types(self, config): def _known_types(self, config):
msg = ("The config entry %r in section %r is of type %r, " msg = ('The config entry %r in section %r is of type %r, '
"which does not match the expected type %r.") 'which does not match the expected type %r.')
for section, conf in config.items(): for section, conf in config.items():
if isinstance(conf, dict): if not isinstance(conf, dict):
for k, v in conf.items(): conf = {section: conf}
if v is not None: for k, v in conf.items():
expected_type = self.known_config_types.get(k, None)
vtype = type(v)
if expected_type and vtype != expected_type:
warnings.warn(msg % (k, section, vtype.__name__,
expected_type.__name__))
else:
k, v = section, conf
if v is not None: if v is not None:
expected_type = self.known_config_types.get(k, None) expected_type = self.known_config_types.get(k, None)
vtype = type(v) vtype = type(v)
@ -326,7 +319,7 @@ class Checker(object):
for k, v in cherrypy.config.items(): for k, v in cherrypy.config.items():
if k == 'server.socket_host' and v == 'localhost': if k == 'server.socket_host' and v == 'localhost':
warnings.warn("The use of 'localhost' as a socket host can " warnings.warn("The use of 'localhost' as a socket host can "
"cause problems on newer systems, since " 'cause problems on newer systems, since '
"'localhost' can map to either an IPv4 or an " "'localhost' can map to either an IPv4 or an "
"IPv6 address. You should use '127.0.0.1' " "IPv6 address. You should use '127.0.0.1' "
"or '[::1]' instead.") "or '[::1]' instead.")

View file

@ -1,32 +1,32 @@
"""Compatibility code for using CherryPy with various versions of Python. """Compatibility code for using CherryPy with various versions of Python.
CherryPy 3.2 is compatible with Python versions 2.3+. This module provides a To retain compatibility with older Python versions, this module provides a
useful abstraction over the differences between Python versions, sometimes by useful abstraction over the differences between Python versions, sometimes by
preferring a newer idiom, sometimes an older one, and sometimes a custom one. preferring a newer idiom, sometimes an older one, and sometimes a custom one.
In particular, Python 2 uses str and '' for byte strings, while Python 3 In particular, Python 2 uses str and '' for byte strings, while Python 3
uses str and '' for unicode strings. We will call each of these the 'native uses str and '' for unicode strings. We will call each of these the 'native
string' type for each version. Because of this major difference, this module string' type for each version. Because of this major difference, this module
provides new 'bytestr', 'unicodestr', and 'nativestr' attributes, as well as provides
two functions: 'ntob', which translates native strings (of type 'str') into two functions: 'ntob', which translates native strings (of type 'str') into
byte strings regardless of Python version, and 'ntou', which translates native byte strings regardless of Python version, and 'ntou', which translates native
strings to unicode strings. This also provides a 'BytesIO' name for dealing strings to unicode strings.
specifically with bytes, and a 'StringIO' name for dealing with native strings.
It also provides a 'base64_decode' function with native strings as input and Try not to use the compatibility functions 'ntob', 'ntou', 'tonative'.
output. They were created with Python 2.3-2.5 compatibility in mind.
Instead, use unicode literals (from __future__) and bytes literals
and their .encode/.decode methods as needed.
""" """
import os
import re import re
import sys import sys
import threading import threading
if sys.version_info >= (3, 0): import six
py3k = True from six.moves import urllib
bytestr = bytes
unicodestr = str
nativestr = unicodestr
basestring = (bytes, str)
if six.PY3:
def ntob(n, encoding='ISO-8859-1'): def ntob(n, encoding='ISO-8859-1'):
"""Return the given native string as a byte string in the given """Return the given native string as a byte string in the given
encoding. encoding.
@ -49,18 +49,8 @@ if sys.version_info >= (3, 0):
if isinstance(n, bytes): if isinstance(n, bytes):
return n.decode(encoding) return n.decode(encoding)
return n return n
# type("")
from io import StringIO
# bytes:
from io import BytesIO as BytesIO
else: else:
# Python 2 # Python 2
py3k = False
bytestr = str
unicodestr = unicode
nativestr = bytestr
basestring = basestring
def ntob(n, encoding='ISO-8859-1'): def ntob(n, encoding='ISO-8859-1'):
"""Return the given native string as a byte string in the given """Return the given native string as a byte string in the given
encoding. encoding.
@ -82,9 +72,9 @@ else:
# escapes, but without having to prefix it with u'' for Python 2, # escapes, but without having to prefix it with u'' for Python 2,
# but no prefix for Python 3. # but no prefix for Python 3.
if encoding == 'escape': if encoding == 'escape':
return unicode( return six.text_type( # unicode for Python 2
re.sub(r'\\u([0-9a-zA-Z]{4})', re.sub(r'\\u([0-9a-zA-Z]{4})',
lambda m: unichr(int(m.group(1), 16)), lambda m: six.unichr(int(m.group(1), 16)),
n.decode('ISO-8859-1'))) n.decode('ISO-8859-1')))
# Assume it's already in the given encoding, which for ISO-8859-1 # Assume it's already in the given encoding, which for ISO-8859-1
# is almost always what was intended. # is almost always what was intended.
@ -93,247 +83,58 @@ else:
def tonative(n, encoding='ISO-8859-1'): def tonative(n, encoding='ISO-8859-1'):
"""Return the given string as a native string in the given encoding.""" """Return the given string as a native string in the given encoding."""
# In Python 2, the native string type is bytes. # In Python 2, the native string type is bytes.
if isinstance(n, unicode): if isinstance(n, six.text_type): # unicode for Python 2
return n.encode(encoding) return n.encode(encoding)
return n return n
try:
# type("")
from cStringIO import StringIO
except ImportError:
# type("")
from StringIO import StringIO
# bytes:
BytesIO = StringIO
def assert_native(n): def assert_native(n):
if not isinstance(n, nativestr): if not isinstance(n, str):
raise TypeError("n must be a native str (got %s)" % type(n).__name__) raise TypeError('n must be a native str (got %s)' % type(n).__name__)
try:
# Python 3.1+
from base64 import decodebytes as _base64_decodebytes
except ImportError:
# Python 3.0-
# since CherryPy claims compability with Python 2.3, we must use
# the legacy API of base64
from base64 import decodestring as _base64_decodebytes
def base64_decode(n, encoding='ISO-8859-1'):
"""Return the native string base64-decoded (as a native string)."""
if isinstance(n, unicodestr):
b = n.encode(encoding)
else:
b = n
b = _base64_decodebytes(b)
if nativestr is unicodestr:
return b.decode(encoding)
else:
return b
try:
sorted = sorted
except NameError:
def sorted(i):
i = i[:]
i.sort()
return i
try:
reversed = reversed
except NameError:
def reversed(x):
i = len(x)
while i > 0:
i -= 1
yield x[i]
try:
# Python 3
from urllib.parse import urljoin, urlencode
from urllib.parse import quote, quote_plus
from urllib.request import unquote, urlopen
from urllib.request import parse_http_list, parse_keqv_list
except ImportError:
# Python 2
from urlparse import urljoin
from urllib import urlencode, urlopen
from urllib import quote, quote_plus
from urllib import unquote
from urllib2 import parse_http_list, parse_keqv_list
try:
from threading import local as threadlocal
except ImportError:
from cherrypy._cpthreadinglocal import local as threadlocal
try:
dict.iteritems
# Python 2
iteritems = lambda d: d.iteritems()
copyitems = lambda d: d.items()
except AttributeError:
# Python 3
iteritems = lambda d: d.items()
copyitems = lambda d: list(d.items())
try:
dict.iterkeys
# Python 2
iterkeys = lambda d: d.iterkeys()
copykeys = lambda d: d.keys()
except AttributeError:
# Python 3
iterkeys = lambda d: d.keys()
copykeys = lambda d: list(d.keys())
try:
dict.itervalues
# Python 2
itervalues = lambda d: d.itervalues()
copyvalues = lambda d: d.values()
except AttributeError:
# Python 3
itervalues = lambda d: d.values()
copyvalues = lambda d: list(d.values())
try:
# Python 3
import builtins
except ImportError:
# Python 2
import __builtin__ as builtins
try:
# Python 2. We try Python 2 first clients on Python 2
# don't try to import the 'http' module from cherrypy.lib
from Cookie import SimpleCookie, CookieError
from httplib import BadStatusLine, HTTPConnection, IncompleteRead
from httplib import NotConnected
from BaseHTTPServer import BaseHTTPRequestHandler
except ImportError:
# Python 3
from http.cookies import SimpleCookie, CookieError
from http.client import BadStatusLine, HTTPConnection, IncompleteRead
from http.client import NotConnected
from http.server import BaseHTTPRequestHandler
# Some platforms don't expose HTTPSConnection, so handle it separately # Some platforms don't expose HTTPSConnection, so handle it separately
if py3k: HTTPSConnection = getattr(six.moves.http_client, 'HTTPSConnection', None)
try:
from http.client import HTTPSConnection
except ImportError: def _unquote_plus_compat(string, encoding='utf-8', errors='replace'):
# Some platforms which don't have SSL don't expose HTTPSConnection return urllib.parse.unquote_plus(string).decode(encoding, errors)
HTTPSConnection = None
else:
try: def _unquote_compat(string, encoding='utf-8', errors='replace'):
from httplib import HTTPSConnection return urllib.parse.unquote(string).decode(encoding, errors)
except ImportError:
HTTPSConnection = None
def _quote_compat(string, encoding='utf-8', errors='replace'):
return urllib.parse.quote(string.encode(encoding, errors))
unquote_plus = urllib.parse.unquote_plus if six.PY3 else _unquote_plus_compat
unquote = urllib.parse.unquote if six.PY3 else _unquote_compat
quote = urllib.parse.quote if six.PY3 else _quote_compat
try: try:
# Python 2 # Prefer simplejson
xrange = xrange
except NameError:
# Python 3
xrange = range
import threading
if hasattr(threading.Thread, "daemon"):
# Python 2.6+
def get_daemon(t):
return t.daemon
def set_daemon(t, val):
t.daemon = val
else:
def get_daemon(t):
return t.isDaemon()
def set_daemon(t, val):
t.setDaemon(val)
try:
from email.utils import formatdate
def HTTPDate(timeval=None):
return formatdate(timeval, usegmt=True)
except ImportError:
from rfc822 import formatdate as HTTPDate
try:
# Python 3
from urllib.parse import unquote as parse_unquote
def unquote_qs(atom, encoding, errors='strict'):
return parse_unquote(
atom.replace('+', ' '),
encoding=encoding,
errors=errors)
except ImportError:
# Python 2
from urllib import unquote as parse_unquote
def unquote_qs(atom, encoding, errors='strict'):
return parse_unquote(atom.replace('+', ' ')).decode(encoding, errors)
try:
# Prefer simplejson, which is usually more advanced than the builtin
# module.
import simplejson as json import simplejson as json
json_decode = json.JSONDecoder().decode
_json_encode = json.JSONEncoder().iterencode
except ImportError: except ImportError:
if sys.version_info >= (2, 6): import json
# Python >=2.6 : json is part of the standard library
import json
json_decode = json.JSONDecoder().decode
_json_encode = json.JSONEncoder().iterencode
else:
json = None
def json_decode(s):
raise ValueError('No JSON library is available')
def _json_encode(s):
raise ValueError('No JSON library is available')
finally:
if json and py3k:
# The two Python 3 implementations (simplejson/json)
# outputs str. We need bytes.
def json_encode(value):
for chunk in _json_encode(value):
yield chunk.encode('utf8')
else:
json_encode = _json_encode
try: json_decode = json.JSONDecoder().decode
import cPickle as pickle _json_encode = json.JSONEncoder().iterencode
except ImportError:
# In Python 2, pickle is a Python version.
# In Python 3, pickle is the sped-up C version.
import pickle
import binascii
def random20(): if six.PY3:
return binascii.hexlify(os.urandom(20)).decode('ascii') # Encode to bytes on Python 3
def json_encode(value):
for chunk in _json_encode(value):
yield chunk.encode('utf-8')
else:
json_encode = _json_encode
try:
from _thread import get_ident as get_thread_ident
except ImportError:
from thread import get_ident as get_thread_ident
try: text_or_bytes = six.text_type, bytes
# Python 3
next = next
except NameError:
# Python 2
def next(i):
return i.next()
if sys.version_info >= (3, 3): if sys.version_info >= (3, 3):
Timer = threading.Timer Timer = threading.Timer
@ -343,17 +144,19 @@ else:
Timer = threading._Timer Timer = threading._Timer
Event = threading._Event Event = threading._Event
# Prior to Python 2.6, the Thread class did not have a .daemon property. # html module come in 3.2 version
# This mix-in adds that property. try:
from html import escape
except ImportError:
from cgi import escape
class SetDaemonProperty: # html module needed the argument quote=False because in cgi the default
# is False. With quote=True the results differ.
def __get_daemon(self): def escape_html(s, escape_quote=False):
return self.isDaemon() """Replace special characters "&", "<" and ">" to HTML-safe sequences.
def __set_daemon(self, daemon): When escape_quote=True, escape (') and (") chars.
self.setDaemon(daemon) """
return escape(s, quote=escape_quote)
if sys.version_info < (2, 6):
daemon = property(__get_daemon, __set_daemon)

File diff suppressed because it is too large Load diff

View file

@ -46,21 +46,21 @@ To declare global configuration entries, place them in a [global] section.
You may also declare config entries directly on the classes and methods You may also declare config entries directly on the classes and methods
(page handlers) that make up your CherryPy application via the ``_cp_config`` (page handlers) that make up your CherryPy application via the ``_cp_config``
attribute. For example:: attribute, set with the ``cherrypy.config`` decorator. For example::
@cherrypy.config(**{'tools.gzip.on': True})
class Demo: class Demo:
_cp_config = {'tools.gzip.on': True}
@cherrypy.expose
@cherrypy.config(**{'request.show_tracebacks': False})
def index(self): def index(self):
return "Hello world" return "Hello world"
index.exposed = True
index._cp_config = {'request.show_tracebacks': False}
.. note:: .. note::
This behavior is only guaranteed for the default dispatcher. This behavior is only guaranteed for the default dispatcher.
Other dispatchers may have different restrictions on where Other dispatchers may have different restrictions on where
you can attach _cp_config attributes. you can attach config attributes.
Namespaces Namespaces
@ -119,11 +119,14 @@ style) context manager.
""" """
import cherrypy import cherrypy
from cherrypy._cpcompat import basestring from cherrypy._cpcompat import text_or_bytes
from cherrypy.lib import reprconf from cherrypy.lib import reprconf
# Deprecated in CherryPy 3.2--remove in 3.3
NamespaceSet = reprconf.NamespaceSet def _if_filename_register_autoreload(ob):
"""Register for autoreload if ob is a string (presumed filename)."""
is_filename = isinstance(ob, text_or_bytes)
is_filename and cherrypy.engine.autoreload.files.add(ob)
def merge(base, other): def merge(base, other):
@ -132,67 +135,68 @@ def merge(base, other):
If the given config is a filename, it will be appended to If the given config is a filename, it will be appended to
the list of files to monitor for "autoreload" changes. the list of files to monitor for "autoreload" changes.
""" """
if isinstance(other, basestring): _if_filename_register_autoreload(other)
cherrypy.engine.autoreload.files.add(other)
# Load other into base # Load other into base
for section, value_map in reprconf.as_dict(other).items(): for section, value_map in reprconf.Parser.load(other).items():
if not isinstance(value_map, dict): if not isinstance(value_map, dict):
raise ValueError( raise ValueError(
"Application config must include section headers, but the " 'Application config must include section headers, but the '
"config you tried to merge doesn't have any sections. " "config you tried to merge doesn't have any sections. "
"Wrap your config in another dict with paths as section " 'Wrap your config in another dict with paths as section '
"headers, for example: {'/': config}.") "headers, for example: {'/': config}.")
base.setdefault(section, {}).update(value_map) base.setdefault(section, {}).update(value_map)
class Config(reprconf.Config): class Config(reprconf.Config):
"""The 'global' configuration data for the entire CherryPy process.""" """The 'global' configuration data for the entire CherryPy process."""
def update(self, config): def update(self, config):
"""Update self from a dict, file or filename.""" """Update self from a dict, file or filename."""
if isinstance(config, basestring): _if_filename_register_autoreload(config)
# Filename super(Config, self).update(config)
cherrypy.engine.autoreload.files.add(config)
reprconf.Config.update(self, config)
def _apply(self, config): def _apply(self, config):
"""Update self from a dict.""" """Update self from a dict."""
if isinstance(config.get("global"), dict): if isinstance(config.get('global'), dict):
if len(config) > 1: if len(config) > 1:
cherrypy.checker.global_config_contained_paths = True cherrypy.checker.global_config_contained_paths = True
config = config["global"] config = config['global']
if 'tools.staticdir.dir' in config: if 'tools.staticdir.dir' in config:
config['tools.staticdir.section'] = "global" config['tools.staticdir.section'] = 'global'
reprconf.Config._apply(self, config) super(Config, self)._apply(config)
def __call__(self, *args, **kwargs):
"""Decorator for page handlers to set _cp_config."""
if args:
raise TypeError(
"The cherrypy.config decorator does not accept positional "
"arguments; you must use keyword arguments.")
@staticmethod
def __call__(**kwargs):
"""Decorate for page handlers to set _cp_config."""
def tool_decorator(f): def tool_decorator(f):
if not hasattr(f, "_cp_config"): _Vars(f).setdefault('_cp_config', {}).update(kwargs)
f._cp_config = {}
for k, v in kwargs.items():
f._cp_config[k] = v
return f return f
return tool_decorator return tool_decorator
class _Vars(object):
"""Adapter allowing setting a default attribute on a function or class."""
def __init__(self, target):
self.target = target
def setdefault(self, key, default):
if not hasattr(self.target, key):
setattr(self.target, key, default)
return getattr(self.target, key)
# Sphinx begin config.environments # Sphinx begin config.environments
Config.environments = environments = { Config.environments = environments = {
"staging": { 'staging': {
'engine.autoreload.on': False, 'engine.autoreload.on': False,
'checker.on': False, 'checker.on': False,
'tools.log_headers.on': False, 'tools.log_headers.on': False,
'request.show_tracebacks': False, 'request.show_tracebacks': False,
'request.show_mismatched_params': False, 'request.show_mismatched_params': False,
}, },
"production": { 'production': {
'engine.autoreload.on': False, 'engine.autoreload.on': False,
'checker.on': False, 'checker.on': False,
'tools.log_headers.on': False, 'tools.log_headers.on': False,
@ -200,7 +204,7 @@ Config.environments = environments = {
'request.show_mismatched_params': False, 'request.show_mismatched_params': False,
'log.screen': False, 'log.screen': False,
}, },
"embedded": { 'embedded': {
# For use with CherryPy embedded in another deployment stack. # For use with CherryPy embedded in another deployment stack.
'engine.autoreload.on': False, 'engine.autoreload.on': False,
'checker.on': False, 'checker.on': False,
@ -211,7 +215,7 @@ Config.environments = environments = {
'engine.SIGHUP': None, 'engine.SIGHUP': None,
'engine.SIGTERM': None, 'engine.SIGTERM': None,
}, },
"test_suite": { 'test_suite': {
'engine.autoreload.on': False, 'engine.autoreload.on': False,
'checker.on': False, 'checker.on': False,
'tools.log_headers.on': False, 'tools.log_headers.on': False,
@ -225,11 +229,11 @@ Config.environments = environments = {
def _server_namespace_handler(k, v): def _server_namespace_handler(k, v):
"""Config handler for the "server" namespace.""" """Config handler for the "server" namespace."""
atoms = k.split(".", 1) atoms = k.split('.', 1)
if len(atoms) > 1: if len(atoms) > 1:
# Special-case config keys of the form 'server.servername.socket_port' # Special-case config keys of the form 'server.servername.socket_port'
# to configure additional HTTP servers. # to configure additional HTTP servers.
if not hasattr(cherrypy, "servers"): if not hasattr(cherrypy, 'servers'):
cherrypy.servers = {} cherrypy.servers = {}
servername, k = atoms servername, k = atoms
@ -248,60 +252,33 @@ def _server_namespace_handler(k, v):
setattr(cherrypy.servers[servername], k, v) setattr(cherrypy.servers[servername], k, v)
else: else:
setattr(cherrypy.server, k, v) setattr(cherrypy.server, k, v)
Config.namespaces["server"] = _server_namespace_handler
Config.namespaces['server'] = _server_namespace_handler
def _engine_namespace_handler(k, v): def _engine_namespace_handler(k, v):
"""Backward compatibility handler for the "engine" namespace.""" """Config handler for the "engine" namespace."""
engine = cherrypy.engine engine = cherrypy.engine
deprecated = { if k in {'SIGHUP', 'SIGTERM'}:
'autoreload_on': 'autoreload.on', engine.subscribe(k, v)
'autoreload_frequency': 'autoreload.frequency', return
'autoreload_match': 'autoreload.match',
'reload_files': 'autoreload.files',
'deadlock_poll_freq': 'timeout_monitor.frequency'
}
if k in deprecated: if '.' in k:
engine.log( plugin, attrname = k.split('.', 1)
'WARNING: Use of engine.%s is deprecated and will be removed in a '
'future version. Use engine.%s instead.' % (k, deprecated[k]))
if k == 'autoreload_on':
if v:
engine.autoreload.subscribe()
else:
engine.autoreload.unsubscribe()
elif k == 'autoreload_frequency':
engine.autoreload.frequency = v
elif k == 'autoreload_match':
engine.autoreload.match = v
elif k == 'reload_files':
engine.autoreload.files = set(v)
elif k == 'deadlock_poll_freq':
engine.timeout_monitor.frequency = v
elif k == 'SIGHUP':
engine.listeners['SIGHUP'] = set([v])
elif k == 'SIGTERM':
engine.listeners['SIGTERM'] = set([v])
elif "." in k:
plugin, attrname = k.split(".", 1)
plugin = getattr(engine, plugin) plugin = getattr(engine, plugin)
if attrname == 'on': op = 'subscribe' if v else 'unsubscribe'
if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'): sub_unsub = getattr(plugin, op, None)
plugin.subscribe() if attrname == 'on' and callable(sub_unsub):
return sub_unsub()
elif ( return
(not v) and
hasattr(getattr(plugin, 'unsubscribe', None), '__call__')
):
plugin.unsubscribe()
return
setattr(plugin, attrname, v) setattr(plugin, attrname, v)
else: else:
setattr(engine, k, v) setattr(engine, k, v)
Config.namespaces["engine"] = _engine_namespace_handler
Config.namespaces['engine'] = _engine_namespace_handler
def _tree_namespace_handler(k, v): def _tree_namespace_handler(k, v):
@ -309,9 +286,11 @@ def _tree_namespace_handler(k, v):
if isinstance(v, dict): if isinstance(v, dict):
for script_name, app in v.items(): for script_name, app in v.items():
cherrypy.tree.graft(app, script_name) cherrypy.tree.graft(app, script_name)
cherrypy.engine.log("Mounted: %s on %s" % msg = 'Mounted: %s on %s' % (app, script_name or '/')
(app, script_name or "/")) cherrypy.engine.log(msg)
else: else:
cherrypy.tree.graft(v, v.script_name) cherrypy.tree.graft(v, v.script_name)
cherrypy.engine.log("Mounted: %s on %s" % (v, v.script_name or "/")) cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/'))
Config.namespaces["tree"] = _tree_namespace_handler
Config.namespaces['tree'] = _tree_namespace_handler

View file

@ -29,32 +29,26 @@ class PageHandler(object):
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
def get_args(self): @property
def args(self):
"""The ordered args should be accessible from post dispatch hooks."""
return cherrypy.serving.request.args return cherrypy.serving.request.args
def set_args(self, args): @args.setter
def args(self, args):
cherrypy.serving.request.args = args cherrypy.serving.request.args = args
return cherrypy.serving.request.args return cherrypy.serving.request.args
args = property( @property
get_args, def kwargs(self):
set_args, """The named kwargs should be accessible from post dispatch hooks."""
doc="The ordered args should be accessible from post dispatch hooks"
)
def get_kwargs(self):
return cherrypy.serving.request.kwargs return cherrypy.serving.request.kwargs
def set_kwargs(self, kwargs): @kwargs.setter
def kwargs(self, kwargs):
cherrypy.serving.request.kwargs = kwargs cherrypy.serving.request.kwargs = kwargs
return cherrypy.serving.request.kwargs return cherrypy.serving.request.kwargs
kwargs = property(
get_kwargs,
set_kwargs,
doc="The named kwargs should be accessible from post dispatch hooks"
)
def __call__(self): def __call__(self):
try: try:
return self.callable(*self.args, **self.kwargs) return self.callable(*self.args, **self.kwargs)
@ -64,7 +58,7 @@ class PageHandler(object):
test_callable_spec(self.callable, self.args, self.kwargs) test_callable_spec(self.callable, self.args, self.kwargs)
except cherrypy.HTTPError: except cherrypy.HTTPError:
raise sys.exc_info()[1] raise sys.exc_info()[1]
except: except Exception:
raise x raise x
raise raise
@ -102,7 +96,13 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
# the original error # the original error
raise raise
if args and args[0] == 'self': if args and (
# For callable objects, which have a __call__(self) method
hasattr(callable, '__call__') or
# For normal methods
inspect.ismethod(callable)
):
# Strip 'self'
args = args[1:] args = args[1:]
arg_usage = dict([(arg, 0,) for arg in args]) arg_usage = dict([(arg, 0,) for arg in args])
@ -153,7 +153,7 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
# arguments it's definitely a 404. # arguments it's definitely a 404.
message = None message = None
if show_mismatched_params: if show_mismatched_params:
message = "Missing parameters: %s" % ",".join(missing_args) message = 'Missing parameters: %s' % ','.join(missing_args)
raise cherrypy.HTTPError(404, message=message) raise cherrypy.HTTPError(404, message=message)
# the extra positional arguments come from the path - 404 Not Found # the extra positional arguments come from the path - 404 Not Found
@ -175,8 +175,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
message = None message = None
if show_mismatched_params: if show_mismatched_params:
message = "Multiple values for parameters: "\ message = 'Multiple values for parameters: '\
"%s" % ",".join(multiple_args) '%s' % ','.join(multiple_args)
raise cherrypy.HTTPError(error, message=message) raise cherrypy.HTTPError(error, message=message)
if not varkw and varkw_usage > 0: if not varkw and varkw_usage > 0:
@ -186,8 +186,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
if extra_qs_params: if extra_qs_params:
message = None message = None
if show_mismatched_params: if show_mismatched_params:
message = "Unexpected query string "\ message = 'Unexpected query string '\
"parameters: %s" % ", ".join(extra_qs_params) 'parameters: %s' % ', '.join(extra_qs_params)
raise cherrypy.HTTPError(404, message=message) raise cherrypy.HTTPError(404, message=message)
# If there were any extra body parameters, it's a 400 Not Found # If there were any extra body parameters, it's a 400 Not Found
@ -195,18 +195,20 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
if extra_body_params: if extra_body_params:
message = None message = None
if show_mismatched_params: if show_mismatched_params:
message = "Unexpected body parameters: "\ message = 'Unexpected body parameters: '\
"%s" % ", ".join(extra_body_params) '%s' % ', '.join(extra_body_params)
raise cherrypy.HTTPError(400, message=message) raise cherrypy.HTTPError(400, message=message)
try: try:
import inspect import inspect
except ImportError: except ImportError:
test_callable_spec = lambda callable, args, kwargs: None def test_callable_spec(callable, args, kwargs): # noqa: F811
return None
else: else:
getargspec = inspect.getargspec getargspec = inspect.getargspec
# Python 3 requires using getfullargspec if keyword-only arguments are present # Python 3 requires using getfullargspec if
# keyword-only arguments are present
if hasattr(inspect, 'getfullargspec'): if hasattr(inspect, 'getfullargspec'):
def getargspec(callable): def getargspec(callable):
return inspect.getfullargspec(callable)[:4] return inspect.getfullargspec(callable)[:4]
@ -222,20 +224,19 @@ class LateParamPageHandler(PageHandler):
(it's more complicated than that, but that's the effect). (it's more complicated than that, but that's the effect).
""" """
def _get_kwargs(self): @property
def kwargs(self):
"""Page handler kwargs (with cherrypy.request.params copied in)."""
kwargs = cherrypy.serving.request.params.copy() kwargs = cherrypy.serving.request.params.copy()
if self._kwargs: if self._kwargs:
kwargs.update(self._kwargs) kwargs.update(self._kwargs)
return kwargs return kwargs
def _set_kwargs(self, kwargs): @kwargs.setter
def kwargs(self, kwargs):
cherrypy.serving.request.kwargs = kwargs cherrypy.serving.request.kwargs = kwargs
self._kwargs = kwargs self._kwargs = kwargs
kwargs = property(_get_kwargs, _set_kwargs,
doc='page handler kwargs (with '
'cherrypy.request.params copied in)')
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
punctuation_to_underscores = string.maketrans( punctuation_to_underscores = string.maketrans(
@ -244,14 +245,14 @@ if sys.version_info < (3, 0):
def validate_translator(t): def validate_translator(t):
if not isinstance(t, str) or len(t) != 256: if not isinstance(t, str) or len(t) != 256:
raise ValueError( raise ValueError(
"The translate argument must be a str of len 256.") 'The translate argument must be a str of len 256.')
else: else:
punctuation_to_underscores = str.maketrans( punctuation_to_underscores = str.maketrans(
string.punctuation, '_' * len(string.punctuation)) string.punctuation, '_' * len(string.punctuation))
def validate_translator(t): def validate_translator(t):
if not isinstance(t, dict): if not isinstance(t, dict):
raise ValueError("The translate argument must be a dict.") raise ValueError('The translate argument must be a dict.')
class Dispatcher(object): class Dispatcher(object):
@ -289,7 +290,7 @@ class Dispatcher(object):
if func: if func:
# Decode any leftover %2F in the virtual_path atoms. # Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace("%2F", "/") for x in vpath] vpath = [x.replace('%2F', '/') for x in vpath]
request.handler = LateParamPageHandler(func, *vpath) request.handler = LateParamPageHandler(func, *vpath)
else: else:
request.handler = cherrypy.NotFound() request.handler = cherrypy.NotFound()
@ -323,10 +324,10 @@ class Dispatcher(object):
fullpath_len = len(fullpath) fullpath_len = len(fullpath)
segleft = fullpath_len segleft = fullpath_len
nodeconf = {} nodeconf = {}
if hasattr(root, "_cp_config"): if hasattr(root, '_cp_config'):
nodeconf.update(root._cp_config) nodeconf.update(root._cp_config)
if "/" in app.config: if '/' in app.config:
nodeconf.update(app.config["/"]) nodeconf.update(app.config['/'])
object_trail = [['root', root, nodeconf, segleft]] object_trail = [['root', root, nodeconf, segleft]]
node = root node = root
@ -361,9 +362,9 @@ class Dispatcher(object):
if segleft > pre_len: if segleft > pre_len:
# No path segment was removed. Raise an error. # No path segment was removed. Raise an error.
raise cherrypy.CherryPyException( raise cherrypy.CherryPyException(
"A vpath segment was added. Custom dispatchers may only " 'A vpath segment was added. Custom dispatchers may only '
+ "remove elements. While trying to process " 'remove elements. While trying to process '
+ "{0} in {1}".format(name, fullpath) '{0} in {1}'.format(name, fullpath)
) )
elif segleft == pre_len: elif segleft == pre_len:
# Assume that the handler used the current path segment, but # Assume that the handler used the current path segment, but
@ -375,7 +376,7 @@ class Dispatcher(object):
if node is not None: if node is not None:
# Get _cp_config attached to this node. # Get _cp_config attached to this node.
if hasattr(node, "_cp_config"): if hasattr(node, '_cp_config'):
nodeconf.update(node._cp_config) nodeconf.update(node._cp_config)
# Mix in values from app.config for this path. # Mix in values from app.config for this path.
@ -414,16 +415,16 @@ class Dispatcher(object):
continue continue
# Try a "default" method on the current leaf. # Try a "default" method on the current leaf.
if hasattr(candidate, "default"): if hasattr(candidate, 'default'):
defhandler = candidate.default defhandler = candidate.default
if getattr(defhandler, 'exposed', False): if getattr(defhandler, 'exposed', False):
# Insert any extra _cp_config from the default handler. # Insert any extra _cp_config from the default handler.
conf = getattr(defhandler, "_cp_config", {}) conf = getattr(defhandler, '_cp_config', {})
object_trail.insert( object_trail.insert(
i + 1, ["default", defhandler, conf, segleft]) i + 1, ['default', defhandler, conf, segleft])
request.config = set_conf() request.config = set_conf()
# See https://bitbucket.org/cherrypy/cherrypy/issue/613 # See https://github.com/cherrypy/cherrypy/issues/613
request.is_index = path.endswith("/") request.is_index = path.endswith('/')
return defhandler, fullpath[fullpath_len - segleft:-1] return defhandler, fullpath[fullpath_len - segleft:-1]
# Uncomment the next line to restrict positional params to # Uncomment the next line to restrict positional params to
@ -470,23 +471,23 @@ class MethodDispatcher(Dispatcher):
if resource: if resource:
# Set Allow header # Set Allow header
avail = [m for m in dir(resource) if m.isupper()] avail = [m for m in dir(resource) if m.isupper()]
if "GET" in avail and "HEAD" not in avail: if 'GET' in avail and 'HEAD' not in avail:
avail.append("HEAD") avail.append('HEAD')
avail.sort() avail.sort()
cherrypy.serving.response.headers['Allow'] = ", ".join(avail) cherrypy.serving.response.headers['Allow'] = ', '.join(avail)
# Find the subhandler # Find the subhandler
meth = request.method.upper() meth = request.method.upper()
func = getattr(resource, meth, None) func = getattr(resource, meth, None)
if func is None and meth == "HEAD": if func is None and meth == 'HEAD':
func = getattr(resource, "GET", None) func = getattr(resource, 'GET', None)
if func: if func:
# Grab any _cp_config on the subhandler. # Grab any _cp_config on the subhandler.
if hasattr(func, "_cp_config"): if hasattr(func, '_cp_config'):
request.config.update(func._cp_config) request.config.update(func._cp_config)
# Decode any leftover %2F in the virtual_path atoms. # Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace("%2F", "/") for x in vpath] vpath = [x.replace('%2F', '/') for x in vpath]
request.handler = LateParamPageHandler(func, *vpath) request.handler = LateParamPageHandler(func, *vpath)
else: else:
request.handler = cherrypy.HTTPError(405) request.handler = cherrypy.HTTPError(405)
@ -554,28 +555,28 @@ class RoutesDispatcher(object):
# Get config for the root object/path. # Get config for the root object/path.
request.config = base = cherrypy.config.copy() request.config = base = cherrypy.config.copy()
curpath = "" curpath = ''
def merge(nodeconf): def merge(nodeconf):
if 'tools.staticdir.dir' in nodeconf: if 'tools.staticdir.dir' in nodeconf:
nodeconf['tools.staticdir.section'] = curpath or "/" nodeconf['tools.staticdir.section'] = curpath or '/'
base.update(nodeconf) base.update(nodeconf)
app = request.app app = request.app
root = app.root root = app.root
if hasattr(root, "_cp_config"): if hasattr(root, '_cp_config'):
merge(root._cp_config) merge(root._cp_config)
if "/" in app.config: if '/' in app.config:
merge(app.config["/"]) merge(app.config['/'])
# Mix in values from app.config. # Mix in values from app.config.
atoms = [x for x in path_info.split("/") if x] atoms = [x for x in path_info.split('/') if x]
if atoms: if atoms:
last = atoms.pop() last = atoms.pop()
else: else:
last = None last = None
for atom in atoms: for atom in atoms:
curpath = "/".join((curpath, atom)) curpath = '/'.join((curpath, atom))
if curpath in app.config: if curpath in app.config:
merge(app.config[curpath]) merge(app.config[curpath])
@ -587,14 +588,14 @@ class RoutesDispatcher(object):
if isinstance(controller, classtype): if isinstance(controller, classtype):
controller = controller() controller = controller()
# Get config from the controller. # Get config from the controller.
if hasattr(controller, "_cp_config"): if hasattr(controller, '_cp_config'):
merge(controller._cp_config) merge(controller._cp_config)
action = result.get('action') action = result.get('action')
if action is not None: if action is not None:
handler = getattr(controller, action, None) handler = getattr(controller, action, None)
# Get config from the handler # Get config from the handler
if hasattr(handler, "_cp_config"): if hasattr(handler, '_cp_config'):
merge(handler._cp_config) merge(handler._cp_config)
else: else:
handler = controller handler = controller
@ -602,7 +603,7 @@ class RoutesDispatcher(object):
# Do the last path atom here so it can # Do the last path atom here so it can
# override the controller's _cp_config. # override the controller's _cp_config.
if last: if last:
curpath = "/".join((curpath, last)) curpath = '/'.join((curpath, last))
if curpath in app.config: if curpath in app.config:
merge(app.config[curpath]) merge(app.config[curpath])
@ -666,16 +667,16 @@ def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
domain = header('Host', '') domain = header('Host', '')
if use_x_forwarded_host: if use_x_forwarded_host:
domain = header("X-Forwarded-Host", domain) domain = header('X-Forwarded-Host', domain)
prefix = domains.get(domain, "") prefix = domains.get(domain, '')
if prefix: if prefix:
path_info = httputil.urljoin(prefix, path_info) path_info = httputil.urljoin(prefix, path_info)
result = next_dispatcher(path_info) result = next_dispatcher(path_info)
# Touch up staticdir config. See # Touch up staticdir config. See
# https://bitbucket.org/cherrypy/cherrypy/issue/614. # https://github.com/cherrypy/cherrypy/issues/614.
section = request.config.get('tools.staticdir.section') section = request.config.get('tools.staticdir.section')
if section: if section:
section = section[len(prefix):] section = section[len(prefix):]

View file

@ -29,8 +29,9 @@ user:
300 Multiple Choices Confirm with the user 300 Multiple Choices Confirm with the user
301 Moved Permanently Confirm with the user 301 Moved Permanently Confirm with the user
302 Found (Object moved temporarily) Confirm with the user 302 Found (Object moved temporarily) Confirm with the user
303 See Other GET the new URI--no confirmation 303 See Other GET the new URI; no confirmation
304 Not modified (for conditional GET only--POST should not raise this error) 304 Not modified for conditional GET only;
POST should not raise this error
305 Use Proxy Confirm with the user 305 Use Proxy Confirm with the user
307 Temporary Redirect Confirm with the user 307 Temporary Redirect Confirm with the user
===== ================================= =========== ===== ================================= ===========
@ -58,7 +59,8 @@ The 'error_page' config namespace can be used to provide custom HTML output for
expected responses (like 404 Not Found). Supply a filename from which the expected responses (like 404 Not Found). Supply a filename from which the
output will be read. The contents will be interpolated with the values output will be read. The contents will be interpolated with the values
%(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python %(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python
`string formatting <http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_. `string formatting
<http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_.
:: ::
@ -100,26 +102,37 @@ send an e-mail containing the error::
def handle_error(): def handle_error():
cherrypy.response.status = 500 cherrypy.response.status = 500
cherrypy.response.body = [ cherrypy.response.body = [
"<html><body>Sorry, an error occured</body></html>" "<html><body>Sorry, an error occurred</body></html>"
] ]
sendMail('error@domain.com', sendMail('error@domain.com',
'Error in your web app', 'Error in your web app',
_cperror.format_exc()) _cperror.format_exc())
@cherrypy.config(**{'request.error_response': handle_error})
class Root: class Root:
_cp_config = {'request.error_response': handle_error} pass
Note that you have to explicitly set Note that you have to explicitly set
:attr:`response.body <cherrypy._cprequest.Response.body>` :attr:`response.body <cherrypy._cprequest.Response.body>`
and not simply return an error message as a result. and not simply return an error message as a result.
""" """
from cgi import escape as _escape import io
import contextlib
from sys import exc_info as _exc_info from sys import exc_info as _exc_info
from traceback import format_exception as _format_exception from traceback import format_exception as _format_exception
from cherrypy._cpcompat import basestring, bytestr, iteritems, ntob from xml.sax import saxutils
from cherrypy._cpcompat import tonative, urljoin as _urljoin
import six
from six.moves import urllib
from more_itertools import always_iterable
import cherrypy
from cherrypy._cpcompat import escape_html
from cherrypy._cpcompat import ntob
from cherrypy._cpcompat import tonative
from cherrypy._helper import classproperty
from cherrypy.lib import httputil as _httputil from cherrypy.lib import httputil as _httputil
@ -129,12 +142,6 @@ class CherryPyException(Exception):
pass pass
class TimeoutError(CherryPyException):
"""Exception raised when Response.timed_out is detected."""
pass
class InternalRedirect(CherryPyException): class InternalRedirect(CherryPyException):
"""Exception raised to switch to the handler for a different URL. """Exception raised to switch to the handler for a different URL.
@ -145,20 +152,19 @@ class InternalRedirect(CherryPyException):
URL. URL.
""" """
def __init__(self, path, query_string=""): def __init__(self, path, query_string=''):
import cherrypy
self.request = cherrypy.serving.request self.request = cherrypy.serving.request
self.query_string = query_string self.query_string = query_string
if "?" in path: if '?' in path:
# Separate any params included in the path # Separate any params included in the path
path, self.query_string = path.split("?", 1) path, self.query_string = path.split('?', 1)
# Note that urljoin will "do the right thing" whether url is: # Note that urljoin will "do the right thing" whether url is:
# 1. a URL relative to root (e.g. "/dummy") # 1. a URL relative to root (e.g. "/dummy")
# 2. a URL relative to the current path # 2. a URL relative to the current path
# Note that any query string will be discarded. # Note that any query string will be discarded.
path = _urljoin(self.request.path_info, path) path = urllib.parse.urljoin(self.request.path_info, path)
# Set a 'path' member attribute so that code which traps this # Set a 'path' member attribute so that code which traps this
# error can have access to it. # error can have access to it.
@ -193,9 +199,6 @@ class HTTPRedirect(CherryPyException):
See :ref:`redirectingpost` for additional caveats. See :ref:`redirectingpost` for additional caveats.
""" """
status = None
"""The integer HTTP status code to emit."""
urls = None urls = None
"""The list of URL's to emit.""" """The list of URL's to emit."""
@ -203,41 +206,46 @@ class HTTPRedirect(CherryPyException):
"""The encoding when passed urls are not native strings""" """The encoding when passed urls are not native strings"""
def __init__(self, urls, status=None, encoding=None): def __init__(self, urls, status=None, encoding=None):
import cherrypy self.urls = abs_urls = [
request = cherrypy.serving.request
if isinstance(urls, basestring):
urls = [urls]
abs_urls = []
for url in urls:
url = tonative(url, encoding or self.encoding)
# Note that urljoin will "do the right thing" whether url is: # Note that urljoin will "do the right thing" whether url is:
# 1. a complete URL with host (e.g. "http://www.example.com/test") # 1. a complete URL with host (e.g. "http://www.example.com/test")
# 2. a URL relative to root (e.g. "/dummy") # 2. a URL relative to root (e.g. "/dummy")
# 3. a URL relative to the current path # 3. a URL relative to the current path
# Note that any query string in cherrypy.request is discarded. # Note that any query string in cherrypy.request is discarded.
url = _urljoin(cherrypy.url(), url) urllib.parse.urljoin(
abs_urls.append(url) cherrypy.url(),
self.urls = abs_urls tonative(url, encoding or self.encoding),
)
for url in always_iterable(urls)
]
# RFC 2616 indicates a 301 response code fits our goal; however, status = (
# browser support for 301 is quite messy. Do 302/303 instead. See int(status)
# http://www.alanflavell.org.uk/www/post-redirect.html if status is not None
if status is None: else self.default_status
if request.protocol >= (1, 1): )
status = 303 if not 300 <= status <= 399:
else: raise ValueError('status must be between 300 and 399.')
status = 302
else:
status = int(status)
if status < 300 or status > 399:
raise ValueError("status must be between 300 and 399.")
self.status = status
CherryPyException.__init__(self, abs_urls, status) CherryPyException.__init__(self, abs_urls, status)
@classproperty
def default_status(cls):
"""
The default redirect status for the request.
RFC 2616 indicates a 301 response code fits our goal; however,
browser support for 301 is quite messy. Use 302/303 instead. See
http://www.alanflavell.org.uk/www/post-redirect.html
"""
return 303 if cherrypy.serving.request.protocol >= (1, 1) else 302
@property
def status(self):
"""The integer HTTP status code to emit."""
_, status = self.args[:2]
return status
def set_response(self): def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent """Modify cherrypy.response status, headers, and body to represent
self. self.
@ -245,12 +253,11 @@ class HTTPRedirect(CherryPyException):
CherryPy uses this internally, but you can also use it to create an CherryPy uses this internally, but you can also use it to create an
HTTPRedirect object and set its output without *raising* the exception. HTTPRedirect object and set its output without *raising* the exception.
""" """
import cherrypy
response = cherrypy.serving.response response = cherrypy.serving.response
response.status = status = self.status response.status = status = self.status
if status in (300, 301, 302, 303, 307): if status in (300, 301, 302, 303, 307):
response.headers['Content-Type'] = "text/html;charset=utf-8" response.headers['Content-Type'] = 'text/html;charset=utf-8'
# "The ... URI SHOULD be given by the Location field # "The ... URI SHOULD be given by the Location field
# in the response." # in the response."
response.headers['Location'] = self.urls[0] response.headers['Location'] = self.urls[0]
@ -259,16 +266,18 @@ class HTTPRedirect(CherryPyException):
# SHOULD contain a short hypertext note with a hyperlink to the # SHOULD contain a short hypertext note with a hyperlink to the
# new URI(s)." # new URI(s)."
msg = { msg = {
300: "This resource can be found at ", 300: 'This resource can be found at ',
301: "This resource has permanently moved to ", 301: 'This resource has permanently moved to ',
302: "This resource resides temporarily at ", 302: 'This resource resides temporarily at ',
303: "This resource can be found at ", 303: 'This resource can be found at ',
307: "This resource has moved temporarily to ", 307: 'This resource has moved temporarily to ',
}[status] }[status]
msg += '<a href=%s>%s</a>.' msg += '<a href=%s>%s</a>.'
from xml.sax import saxutils msgs = [
msgs = [msg % (saxutils.quoteattr(u), u) for u in self.urls] msg % (saxutils.quoteattr(u), escape_html(u))
response.body = ntob("<br />\n".join(msgs), 'utf-8') for u in self.urls
]
response.body = ntob('<br />\n'.join(msgs), 'utf-8')
# Previous code may have set C-L, so we have to reset it # Previous code may have set C-L, so we have to reset it
# (allow finalize to set it). # (allow finalize to set it).
response.headers.pop('Content-Length', None) response.headers.pop('Content-Length', None)
@ -293,12 +302,12 @@ class HTTPRedirect(CherryPyException):
elif status == 305: elif status == 305:
# Use Proxy. # Use Proxy.
# self.urls[0] should be the URI of the proxy. # self.urls[0] should be the URI of the proxy.
response.headers['Location'] = self.urls[0] response.headers['Location'] = ntob(self.urls[0], 'utf-8')
response.body = None response.body = None
# Previous code may have set C-L, so we have to reset it. # Previous code may have set C-L, so we have to reset it.
response.headers.pop('Content-Length', None) response.headers.pop('Content-Length', None)
else: else:
raise ValueError("The %s status code is unknown." % status) raise ValueError('The %s status code is unknown.' % status)
def __call__(self): def __call__(self):
"""Use this exception as a request.handler (raise self).""" """Use this exception as a request.handler (raise self)."""
@ -307,16 +316,14 @@ class HTTPRedirect(CherryPyException):
def clean_headers(status): def clean_headers(status):
"""Remove any headers which should not apply to an error response.""" """Remove any headers which should not apply to an error response."""
import cherrypy
response = cherrypy.serving.response response = cherrypy.serving.response
# Remove headers which applied to the original content, # Remove headers which applied to the original content,
# but do not apply to the error page. # but do not apply to the error page.
respheaders = response.headers respheaders = response.headers
for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After", for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After',
"Vary", "Content-Encoding", "Content-Length", "Expires", 'Vary', 'Content-Encoding', 'Content-Length', 'Expires',
"Content-Location", "Content-MD5", "Last-Modified"]: 'Content-Location', 'Content-MD5', 'Last-Modified']:
if key in respheaders: if key in respheaders:
del respheaders[key] del respheaders[key]
@ -327,8 +334,8 @@ def clean_headers(status):
# specifies the current length of the selected resource. # specifies the current length of the selected resource.
# A response with status code 206 (Partial Content) MUST NOT # A response with status code 206 (Partial Content) MUST NOT
# include a Content-Range field with a byte-range- resp-spec of "*". # include a Content-Range field with a byte-range- resp-spec of "*".
if "Content-Range" in respheaders: if 'Content-Range' in respheaders:
del respheaders["Content-Range"] del respheaders['Content-Range']
class HTTPError(CherryPyException): class HTTPError(CherryPyException):
@ -368,7 +375,7 @@ class HTTPError(CherryPyException):
raise self.__class__(500, _exc_info()[1].args[0]) raise self.__class__(500, _exc_info()[1].args[0])
if self.code < 400 or self.code > 599: if self.code < 400 or self.code > 599:
raise ValueError("status must be between 400 and 599.") raise ValueError('status must be between 400 and 599.')
# See http://www.python.org/dev/peps/pep-0352/ # See http://www.python.org/dev/peps/pep-0352/
# self.message = message # self.message = message
@ -382,8 +389,6 @@ class HTTPError(CherryPyException):
CherryPy uses this internally, but you can also use it to create an CherryPy uses this internally, but you can also use it to create an
HTTPError object and set its output without *raising* the exception. HTTPError object and set its output without *raising* the exception.
""" """
import cherrypy
response = cherrypy.serving.response response = cherrypy.serving.response
clean_headers(self.code) clean_headers(self.code)
@ -410,6 +415,15 @@ class HTTPError(CherryPyException):
"""Use this exception as a request.handler (raise self).""" """Use this exception as a request.handler (raise self)."""
raise self raise self
@classmethod
@contextlib.contextmanager
def handle(cls, exception, status=500, message=''):
"""Translate exception into an HTTPError."""
try:
yield
except exception as exc:
raise cls(status, message or str(exc))
class NotFound(HTTPError): class NotFound(HTTPError):
@ -421,7 +435,6 @@ class NotFound(HTTPError):
def __init__(self, path=None): def __init__(self, path=None):
if path is None: if path is None:
import cherrypy
request = cherrypy.serving.request request = cherrypy.serving.request
path = request.script_name + request.path_info path = request.script_name + request.path_info
self.args = (path,) self.args = (path,)
@ -467,8 +480,6 @@ def get_error_page(status, **kwargs):
status should be an int or a str. status should be an int or a str.
kwargs will be interpolated into the page template. kwargs will be interpolated into the page template.
""" """
import cherrypy
try: try:
code, reason, message = _httputil.valid_status(status) code, reason, message = _httputil.valid_status(status)
except ValueError: except ValueError:
@ -477,7 +488,7 @@ def get_error_page(status, **kwargs):
# We can't use setdefault here, because some # We can't use setdefault here, because some
# callers send None for kwarg values. # callers send None for kwarg values.
if kwargs.get('status') is None: if kwargs.get('status') is None:
kwargs['status'] = "%s %s" % (code, reason) kwargs['status'] = '%s %s' % (code, reason)
if kwargs.get('message') is None: if kwargs.get('message') is None:
kwargs['message'] = message kwargs['message'] = message
if kwargs.get('traceback') is None: if kwargs.get('traceback') is None:
@ -485,11 +496,11 @@ def get_error_page(status, **kwargs):
if kwargs.get('version') is None: if kwargs.get('version') is None:
kwargs['version'] = cherrypy.__version__ kwargs['version'] = cherrypy.__version__
for k, v in iteritems(kwargs): for k, v in six.iteritems(kwargs):
if v is None: if v is None:
kwargs[k] = "" kwargs[k] = ''
else: else:
kwargs[k] = _escape(kwargs[k]) kwargs[k] = escape_html(kwargs[k])
# Use a custom template or callable for the error page? # Use a custom template or callable for the error page?
pages = cherrypy.serving.request.error_page pages = cherrypy.serving.request.error_page
@ -509,33 +520,33 @@ def get_error_page(status, **kwargs):
if cherrypy.lib.is_iterator(result): if cherrypy.lib.is_iterator(result):
from cherrypy.lib.encoding import UTF8StreamEncoder from cherrypy.lib.encoding import UTF8StreamEncoder
return UTF8StreamEncoder(result) return UTF8StreamEncoder(result)
elif isinstance(result, cherrypy._cpcompat.unicodestr): elif isinstance(result, six.text_type):
return result.encode('utf-8') return result.encode('utf-8')
else: else:
if not isinstance(result, cherrypy._cpcompat.bytestr): if not isinstance(result, bytes):
raise ValueError('error page function did not ' raise ValueError(
'return a bytestring, unicodestring or an ' 'error page function did not '
'return a bytestring, six.text_type or an '
'iterator - returned object of type %s.' 'iterator - returned object of type %s.'
% (type(result).__name__)) % (type(result).__name__))
return result return result
else: else:
# Load the template from this path. # Load the template from this path.
template = tonative(open(error_page, 'rb').read()) template = io.open(error_page, newline='').read()
except: except Exception:
e = _format_exception(*_exc_info())[-1] e = _format_exception(*_exc_info())[-1]
m = kwargs['message'] m = kwargs['message']
if m: if m:
m += "<br />" m += '<br />'
m += "In addition, the custom error page failed:\n<br />%s" % e m += 'In addition, the custom error page failed:\n<br />%s' % e
kwargs['message'] = m kwargs['message'] = m
response = cherrypy.serving.response response = cherrypy.serving.response
response.headers['Content-Type'] = "text/html;charset=utf-8" response.headers['Content-Type'] = 'text/html;charset=utf-8'
result = template % kwargs result = template % kwargs
return result.encode('utf-8') return result.encode('utf-8')
_ie_friendly_error_sizes = { _ie_friendly_error_sizes = {
400: 512, 403: 256, 404: 512, 405: 256, 400: 512, 403: 256, 404: 512, 405: 256,
406: 512, 408: 512, 409: 512, 410: 256, 406: 512, 408: 512, 409: 512, 410: 256,
@ -544,7 +555,6 @@ _ie_friendly_error_sizes = {
def _be_ie_unfriendly(status): def _be_ie_unfriendly(status):
import cherrypy
response = cherrypy.serving.response response = cherrypy.serving.response
# For some statuses, Internet Explorer 5+ shows "friendly error # For some statuses, Internet Explorer 5+ shows "friendly error
@ -558,11 +568,11 @@ def _be_ie_unfriendly(status):
# Since we are issuing an HTTP error status, we assume that # Since we are issuing an HTTP error status, we assume that
# the entity is short, and we should just collapse it. # the entity is short, and we should just collapse it.
content = response.collapse_body() content = response.collapse_body()
l = len(content) content_length = len(content)
if l and l < s: if content_length and content_length < s:
# IN ADDITION: the response must be written to IE # IN ADDITION: the response must be written to IE
# in one chunk or it will still get replaced! Bah. # in one chunk or it will still get replaced! Bah.
content = content + (ntob(" ") * (s - l)) content = content + (b' ' * (s - content_length))
response.body = content response.body = content
response.headers['Content-Length'] = str(len(content)) response.headers['Content-Length'] = str(len(content))
@ -573,9 +583,9 @@ def format_exc(exc=None):
if exc is None: if exc is None:
exc = _exc_info() exc = _exc_info()
if exc == (None, None, None): if exc == (None, None, None):
return "" return ''
import traceback import traceback
return "".join(traceback.format_exception(*exc)) return ''.join(traceback.format_exception(*exc))
finally: finally:
del exc del exc
@ -597,13 +607,13 @@ def bare_error(extrabody=None):
# it cannot be allowed to fail. Therefore, don't add to it! # it cannot be allowed to fail. Therefore, don't add to it!
# In particular, don't call any other CP functions. # In particular, don't call any other CP functions.
body = ntob("Unrecoverable error in the server.") body = b'Unrecoverable error in the server.'
if extrabody is not None: if extrabody is not None:
if not isinstance(extrabody, bytestr): if not isinstance(extrabody, bytes):
extrabody = extrabody.encode('utf-8') extrabody = extrabody.encode('utf-8')
body += ntob("\n") + extrabody body += b'\n' + extrabody
return (ntob("500 Internal Server Error"), return (b'500 Internal Server Error',
[(ntob('Content-Type'), ntob('text/plain')), [(b'Content-Type', b'text/plain'),
(ntob('Content-Length'), ntob(str(len(body)), 'ISO-8859-1'))], (b'Content-Length', ntob(str(len(body)), 'ISO-8859-1'))],
[body]) [body])

View file

@ -59,7 +59,8 @@ tracebacks, if enabled).
If you are logging the access log and error log to the same source, then there If you are logging the access log and error log to the same source, then there
is a possibility that a specially crafted error message may replicate an access is a possibility that a specially crafted error message may replicate an access
log message as described in CWE-117. In this case it is the application log message as described in CWE-117. In this case it is the application
developer's responsibility to manually escape data before using CherryPy's log() developer's responsibility to manually escape data before
using CherryPy's log()
functionality, or they may create an application that is vulnerable to CWE-117. functionality, or they may create an application that is vulnerable to CWE-117.
This would be achieved by using a custom handler escape any special characters, This would be achieved by using a custom handler escape any special characters,
and attached as described below. and attached as described below.
@ -109,15 +110,18 @@ the "log.error_file" config entry, for example).
import datetime import datetime
import logging import logging
# Silence the no-handlers "warning" (stderr write!) in stdlib logging
logging.Logger.manager.emittedNoHandlerWarning = 1
logfmt = logging.Formatter("%(message)s")
import os import os
import sys import sys
import six
import cherrypy import cherrypy
from cherrypy import _cperror from cherrypy import _cperror
from cherrypy._cpcompat import ntob, py3k
# Silence the no-handlers "warning" (stderr write!) in stdlib logging
logging.Logger.manager.emittedNoHandlerWarning = 1
logfmt = logging.Formatter('%(message)s')
class NullHandler(logging.Handler): class NullHandler(logging.Handler):
@ -151,12 +155,11 @@ class LogManager(object):
access_log = None access_log = None
"""The actual :class:`logging.Logger` instance for access messages.""" """The actual :class:`logging.Logger` instance for access messages."""
if py3k: access_log_format = (
access_log_format = \ '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"' if six.PY3 else
else: '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
access_log_format = \ )
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
logger_root = None logger_root = None
"""The "top-level" logger name. """The "top-level" logger name.
@ -169,17 +172,17 @@ class LogManager(object):
cherrypy.access.<appid> cherrypy.access.<appid>
""" """
def __init__(self, appid=None, logger_root="cherrypy"): def __init__(self, appid=None, logger_root='cherrypy'):
self.logger_root = logger_root self.logger_root = logger_root
self.appid = appid self.appid = appid
if appid is None: if appid is None:
self.error_log = logging.getLogger("%s.error" % logger_root) self.error_log = logging.getLogger('%s.error' % logger_root)
self.access_log = logging.getLogger("%s.access" % logger_root) self.access_log = logging.getLogger('%s.access' % logger_root)
else: else:
self.error_log = logging.getLogger( self.error_log = logging.getLogger(
"%s.error.%s" % (logger_root, appid)) '%s.error.%s' % (logger_root, appid))
self.access_log = logging.getLogger( self.access_log = logging.getLogger(
"%s.access.%s" % (logger_root, appid)) '%s.access.%s' % (logger_root, appid))
self.error_log.setLevel(logging.INFO) self.error_log.setLevel(logging.INFO)
self.access_log.setLevel(logging.INFO) self.access_log.setLevel(logging.INFO)
@ -213,7 +216,11 @@ class LogManager(object):
if traceback: if traceback:
exc_info = _cperror._exc_info() exc_info = _cperror._exc_info()
self.error_log.log(severity, ' '.join((self.time(), context, msg)), exc_info=exc_info) self.error_log.log(
severity,
' '.join((self.time(), context, msg)),
exc_info=exc_info,
)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
"""An alias for ``error``.""" """An alias for ``error``."""
@ -223,7 +230,8 @@ class LogManager(object):
"""Write to the access log (in Apache/NCSA Combined Log format). """Write to the access log (in Apache/NCSA Combined Log format).
See the See the
`apache documentation <http://httpd.apache.org/docs/current/logs.html#combined>`_ `apache documentation
<http://httpd.apache.org/docs/current/logs.html#combined>`_
for format details. for format details.
CherryPy calls this automatically for you. Note there are no arguments; CherryPy calls this automatically for you. Note there are no arguments;
@ -243,24 +251,26 @@ class LogManager(object):
outheaders = response.headers outheaders = response.headers
inheaders = request.headers inheaders = request.headers
if response.output_status is None: if response.output_status is None:
status = "-" status = '-'
else: else:
status = response.output_status.split(ntob(" "), 1)[0] status = response.output_status.split(b' ', 1)[0]
if py3k: if six.PY3:
status = status.decode('ISO-8859-1') status = status.decode('ISO-8859-1')
atoms = {'h': remote.name or remote.ip, atoms = {'h': remote.name or remote.ip,
'l': '-', 'l': '-',
'u': getattr(request, "login", None) or "-", 'u': getattr(request, 'login', None) or '-',
't': self.time(), 't': self.time(),
'r': request.request_line, 'r': request.request_line,
's': status, 's': status,
'b': dict.get(outheaders, 'Content-Length', '') or "-", 'b': dict.get(outheaders, 'Content-Length', '') or '-',
'f': dict.get(inheaders, 'Referer', ''), 'f': dict.get(inheaders, 'Referer', ''),
'a': dict.get(inheaders, 'User-Agent', ''), 'a': dict.get(inheaders, 'User-Agent', ''),
'o': dict.get(inheaders, 'Host', '-'), 'o': dict.get(inheaders, 'Host', '-'),
'i': request.unique_id,
'z': LazyRfc3339UtcTime(),
} }
if py3k: if six.PY3:
for k, v in atoms.items(): for k, v in atoms.items():
if not isinstance(v, str): if not isinstance(v, str):
v = str(v) v = str(v)
@ -280,11 +290,11 @@ class LogManager(object):
try: try:
self.access_log.log( self.access_log.log(
logging.INFO, self.access_log_format.format(**atoms)) logging.INFO, self.access_log_format.format(**atoms))
except: except Exception:
self(traceback=True) self(traceback=True)
else: else:
for k, v in atoms.items(): for k, v in atoms.items():
if isinstance(v, unicode): if isinstance(v, six.text_type):
v = v.encode('utf8') v = v.encode('utf8')
elif not isinstance(v, str): elif not isinstance(v, str):
v = str(v) v = str(v)
@ -297,7 +307,7 @@ class LogManager(object):
try: try:
self.access_log.log( self.access_log.log(
logging.INFO, self.access_log_format % atoms) logging.INFO, self.access_log_format % atoms)
except: except Exception:
self(traceback=True) self(traceback=True)
def time(self): def time(self):
@ -311,48 +321,49 @@ class LogManager(object):
def _get_builtin_handler(self, log, key): def _get_builtin_handler(self, log, key):
for h in log.handlers: for h in log.handlers:
if getattr(h, "_cpbuiltin", None) == key: if getattr(h, '_cpbuiltin', None) == key:
return h return h
# ------------------------- Screen handlers ------------------------- # # ------------------------- Screen handlers ------------------------- #
def _set_screen_handler(self, log, enable, stream=None): def _set_screen_handler(self, log, enable, stream=None):
h = self._get_builtin_handler(log, "screen") h = self._get_builtin_handler(log, 'screen')
if enable: if enable:
if not h: if not h:
if stream is None: if stream is None:
stream = sys.stderr stream = sys.stderr
h = logging.StreamHandler(stream) h = logging.StreamHandler(stream)
h.setFormatter(logfmt) h.setFormatter(logfmt)
h._cpbuiltin = "screen" h._cpbuiltin = 'screen'
log.addHandler(h) log.addHandler(h)
elif h: elif h:
log.handlers.remove(h) log.handlers.remove(h)
def _get_screen(self): @property
h = self._get_builtin_handler def screen(self):
has_h = h(self.error_log, "screen") or h(self.access_log, "screen") """Turn stderr/stdout logging on or off.
return bool(has_h)
def _set_screen(self, newvalue):
self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
screen = property(_get_screen, _set_screen,
doc="""Turn stderr/stdout logging on or off.
If you set this to True, it'll add the appropriate StreamHandler for If you set this to True, it'll add the appropriate StreamHandler for
you. If you set it to False, it will remove the handler. you. If you set it to False, it will remove the handler.
""") """
h = self._get_builtin_handler
has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen')
return bool(has_h)
@screen.setter
def screen(self, newvalue):
self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
# -------------------------- File handlers -------------------------- # # -------------------------- File handlers -------------------------- #
def _add_builtin_file_handler(self, log, fname): def _add_builtin_file_handler(self, log, fname):
h = logging.FileHandler(fname) h = logging.FileHandler(fname)
h.setFormatter(logfmt) h.setFormatter(logfmt)
h._cpbuiltin = "file" h._cpbuiltin = 'file'
log.addHandler(h) log.addHandler(h)
def _set_file_handler(self, log, filename): def _set_file_handler(self, log, filename):
h = self._get_builtin_handler(log, "file") h = self._get_builtin_handler(log, 'file')
if filename: if filename:
if h: if h:
if h.baseFilename != os.path.abspath(filename): if h.baseFilename != os.path.abspath(filename):
@ -366,62 +377,65 @@ class LogManager(object):
h.close() h.close()
log.handlers.remove(h) log.handlers.remove(h)
def _get_error_file(self): @property
h = self._get_builtin_handler(self.error_log, "file") def error_file(self):
"""The filename for self.error_log.
If you set this to a string, it'll add the appropriate FileHandler for
you. If you set it to ``None`` or ``''``, it will remove the handler.
"""
h = self._get_builtin_handler(self.error_log, 'file')
if h: if h:
return h.baseFilename return h.baseFilename
return '' return ''
def _set_error_file(self, newvalue): @error_file.setter
def error_file(self, newvalue):
self._set_file_handler(self.error_log, newvalue) self._set_file_handler(self.error_log, newvalue)
error_file = property(_get_error_file, _set_error_file,
doc="""The filename for self.error_log. @property
def access_file(self):
"""The filename for self.access_log.
If you set this to a string, it'll add the appropriate FileHandler for If you set this to a string, it'll add the appropriate FileHandler for
you. If you set it to ``None`` or ``''``, it will remove the handler. you. If you set it to ``None`` or ``''``, it will remove the handler.
""") """
h = self._get_builtin_handler(self.access_log, 'file')
def _get_access_file(self):
h = self._get_builtin_handler(self.access_log, "file")
if h: if h:
return h.baseFilename return h.baseFilename
return '' return ''
def _set_access_file(self, newvalue): @access_file.setter
def access_file(self, newvalue):
self._set_file_handler(self.access_log, newvalue) self._set_file_handler(self.access_log, newvalue)
access_file = property(_get_access_file, _set_access_file,
doc="""The filename for self.access_log.
If you set this to a string, it'll add the appropriate FileHandler for
you. If you set it to ``None`` or ``''``, it will remove the handler.
""")
# ------------------------- WSGI handlers ------------------------- # # ------------------------- WSGI handlers ------------------------- #
def _set_wsgi_handler(self, log, enable): def _set_wsgi_handler(self, log, enable):
h = self._get_builtin_handler(log, "wsgi") h = self._get_builtin_handler(log, 'wsgi')
if enable: if enable:
if not h: if not h:
h = WSGIErrorHandler() h = WSGIErrorHandler()
h.setFormatter(logfmt) h.setFormatter(logfmt)
h._cpbuiltin = "wsgi" h._cpbuiltin = 'wsgi'
log.addHandler(h) log.addHandler(h)
elif h: elif h:
log.handlers.remove(h) log.handlers.remove(h)
def _get_wsgi(self): @property
return bool(self._get_builtin_handler(self.error_log, "wsgi")) def wsgi(self):
"""Write errors to wsgi.errors.
def _set_wsgi(self, newvalue):
self._set_wsgi_handler(self.error_log, newvalue)
wsgi = property(_get_wsgi, _set_wsgi,
doc="""Write errors to wsgi.errors.
If you set this to True, it'll add the appropriate If you set this to True, it'll add the appropriate
:class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you :class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you
(which writes errors to ``wsgi.errors``). (which writes errors to ``wsgi.errors``).
If you set it to False, it will remove the handler. If you set it to False, it will remove the handler.
""") """
return bool(self._get_builtin_handler(self.error_log, 'wsgi'))
@wsgi.setter
def wsgi(self, newvalue):
self._set_wsgi_handler(self.error_log, newvalue)
class WSGIErrorHandler(logging.Handler): class WSGIErrorHandler(logging.Handler):
@ -446,16 +460,23 @@ class WSGIErrorHandler(logging.Handler):
else: else:
try: try:
msg = self.format(record) msg = self.format(record)
fs = "%s\n" fs = '%s\n'
import types import types
# if no unicode support... # if no unicode support...
if not hasattr(types, "UnicodeType"): if not hasattr(types, 'UnicodeType'):
stream.write(fs % msg) stream.write(fs % msg)
else: else:
try: try:
stream.write(fs % msg) stream.write(fs % msg)
except UnicodeError: except UnicodeError:
stream.write(fs % msg.encode("UTF-8")) stream.write(fs % msg.encode('UTF-8'))
self.flush() self.flush()
except: except Exception:
self.handleError(record) self.handleError(record)
class LazyRfc3339UtcTime(object):
def __str__(self):
"""Return now() in RFC3339 UTC Format."""
now = datetime.datetime.now()
return now.isoformat('T') + 'Z'

View file

@ -55,11 +55,17 @@ resides in the global site-package this won't be needed.
Then restart apache2 and access http://127.0.0.1:8080 Then restart apache2 and access http://127.0.0.1:8080
""" """
import io
import logging import logging
import os
import re
import sys import sys
import six
from more_itertools import always_iterable
import cherrypy import cherrypy
from cherrypy._cpcompat import BytesIO, copyitems, ntob
from cherrypy._cperror import format_exc, bare_error from cherrypy._cperror import format_exc, bare_error
from cherrypy.lib import httputil from cherrypy.lib import httputil
@ -85,18 +91,19 @@ def setup(req):
func() func()
cherrypy.config.update({'log.screen': False, cherrypy.config.update({'log.screen': False,
"tools.ignore_headers.on": True, 'tools.ignore_headers.on': True,
"tools.ignore_headers.headers": ['Range'], 'tools.ignore_headers.headers': ['Range'],
}) })
engine = cherrypy.engine engine = cherrypy.engine
if hasattr(engine, "signal_handler"): if hasattr(engine, 'signal_handler'):
engine.signal_handler.unsubscribe() engine.signal_handler.unsubscribe()
if hasattr(engine, "console_control_handler"): if hasattr(engine, 'console_control_handler'):
engine.console_control_handler.unsubscribe() engine.console_control_handler.unsubscribe()
engine.autoreload.unsubscribe() engine.autoreload.unsubscribe()
cherrypy.server.unsubscribe() cherrypy.server.unsubscribe()
@engine.subscribe('log')
def _log(msg, level): def _log(msg, level):
newlevel = apache.APLOG_ERR newlevel = apache.APLOG_ERR
if logging.DEBUG >= level: if logging.DEBUG >= level:
@ -109,7 +116,6 @@ def setup(req):
# http://www.modpython.org/pipermail/mod_python/2003-October/014291.html # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html
# Also, "When server is not specified...LogLevel does not apply..." # Also, "When server is not specified...LogLevel does not apply..."
apache.log_error(msg, newlevel, req.server) apache.log_error(msg, newlevel, req.server)
engine.subscribe('log', _log)
engine.start() engine.start()
@ -146,10 +152,10 @@ def handler(req):
# Obtain a Request object from CherryPy # Obtain a Request object from CherryPy
local = req.connection.local_addr local = req.connection.local_addr
local = httputil.Host( local = httputil.Host(
local[0], local[1], req.connection.local_host or "") local[0], local[1], req.connection.local_host or '')
remote = req.connection.remote_addr remote = req.connection.remote_addr
remote = httputil.Host( remote = httputil.Host(
remote[0], remote[1], req.connection.remote_host or "") remote[0], remote[1], req.connection.remote_host or '')
scheme = req.parsed_uri[0] or 'http' scheme = req.parsed_uri[0] or 'http'
req.get_basic_auth_pw() req.get_basic_auth_pw()
@ -162,7 +168,9 @@ def handler(req):
except AttributeError: except AttributeError:
bad_value = ("You must provide a PythonOption '%s', " bad_value = ("You must provide a PythonOption '%s', "
"either 'on' or 'off', when running a version " "either 'on' or 'off', when running a version "
"of mod_python < 3.1") 'of mod_python < 3.1')
options = req.get_options()
threaded = options.get('multithread', '').lower() threaded = options.get('multithread', '').lower()
if threaded == 'on': if threaded == 'on':
@ -170,7 +178,7 @@ def handler(req):
elif threaded == 'off': elif threaded == 'off':
threaded = False threaded = False
else: else:
raise ValueError(bad_value % "multithread") raise ValueError(bad_value % 'multithread')
forked = options.get('multiprocess', '').lower() forked = options.get('multiprocess', '').lower()
if forked == 'on': if forked == 'on':
@ -178,18 +186,18 @@ def handler(req):
elif forked == 'off': elif forked == 'off':
forked = False forked = False
else: else:
raise ValueError(bad_value % "multiprocess") raise ValueError(bad_value % 'multiprocess')
sn = cherrypy.tree.script_name(req.uri or "/") sn = cherrypy.tree.script_name(req.uri or '/')
if sn is None: if sn is None:
send_response(req, '404 Not Found', [], '') send_response(req, '404 Not Found', [], '')
else: else:
app = cherrypy.tree.apps[sn] app = cherrypy.tree.apps[sn]
method = req.method method = req.method
path = req.uri path = req.uri
qs = req.args or "" qs = req.args or ''
reqproto = req.protocol reqproto = req.protocol
headers = copyitems(req.headers_in) headers = list(six.iteritems(req.headers_in))
rfile = _ReadOnlyRequest(req) rfile = _ReadOnlyRequest(req)
prev = None prev = None
@ -197,7 +205,7 @@ def handler(req):
redirections = [] redirections = []
while True: while True:
request, response = app.get_serving(local, remote, scheme, request, response = app.get_serving(local, remote, scheme,
"HTTP/1.1") 'HTTP/1.1')
request.login = req.user request.login = req.user
request.multithread = bool(threaded) request.multithread = bool(threaded)
request.multiprocess = bool(forked) request.multiprocess = bool(forked)
@ -216,27 +224,27 @@ def handler(req):
if not recursive: if not recursive:
if ir.path in redirections: if ir.path in redirections:
raise RuntimeError( raise RuntimeError(
"InternalRedirector visited the same URL " 'InternalRedirector visited the same URL '
"twice: %r" % ir.path) 'twice: %r' % ir.path)
else: else:
# Add the *previous* path_info + qs to # Add the *previous* path_info + qs to
# redirections. # redirections.
if qs: if qs:
qs = "?" + qs qs = '?' + qs
redirections.append(sn + path + qs) redirections.append(sn + path + qs)
# Munge environment and try again. # Munge environment and try again.
method = "GET" method = 'GET'
path = ir.path path = ir.path
qs = ir.query_string qs = ir.query_string
rfile = BytesIO() rfile = io.BytesIO()
send_response( send_response(
req, response.output_status, response.header_list, req, response.output_status, response.header_list,
response.body, response.stream) response.body, response.stream)
finally: finally:
app.release_serving() app.release_serving()
except: except Exception:
tb = format_exc() tb = format_exc()
cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR) cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR)
s, h, b = bare_error() s, h, b = bare_error()
@ -249,7 +257,7 @@ def send_response(req, status, headers, body, stream=False):
req.status = int(status[:3]) req.status = int(status[:3])
# Set response headers # Set response headers
req.content_type = "text/plain" req.content_type = 'text/plain'
for header, value in headers: for header, value in headers:
if header.lower() == 'content-type': if header.lower() == 'content-type':
req.content_type = value req.content_type = value
@ -261,16 +269,11 @@ def send_response(req, status, headers, body, stream=False):
req.flush() req.flush()
# Set response body # Set response body
if isinstance(body, basestring): for seg in always_iterable(body):
req.write(body) req.write(seg)
else:
for seg in body:
req.write(seg)
# --------------- Startup tools for CherryPy + mod_python --------------- # # --------------- Startup tools for CherryPy + mod_python --------------- #
import os
import re
try: try:
import subprocess import subprocess
@ -285,13 +288,13 @@ except ImportError:
return pipeout return pipeout
def read_process(cmd, args=""): def read_process(cmd, args=''):
fullcmd = "%s %s" % (cmd, args) fullcmd = '%s %s' % (cmd, args)
pipeout = popen(fullcmd) pipeout = popen(fullcmd)
try: try:
firstline = pipeout.readline() firstline = pipeout.readline()
cmd_not_found = re.search( cmd_not_found = re.search(
ntob("(not recognized|No such file|not found)"), b'(not recognized|No such file|not found)',
firstline, firstline,
re.IGNORECASE re.IGNORECASE
) )
@ -320,8 +323,8 @@ LoadModule python_module modules/mod_python.so
</Location> </Location>
""" """
def __init__(self, loc="/", port=80, opts=None, apache_path="apache", def __init__(self, loc='/', port=80, opts=None, apache_path='apache',
handler="cherrypy._cpmodpy::handler"): handler='cherrypy._cpmodpy::handler'):
self.loc = loc self.loc = loc
self.port = port self.port = port
self.opts = opts self.opts = opts
@ -329,25 +332,25 @@ LoadModule python_module modules/mod_python.so
self.handler = handler self.handler = handler
def start(self): def start(self):
opts = "".join([" PythonOption %s %s\n" % (k, v) opts = ''.join([' PythonOption %s %s\n' % (k, v)
for k, v in self.opts]) for k, v in self.opts])
conf_data = self.template % {"port": self.port, conf_data = self.template % {'port': self.port,
"loc": self.loc, 'loc': self.loc,
"opts": opts, 'opts': opts,
"handler": self.handler, 'handler': self.handler,
} }
mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf") mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf')
f = open(mpconf, 'wb') f = open(mpconf, 'wb')
try: try:
f.write(conf_data) f.write(conf_data)
finally: finally:
f.close() f.close()
response = read_process(self.apache_path, "-k start -f %s" % mpconf) response = read_process(self.apache_path, '-k start -f %s' % mpconf)
self.ready = True self.ready = True
return response return response
def stop(self): def stop(self):
os.popen("apache -k stop") os.popen('apache -k stop')
self.ready = False self.ready = False

View file

@ -2,37 +2,45 @@
import logging import logging
import sys import sys
import io
import cheroot.server
import cherrypy import cherrypy
from cherrypy._cpcompat import BytesIO
from cherrypy._cperror import format_exc, bare_error from cherrypy._cperror import format_exc, bare_error
from cherrypy.lib import httputil from cherrypy.lib import httputil
from cherrypy import wsgiserver from ._cpcompat import tonative
class NativeGateway(wsgiserver.Gateway): class NativeGateway(cheroot.server.Gateway):
"""Native gateway implementation allowing to bypass WSGI."""
recursive = False recursive = False
def respond(self): def respond(self):
"""Obtain response from CherryPy machinery and then send it."""
req = self.req req = self.req
try: try:
# Obtain a Request object from CherryPy # Obtain a Request object from CherryPy
local = req.server.bind_addr local = req.server.bind_addr # FIXME: handle UNIX sockets
local = httputil.Host(local[0], local[1], "") local = tonative(local[0]), local[1]
remote = req.conn.remote_addr, req.conn.remote_port local = httputil.Host(local[0], local[1], '')
remote = httputil.Host(remote[0], remote[1], "") remote = tonative(req.conn.remote_addr), req.conn.remote_port
remote = httputil.Host(remote[0], remote[1], '')
scheme = req.scheme scheme = tonative(req.scheme)
sn = cherrypy.tree.script_name(req.uri or "/") sn = cherrypy.tree.script_name(tonative(req.uri or '/'))
if sn is None: if sn is None:
self.send_response('404 Not Found', [], ['']) self.send_response('404 Not Found', [], [''])
else: else:
app = cherrypy.tree.apps[sn] app = cherrypy.tree.apps[sn]
method = req.method method = tonative(req.method)
path = req.path path = tonative(req.path)
qs = req.qs or "" qs = tonative(req.qs or '')
headers = req.inheaders.items() headers = (
(tonative(h), tonative(v))
for h, v in req.inheaders.items()
)
rfile = req.rfile rfile = req.rfile
prev = None prev = None
@ -40,7 +48,7 @@ class NativeGateway(wsgiserver.Gateway):
redirections = [] redirections = []
while True: while True:
request, response = app.get_serving( request, response = app.get_serving(
local, remote, scheme, "HTTP/1.1") local, remote, scheme, 'HTTP/1.1')
request.multithread = True request.multithread = True
request.multiprocess = False request.multiprocess = False
request.app = app request.app = app
@ -49,8 +57,11 @@ class NativeGateway(wsgiserver.Gateway):
# Run the CherryPy Request object and obtain the # Run the CherryPy Request object and obtain the
# response # response
try: try:
request.run(method, path, qs, request.run(
req.request_protocol, headers, rfile) method, path, qs,
tonative(req.request_protocol),
headers, rfile,
)
break break
except cherrypy.InternalRedirect: except cherrypy.InternalRedirect:
ir = sys.exc_info()[1] ir = sys.exc_info()[1]
@ -60,27 +71,27 @@ class NativeGateway(wsgiserver.Gateway):
if not self.recursive: if not self.recursive:
if ir.path in redirections: if ir.path in redirections:
raise RuntimeError( raise RuntimeError(
"InternalRedirector visited the same " 'InternalRedirector visited the same '
"URL twice: %r" % ir.path) 'URL twice: %r' % ir.path)
else: else:
# Add the *previous* path_info + qs to # Add the *previous* path_info + qs to
# redirections. # redirections.
if qs: if qs:
qs = "?" + qs qs = '?' + qs
redirections.append(sn + path + qs) redirections.append(sn + path + qs)
# Munge environment and try again. # Munge environment and try again.
method = "GET" method = 'GET'
path = ir.path path = ir.path
qs = ir.query_string qs = ir.query_string
rfile = BytesIO() rfile = io.BytesIO()
self.send_response( self.send_response(
response.output_status, response.header_list, response.output_status, response.header_list,
response.body) response.body)
finally: finally:
app.release_serving() app.release_serving()
except: except Exception:
tb = format_exc() tb = format_exc()
# print tb # print tb
cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR) cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
@ -88,10 +99,11 @@ class NativeGateway(wsgiserver.Gateway):
self.send_response(s, h, b) self.send_response(s, h, b)
def send_response(self, status, headers, body): def send_response(self, status, headers, body):
"""Send response to HTTP request."""
req = self.req req = self.req
# Set response status # Set response status
req.status = str(status or "500 Server Error") req.status = status or b'500 Server Error'
# Set response headers # Set response headers
for header, value in headers: for header, value in headers:
@ -105,24 +117,24 @@ class NativeGateway(wsgiserver.Gateway):
req.write(seg) req.write(seg)
class CPHTTPServer(wsgiserver.HTTPServer): class CPHTTPServer(cheroot.server.HTTPServer):
"""Wrapper for cheroot.server.HTTPServer.
"""Wrapper for wsgiserver.HTTPServer. cheroot has been designed to not reference CherryPy in any way,
wsgiserver has been designed to not reference CherryPy in any way,
so that it can be used in other frameworks and applications. so that it can be used in other frameworks and applications.
Therefore, we wrap it here, so we can apply some attributes Therefore, we wrap it here, so we can apply some attributes
from config -> cherrypy.server -> HTTPServer. from config -> cherrypy.server -> HTTPServer.
""" """
def __init__(self, server_adapter=cherrypy.server): def __init__(self, server_adapter=cherrypy.server):
"""Initialize CPHTTPServer."""
self.server_adapter = server_adapter self.server_adapter = server_adapter
server_name = (self.server_adapter.socket_host or server_name = (self.server_adapter.socket_host or
self.server_adapter.socket_file or self.server_adapter.socket_file or
None) None)
wsgiserver.HTTPServer.__init__( cheroot.server.HTTPServer.__init__(
self, server_adapter.bind_addr, NativeGateway, self, server_adapter.bind_addr, NativeGateway,
minthreads=server_adapter.thread_pool, minthreads=server_adapter.thread_pool,
maxthreads=server_adapter.thread_pool_max, maxthreads=server_adapter.thread_pool_max,
@ -140,15 +152,17 @@ class CPHTTPServer(wsgiserver.HTTPServer):
ssl_module = self.server_adapter.ssl_module or 'pyopenssl' ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
if self.server_adapter.ssl_context: if self.server_adapter.ssl_context:
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
self.ssl_adapter = adapter_class( self.ssl_adapter = adapter_class(
self.server_adapter.ssl_certificate, self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key, self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain) self.server_adapter.ssl_certificate_chain,
self.server_adapter.ssl_ciphers)
self.ssl_adapter.context = self.server_adapter.ssl_context self.ssl_adapter.context = self.server_adapter.ssl_context
elif self.server_adapter.ssl_certificate: elif self.server_adapter.ssl_certificate:
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
self.ssl_adapter = adapter_class( self.ssl_adapter = adapter_class(
self.server_adapter.ssl_certificate, self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key, self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain) self.server_adapter.ssl_certificate_chain,
self.server_adapter.ssl_ciphers)

View file

@ -61,7 +61,7 @@ Here's the built-in JSON tool for an example::
def json_in(force=True, debug=False): def json_in(force=True, debug=False):
request = cherrypy.serving.request request = cherrypy.serving.request
def json_processor(entity): def json_processor(entity):
\"""Read application/json data into request.json.\""" '''Read application/json data into request.json.'''
if not entity.headers.get("Content-Length", ""): if not entity.headers.get("Content-Length", ""):
raise cherrypy.HTTPError(411) raise cherrypy.HTTPError(411)
@ -120,8 +120,8 @@ try:
except ImportError: except ImportError:
def unquote_plus(bs): def unquote_plus(bs):
"""Bytes version of urllib.parse.unquote_plus.""" """Bytes version of urllib.parse.unquote_plus."""
bs = bs.replace(ntob('+'), ntob(' ')) bs = bs.replace(b'+', b' ')
atoms = bs.split(ntob('%')) atoms = bs.split(b'%')
for i in range(1, len(atoms)): for i in range(1, len(atoms)):
item = atoms[i] item = atoms[i]
try: try:
@ -129,10 +129,13 @@ except ImportError:
atoms[i] = bytes([pct]) + item[2:] atoms[i] = bytes([pct]) + item[2:]
except ValueError: except ValueError:
pass pass
return ntob('').join(atoms) return b''.join(atoms)
import six
import cheroot.server
import cherrypy import cherrypy
from cherrypy._cpcompat import basestring, ntob, ntou from cherrypy._cpcompat import ntou, unquote
from cherrypy.lib import httputil from cherrypy.lib import httputil
@ -144,14 +147,14 @@ def process_urlencoded(entity):
for charset in entity.attempt_charsets: for charset in entity.attempt_charsets:
try: try:
params = {} params = {}
for aparam in qs.split(ntob('&')): for aparam in qs.split(b'&'):
for pair in aparam.split(ntob(';')): for pair in aparam.split(b';'):
if not pair: if not pair:
continue continue
atoms = pair.split(ntob('='), 1) atoms = pair.split(b'=', 1)
if len(atoms) == 1: if len(atoms) == 1:
atoms.append(ntob('')) atoms.append(b'')
key = unquote_plus(atoms[0]).decode(charset) key = unquote_plus(atoms[0]).decode(charset)
value = unquote_plus(atoms[1]).decode(charset) value = unquote_plus(atoms[1]).decode(charset)
@ -169,8 +172,8 @@ def process_urlencoded(entity):
break break
else: else:
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
400, "The request entity could not be decoded. The following " 400, 'The request entity could not be decoded. The following '
"charsets were attempted: %s" % repr(entity.attempt_charsets)) 'charsets were attempted: %s' % repr(entity.attempt_charsets))
# Now that all values have been successfully parsed and decoded, # Now that all values have been successfully parsed and decoded,
# apply them to the entity.params dict. # apply them to the entity.params dict.
@ -185,7 +188,7 @@ def process_urlencoded(entity):
def process_multipart(entity): def process_multipart(entity):
"""Read all multipart parts into entity.parts.""" """Read all multipart parts into entity.parts."""
ib = "" ib = ''
if 'boundary' in entity.content_type.params: if 'boundary' in entity.content_type.params:
# http://tools.ietf.org/html/rfc2046#section-5.1.1 # http://tools.ietf.org/html/rfc2046#section-5.1.1
# "The grammar for parameters on the Content-type field is such that it # "The grammar for parameters on the Content-type field is such that it
@ -193,7 +196,7 @@ def process_multipart(entity):
# on the Content-type line" # on the Content-type line"
ib = entity.content_type.params['boundary'].strip('"') ib = entity.content_type.params['boundary'].strip('"')
if not re.match("^[ -~]{0,200}[!-~]$", ib): if not re.match('^[ -~]{0,200}[!-~]$', ib):
raise ValueError('Invalid boundary in multipart form: %r' % (ib,)) raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
ib = ('--' + ib).encode('ascii') ib = ('--' + ib).encode('ascii')
@ -315,7 +318,8 @@ class Entity(object):
:attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can :attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can
enable it with:: enable it with::
cherrypy.request.body.processors['multipart'] = _cpreqbody.process_multipart cherrypy.request.body.processors['multipart'] = \
_cpreqbody.process_multipart
in an ``on_start_resource`` tool. in an ``on_start_resource`` tool.
""" """
@ -325,14 +329,15 @@ class Entity(object):
# absence of a charset parameter, is US-ASCII." # absence of a charset parameter, is US-ASCII."
# However, many browsers send data in utf-8 with no charset. # However, many browsers send data in utf-8 with no charset.
attempt_charsets = ['utf-8'] attempt_charsets = ['utf-8']
"""A list of strings, each of which should be a known encoding. r"""A list of strings, each of which should be a known encoding.
When the Content-Type of the request body warrants it, each of the given When the Content-Type of the request body warrants it, each of the given
encodings will be tried in order. The first one to successfully decode the encodings will be tried in order. The first one to successfully decode the
entity without raising an error is stored as entity without raising an error is stored as
:attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
`HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_), `HTTP/1.1
<http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
but ``['us-ascii', 'utf-8']`` for multipart parts. but ``['us-ascii', 'utf-8']`` for multipart parts.
""" """
@ -428,7 +433,7 @@ class Entity(object):
# Copy the class 'attempt_charsets', prepending any Content-Type # Copy the class 'attempt_charsets', prepending any Content-Type
# charset # charset
dec = self.content_type.params.get("charset", None) dec = self.content_type.params.get('charset', None)
if dec: if dec:
self.attempt_charsets = [dec] + [c for c in self.attempt_charsets self.attempt_charsets = [dec] + [c for c in self.attempt_charsets
if c != dec] if c != dec]
@ -465,13 +470,10 @@ class Entity(object):
self.filename.endswith('"') self.filename.endswith('"')
): ):
self.filename = self.filename[1:-1] self.filename = self.filename[1:-1]
if 'filename*' in disp.params:
# The 'type' attribute is deprecated in 3.2; remove it in 3.3. # @see https://tools.ietf.org/html/rfc5987
type = property( encoding, lang, filename = disp.params['filename*'].split("'")
lambda self: self.content_type, self.filename = unquote(str(filename), encoding)
doc="A deprecated alias for "
":attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`."
)
def read(self, size=None, fp_out=None): def read(self, size=None, fp_out=None):
return self.fp.read(size, fp_out) return self.fp.read(size, fp_out)
@ -520,8 +522,26 @@ class Entity(object):
self.file.seek(0) self.file.seek(0)
else: else:
value = self.value value = self.value
value = self.decode_entity(value)
return value return value
def decode_entity(self, value):
"""Return a given byte encoded value as a string"""
for charset in self.attempt_charsets:
try:
value = value.decode(charset)
except UnicodeDecodeError:
pass
else:
self.charset = charset
return value
else:
raise cherrypy.HTTPError(
400,
'The request entity could not be decoded. The following '
'charsets were attempted: %s' % repr(self.attempt_charsets)
)
def process(self): def process(self):
"""Execute the best-match processor for the given media type.""" """Execute the best-match processor for the given media type."""
proc = None proc = None
@ -556,14 +576,15 @@ class Part(Entity):
# "The default character set, which must be assumed in the absence of a # "The default character set, which must be assumed in the absence of a
# charset parameter, is US-ASCII." # charset parameter, is US-ASCII."
attempt_charsets = ['us-ascii', 'utf-8'] attempt_charsets = ['us-ascii', 'utf-8']
"""A list of strings, each of which should be a known encoding. r"""A list of strings, each of which should be a known encoding.
When the Content-Type of the request body warrants it, each of the given When the Content-Type of the request body warrants it, each of the given
encodings will be tried in order. The first one to successfully decode the encodings will be tried in order. The first one to successfully decode the
entity without raising an error is stored as entity without raising an error is stored as
:attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
`HTTP/1.1 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_), `HTTP/1.1
<http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
but ``['us-ascii', 'utf-8']`` for multipart parts. but ``['us-ascii', 'utf-8']`` for multipart parts.
""" """
@ -595,40 +616,40 @@ class Part(Entity):
self.file = None self.file = None
self.value = None self.value = None
@classmethod
def from_fp(cls, fp, boundary): def from_fp(cls, fp, boundary):
headers = cls.read_headers(fp) headers = cls.read_headers(fp)
return cls(fp, headers, boundary) return cls(fp, headers, boundary)
from_fp = classmethod(from_fp)
@classmethod
def read_headers(cls, fp): def read_headers(cls, fp):
headers = httputil.HeaderMap() headers = httputil.HeaderMap()
while True: while True:
line = fp.readline() line = fp.readline()
if not line: if not line:
# No more data--illegal end of headers # No more data--illegal end of headers
raise EOFError("Illegal end of headers.") raise EOFError('Illegal end of headers.')
if line == ntob('\r\n'): if line == b'\r\n':
# Normal end of headers # Normal end of headers
break break
if not line.endswith(ntob('\r\n')): if not line.endswith(b'\r\n'):
raise ValueError("MIME requires CRLF terminators: %r" % line) raise ValueError('MIME requires CRLF terminators: %r' % line)
if line[0] in ntob(' \t'): if line[0] in b' \t':
# It's a continuation line. # It's a continuation line.
v = line.strip().decode('ISO-8859-1') v = line.strip().decode('ISO-8859-1')
else: else:
k, v = line.split(ntob(":"), 1) k, v = line.split(b':', 1)
k = k.strip().decode('ISO-8859-1') k = k.strip().decode('ISO-8859-1')
v = v.strip().decode('ISO-8859-1') v = v.strip().decode('ISO-8859-1')
existing = headers.get(k) existing = headers.get(k)
if existing: if existing:
v = ", ".join((existing, v)) v = ', '.join((existing, v))
headers[k] = v headers[k] = v
return headers return headers
read_headers = classmethod(read_headers)
def read_lines_to_boundary(self, fp_out=None): def read_lines_to_boundary(self, fp_out=None):
"""Read bytes from self.fp and return or write them to a file. """Read bytes from self.fp and return or write them to a file.
@ -640,16 +661,16 @@ class Part(Entity):
object that supports the 'write' method; all bytes read will be object that supports the 'write' method; all bytes read will be
written to the fp, and that fp is returned. written to the fp, and that fp is returned.
""" """
endmarker = self.boundary + ntob("--") endmarker = self.boundary + b'--'
delim = ntob("") delim = b''
prev_lf = True prev_lf = True
lines = [] lines = []
seen = 0 seen = 0
while True: while True:
line = self.fp.readline(1 << 16) line = self.fp.readline(1 << 16)
if not line: if not line:
raise EOFError("Illegal end of multipart body.") raise EOFError('Illegal end of multipart body.')
if line.startswith(ntob("--")) and prev_lf: if line.startswith(b'--') and prev_lf:
strippedline = line.strip() strippedline = line.strip()
if strippedline == self.boundary: if strippedline == self.boundary:
break break
@ -659,16 +680,16 @@ class Part(Entity):
line = delim + line line = delim + line
if line.endswith(ntob("\r\n")): if line.endswith(b'\r\n'):
delim = ntob("\r\n") delim = b'\r\n'
line = line[:-2] line = line[:-2]
prev_lf = True prev_lf = True
elif line.endswith(ntob("\n")): elif line.endswith(b'\n'):
delim = ntob("\n") delim = b'\n'
line = line[:-1] line = line[:-1]
prev_lf = True prev_lf = True
else: else:
delim = ntob("") delim = b''
prev_lf = False prev_lf = False
if fp_out is None: if fp_out is None:
@ -682,21 +703,8 @@ class Part(Entity):
fp_out.write(line) fp_out.write(line)
if fp_out is None: if fp_out is None:
result = ntob('').join(lines) result = b''.join(lines)
for charset in self.attempt_charsets: return result
try:
result = result.decode(charset)
except UnicodeDecodeError:
pass
else:
self.charset = charset
return result
else:
raise cherrypy.HTTPError(
400,
"The request entity could not be decoded. The following "
"charsets were attempted: %s" % repr(self.attempt_charsets)
)
else: else:
fp_out.seek(0) fp_out.seek(0)
return fp_out return fp_out
@ -710,7 +718,7 @@ class Part(Entity):
self.file = self.read_into_file() self.file = self.read_into_file()
else: else:
result = self.read_lines_to_boundary() result = self.read_lines_to_boundary()
if isinstance(result, basestring): if isinstance(result, bytes):
self.value = result self.value = result
else: else:
self.file = result self.file = result
@ -725,31 +733,10 @@ class Part(Entity):
self.read_lines_to_boundary(fp_out=fp_out) self.read_lines_to_boundary(fp_out=fp_out)
return fp_out return fp_out
Entity.part_class = Part Entity.part_class = Part
try: inf = float('inf')
inf = float('inf')
except ValueError:
# Python 2.4 and lower
class Infinity(object):
def __cmp__(self, other):
return 1
def __sub__(self, other):
return self
inf = Infinity()
comma_separated_headers = [
'Accept', 'Accept-Charset', 'Accept-Encoding',
'Accept-Language', 'Accept-Ranges', 'Allow',
'Cache-Control', 'Connection', 'Content-Encoding',
'Content-Language', 'Expect', 'If-Match',
'If-None-Match', 'Pragma', 'Proxy-Authenticate',
'Te', 'Trailer', 'Transfer-Encoding', 'Upgrade',
'Vary', 'Via', 'Warning', 'Www-Authenticate'
]
class SizedReader: class SizedReader:
@ -760,7 +747,7 @@ class SizedReader:
self.fp = fp self.fp = fp
self.length = length self.length = length
self.maxbytes = maxbytes self.maxbytes = maxbytes
self.buffer = ntob('') self.buffer = b''
self.bufsize = bufsize self.bufsize = bufsize
self.bytes_read = 0 self.bytes_read = 0
self.done = False self.done = False
@ -796,7 +783,7 @@ class SizedReader:
if remaining == 0: if remaining == 0:
self.finish() self.finish()
if fp_out is None: if fp_out is None:
return ntob('') return b''
else: else:
return None return None
@ -806,7 +793,7 @@ class SizedReader:
if self.buffer: if self.buffer:
if remaining is inf: if remaining is inf:
data = self.buffer data = self.buffer
self.buffer = ntob('') self.buffer = b''
else: else:
data = self.buffer[:remaining] data = self.buffer[:remaining]
self.buffer = self.buffer[remaining:] self.buffer = self.buffer[remaining:]
@ -834,7 +821,7 @@ class SizedReader:
if e.__class__.__name__ == 'MaxSizeExceeded': if e.__class__.__name__ == 'MaxSizeExceeded':
# Post data is too big # Post data is too big
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
413, "Maximum request length: %r" % e.args[1]) 413, 'Maximum request length: %r' % e.args[1])
else: else:
raise raise
if not data: if not data:
@ -855,7 +842,7 @@ class SizedReader:
fp_out.write(data) fp_out.write(data)
if fp_out is None: if fp_out is None:
return ntob('').join(chunks) return b''.join(chunks)
def readline(self, size=None): def readline(self, size=None):
"""Read a line from the request body and return it.""" """Read a line from the request body and return it."""
@ -867,7 +854,7 @@ class SizedReader:
data = self.read(chunksize) data = self.read(chunksize)
if not data: if not data:
break break
pos = data.find(ntob('\n')) + 1 pos = data.find(b'\n') + 1
if pos: if pos:
chunks.append(data[:pos]) chunks.append(data[:pos])
remainder = data[pos:] remainder = data[pos:]
@ -876,7 +863,7 @@ class SizedReader:
break break
else: else:
chunks.append(data) chunks.append(data)
return ntob('').join(chunks) return b''.join(chunks)
def readlines(self, sizehint=None): def readlines(self, sizehint=None):
"""Read lines from the request body and return them.""" """Read lines from the request body and return them."""
@ -905,28 +892,28 @@ class SizedReader:
try: try:
for line in self.fp.read_trailer_lines(): for line in self.fp.read_trailer_lines():
if line[0] in ntob(' \t'): if line[0] in b' \t':
# It's a continuation line. # It's a continuation line.
v = line.strip() v = line.strip()
else: else:
try: try:
k, v = line.split(ntob(":"), 1) k, v = line.split(b':', 1)
except ValueError: except ValueError:
raise ValueError("Illegal header line.") raise ValueError('Illegal header line.')
k = k.strip().title() k = k.strip().title()
v = v.strip() v = v.strip()
if k in comma_separated_headers: if k in cheroot.server.comma_separated_headers:
existing = self.trailers.get(envname) existing = self.trailers.get(k)
if existing: if existing:
v = ntob(", ").join((existing, v)) v = b', '.join((existing, v))
self.trailers[k] = v self.trailers[k] = v
except Exception: except Exception:
e = sys.exc_info()[1] e = sys.exc_info()[1]
if e.__class__.__name__ == 'MaxSizeExceeded': if e.__class__.__name__ == 'MaxSizeExceeded':
# Post data is too big # Post data is too big
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
413, "Maximum request length: %r" % e.args[1]) 413, 'Maximum request length: %r' % e.args[1])
else: else:
raise raise
@ -940,7 +927,7 @@ class RequestBody(Entity):
# Don't parse the request body at all if the client didn't provide # Don't parse the request body at all if the client didn't provide
# a Content-Type header. See # a Content-Type header. See
# https://bitbucket.org/cherrypy/cherrypy/issue/790 # https://github.com/cherrypy/cherrypy/issues/790
default_content_type = '' default_content_type = ''
"""This defines a default ``Content-Type`` to use if no Content-Type header """This defines a default ``Content-Type`` to use if no Content-Type header
is given. The empty string is used for RequestBody, which results in the is given. The empty string is used for RequestBody, which results in the
@ -1002,7 +989,7 @@ class RequestBody(Entity):
# Python 2 only: keyword arguments must be byte strings (type # Python 2 only: keyword arguments must be byte strings (type
# 'str'). # 'str').
if sys.version_info < (3, 0): if sys.version_info < (3, 0):
if isinstance(key, unicode): if isinstance(key, six.text_type):
key = key.encode('ISO-8859-1') key = key.encode('ISO-8859-1')
if key in request_params: if key in request_params:

View file

@ -1,15 +1,18 @@
import os
import sys import sys
import time import time
import warnings
import uuid
import six
from six.moves.http_cookies import SimpleCookie, CookieError
from more_itertools import consume
import cherrypy import cherrypy
from cherrypy._cpcompat import basestring, copykeys, ntob, unicodestr from cherrypy._cpcompat import ntob
from cherrypy._cpcompat import SimpleCookie, CookieError, py3k from cherrypy import _cpreqbody
from cherrypy import _cpreqbody, _cpconfig
from cherrypy._cperror import format_exc, bare_error from cherrypy._cperror import format_exc, bare_error
from cherrypy.lib import httputil, file_generator from cherrypy.lib import httputil, reprconf, encoding
class Hook(object): class Hook(object):
@ -41,33 +44,32 @@ class Hook(object):
self.callback = callback self.callback = callback
if failsafe is None: if failsafe is None:
failsafe = getattr(callback, "failsafe", False) failsafe = getattr(callback, 'failsafe', False)
self.failsafe = failsafe self.failsafe = failsafe
if priority is None: if priority is None:
priority = getattr(callback, "priority", 50) priority = getattr(callback, 'priority', 50)
self.priority = priority self.priority = priority
self.kwargs = kwargs self.kwargs = kwargs
def __lt__(self, other): def __lt__(self, other):
# Python 3 """
Hooks sort by priority, ascending, such that
hooks of lower priority are run first.
"""
return self.priority < other.priority return self.priority < other.priority
def __cmp__(self, other):
# Python 2
return cmp(self.priority, other.priority)
def __call__(self): def __call__(self):
"""Run self.callback(**self.kwargs).""" """Run self.callback(**self.kwargs)."""
return self.callback(**self.kwargs) return self.callback(**self.kwargs)
def __repr__(self): def __repr__(self):
cls = self.__class__ cls = self.__class__
return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)" return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)'
% (cls.__module__, cls.__name__, self.callback, % (cls.__module__, cls.__name__, self.callback,
self.failsafe, self.priority, self.failsafe, self.priority,
", ".join(['%s=%r' % (k, v) ', '.join(['%s=%r' % (k, v)
for k, v in self.kwargs.items()]))) for k, v in self.kwargs.items()])))
@ -107,7 +109,7 @@ class HookMap(dict):
except (cherrypy.HTTPError, cherrypy.HTTPRedirect, except (cherrypy.HTTPError, cherrypy.HTTPRedirect,
cherrypy.InternalRedirect): cherrypy.InternalRedirect):
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
except: except Exception:
exc = sys.exc_info()[1] exc = sys.exc_info()[1]
cherrypy.log(traceback=True, severity=40) cherrypy.log(traceback=True, severity=40)
if exc: if exc:
@ -124,10 +126,10 @@ class HookMap(dict):
def __repr__(self): def __repr__(self):
cls = self.__class__ cls = self.__class__
return "%s.%s(points=%r)" % ( return '%s.%s(points=%r)' % (
cls.__module__, cls.__module__,
cls.__name__, cls.__name__,
copykeys(self) list(self)
) )
@ -138,9 +140,9 @@ def hooks_namespace(k, v):
# Use split again to allow multiple hooks for a single # Use split again to allow multiple hooks for a single
# hookpoint per path (e.g. "hooks.before_handler.1"). # hookpoint per path (e.g. "hooks.before_handler.1").
# Little-known fact you only get from reading source ;) # Little-known fact you only get from reading source ;)
hookpoint = k.split(".", 1)[0] hookpoint = k.split('.', 1)[0]
if isinstance(v, basestring): if isinstance(v, six.string_types):
v = cherrypy.lib.attributes(v) v = cherrypy.lib.reprconf.attributes(v)
if not isinstance(v, Hook): if not isinstance(v, Hook):
v = Hook(v) v = Hook(v)
cherrypy.serving.request.hooks[hookpoint].append(v) cherrypy.serving.request.hooks[hookpoint].append(v)
@ -199,23 +201,23 @@ class Request(object):
unless we are processing an InternalRedirect.""" unless we are processing an InternalRedirect."""
# Conversation/connection attributes # Conversation/connection attributes
local = httputil.Host("127.0.0.1", 80) local = httputil.Host('127.0.0.1', 80)
"An httputil.Host(ip, port, hostname) object for the server socket." 'An httputil.Host(ip, port, hostname) object for the server socket.'
remote = httputil.Host("127.0.0.1", 1111) remote = httputil.Host('127.0.0.1', 1111)
"An httputil.Host(ip, port, hostname) object for the client socket." 'An httputil.Host(ip, port, hostname) object for the client socket.'
scheme = "http" scheme = 'http'
""" """
The protocol used between client and server. In most cases, The protocol used between client and server. In most cases,
this will be either 'http' or 'https'.""" this will be either 'http' or 'https'."""
server_protocol = "HTTP/1.1" server_protocol = 'HTTP/1.1'
""" """
The HTTP version for which the HTTP server is at least The HTTP version for which the HTTP server is at least
conditionally compliant.""" conditionally compliant."""
base = "" base = ''
"""The (scheme://host) portion of the requested URL. """The (scheme://host) portion of the requested URL.
In some cases (e.g. when proxying via mod_rewrite), this may contain In some cases (e.g. when proxying via mod_rewrite), this may contain
path segments which cherrypy.url uses when constructing url's, but path segments which cherrypy.url uses when constructing url's, but
@ -223,13 +225,13 @@ class Request(object):
MUST NOT end in a slash.""" MUST NOT end in a slash."""
# Request-Line attributes # Request-Line attributes
request_line = "" request_line = ''
""" """
The complete Request-Line received from the client. This is a The complete Request-Line received from the client. This is a
single string consisting of the request method, URI, and protocol single string consisting of the request method, URI, and protocol
version (joined by spaces). Any final CRLF is removed.""" version (joined by spaces). Any final CRLF is removed."""
method = "GET" method = 'GET'
""" """
Indicates the HTTP method to be performed on the resource identified Indicates the HTTP method to be performed on the resource identified
by the Request-URI. Common methods include GET, HEAD, POST, PUT, and by the Request-URI. Common methods include GET, HEAD, POST, PUT, and
@ -237,7 +239,7 @@ class Request(object):
servers and gateways may restrict the set of allowable methods. servers and gateways may restrict the set of allowable methods.
CherryPy applications SHOULD restrict the set (on a per-URI basis).""" CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
query_string = "" query_string = ''
""" """
The query component of the Request-URI, a string of information to be The query component of the Request-URI, a string of information to be
interpreted by the resource. The query portion of a URI follows the interpreted by the resource. The query portion of a URI follows the
@ -312,7 +314,7 @@ class Request(object):
If True, the rfile (if any) is automatically read and parsed, If True, the rfile (if any) is automatically read and parsed,
and the result placed into request.params or request.body.""" and the result placed into request.params or request.body."""
methods_with_bodies = ("POST", "PUT") methods_with_bodies = ('POST', 'PUT', 'PATCH')
""" """
A sequence of HTTP methods for which CherryPy will automatically A sequence of HTTP methods for which CherryPy will automatically
attempt to read a body from the rfile. If you are going to change attempt to read a body from the rfile. If you are going to change
@ -341,7 +343,7 @@ class Request(object):
to a hierarchical arrangement of objects, starting at request.app.root. to a hierarchical arrangement of objects, starting at request.app.root.
See help(cherrypy.dispatch) for more information.""" See help(cherrypy.dispatch) for more information."""
script_name = "" script_name = ''
""" """
The 'mount point' of the application which is handling this request. The 'mount point' of the application which is handling this request.
@ -349,7 +351,7 @@ class Request(object):
the root of the URI, it MUST be an empty string (not "/"). the root of the URI, it MUST be an empty string (not "/").
""" """
path_info = "/" path_info = '/'
""" """
The 'relative path' portion of the Request-URI. This is relative The 'relative path' portion of the Request-URI. This is relative
to the script_name ('mount point') of the application which is to the script_name ('mount point') of the application which is
@ -467,16 +469,19 @@ class Request(object):
A string containing the stage reached in the request-handling process. A string containing the stage reached in the request-handling process.
This is useful when debugging a live server with hung requests.""" This is useful when debugging a live server with hung requests."""
namespaces = _cpconfig.NamespaceSet( unique_id = None
**{"hooks": hooks_namespace, """A lazy object generating and memorizing UUID4 on ``str()`` render."""
"request": request_namespace,
"response": response_namespace, namespaces = reprconf.NamespaceSet(
"error_page": error_page_namespace, **{'hooks': hooks_namespace,
"tools": cherrypy.tools, 'request': request_namespace,
'response': response_namespace,
'error_page': error_page_namespace,
'tools': cherrypy.tools,
}) })
def __init__(self, local_host, remote_host, scheme="http", def __init__(self, local_host, remote_host, scheme='http',
server_protocol="HTTP/1.1"): server_protocol='HTTP/1.1'):
"""Populate a new Request object. """Populate a new Request object.
local_host should be an httputil.Host object with the server info. local_host should be an httputil.Host object with the server info.
@ -498,6 +503,8 @@ class Request(object):
self.stage = None self.stage = None
self.unique_id = LazyUUID4()
def close(self): def close(self):
"""Run cleanup code. (Core)""" """Run cleanup code. (Core)"""
if not self.closed: if not self.closed:
@ -544,7 +551,7 @@ class Request(object):
self.error_response = cherrypy.HTTPError(500).set_response self.error_response = cherrypy.HTTPError(500).set_response
self.method = method self.method = method
path = path or "/" path = path or '/'
self.query_string = query_string or '' self.query_string = query_string or ''
self.params = {} self.params = {}
@ -590,7 +597,7 @@ class Request(object):
except self.throws: except self.throws:
raise raise
except: except Exception:
if self.throw_errors: if self.throw_errors:
raise raise
else: else:
@ -600,95 +607,92 @@ class Request(object):
if self.show_tracebacks: if self.show_tracebacks:
body = format_exc() body = format_exc()
else: else:
body = "" body = ''
r = bare_error(body) r = bare_error(body)
response.output_status, response.header_list, response.body = r response.output_status, response.header_list, response.body = r
if self.method == "HEAD": if self.method == 'HEAD':
# HEAD requests MUST NOT return a message-body in the response. # HEAD requests MUST NOT return a message-body in the response.
response.body = [] response.body = []
try: try:
cherrypy.log.access() cherrypy.log.access()
except: except Exception:
cherrypy.log.error(traceback=True) cherrypy.log.error(traceback=True)
if response.timed_out:
raise cherrypy.TimeoutError()
return response return response
# Uncomment for stage debugging
# stage = property(lambda self: self._stage, lambda self, v: print(v))
def respond(self, path_info): def respond(self, path_info):
"""Generate a response for the resource at self.path_info. (Core)""" """Generate a response for the resource at self.path_info. (Core)"""
response = cherrypy.serving.response
try: try:
try: try:
try: try:
if self.app is None: self._do_respond(path_info)
raise cherrypy.NotFound()
# Get the 'Host' header, so we can HTTPRedirect properly.
self.stage = 'process_headers'
self.process_headers()
# Make a copy of the class hooks
self.hooks = self.__class__.hooks.copy()
self.toolmaps = {}
self.stage = 'get_resource'
self.get_resource(path_info)
self.body = _cpreqbody.RequestBody(
self.rfile, self.headers, request_params=self.params)
self.namespaces(self.config)
self.stage = 'on_start_resource'
self.hooks.run('on_start_resource')
# Parse the querystring
self.stage = 'process_query_string'
self.process_query_string()
# Process the body
if self.process_request_body:
if self.method not in self.methods_with_bodies:
self.process_request_body = False
self.stage = 'before_request_body'
self.hooks.run('before_request_body')
if self.process_request_body:
self.body.process()
# Run the handler
self.stage = 'before_handler'
self.hooks.run('before_handler')
if self.handler:
self.stage = 'handler'
response.body = self.handler()
# Finalize
self.stage = 'before_finalize'
self.hooks.run('before_finalize')
response.finalize()
except (cherrypy.HTTPRedirect, cherrypy.HTTPError): except (cherrypy.HTTPRedirect, cherrypy.HTTPError):
inst = sys.exc_info()[1] inst = sys.exc_info()[1]
inst.set_response() inst.set_response()
self.stage = 'before_finalize (HTTPError)' self.stage = 'before_finalize (HTTPError)'
self.hooks.run('before_finalize') self.hooks.run('before_finalize')
response.finalize() cherrypy.serving.response.finalize()
finally: finally:
self.stage = 'on_end_resource' self.stage = 'on_end_resource'
self.hooks.run('on_end_resource') self.hooks.run('on_end_resource')
except self.throws: except self.throws:
raise raise
except: except Exception:
if self.throw_errors: if self.throw_errors:
raise raise
self.handle_error() self.handle_error()
def _do_respond(self, path_info):
response = cherrypy.serving.response
if self.app is None:
raise cherrypy.NotFound()
self.hooks = self.__class__.hooks.copy()
self.toolmaps = {}
# Get the 'Host' header, so we can HTTPRedirect properly.
self.stage = 'process_headers'
self.process_headers()
self.stage = 'get_resource'
self.get_resource(path_info)
self.body = _cpreqbody.RequestBody(
self.rfile, self.headers, request_params=self.params)
self.namespaces(self.config)
self.stage = 'on_start_resource'
self.hooks.run('on_start_resource')
# Parse the querystring
self.stage = 'process_query_string'
self.process_query_string()
# Process the body
if self.process_request_body:
if self.method not in self.methods_with_bodies:
self.process_request_body = False
self.stage = 'before_request_body'
self.hooks.run('before_request_body')
if self.process_request_body:
self.body.process()
# Run the handler
self.stage = 'before_handler'
self.hooks.run('before_handler')
if self.handler:
self.stage = 'handler'
response.body = self.handler()
# Finalize
self.stage = 'before_finalize'
self.hooks.run('before_finalize')
response.finalize()
def process_query_string(self): def process_query_string(self):
"""Parse the query string into Python structures. (Core)""" """Parse the query string into Python structures. (Core)"""
try: try:
@ -696,14 +700,14 @@ class Request(object):
self.query_string, encoding=self.query_string_encoding) self.query_string, encoding=self.query_string_encoding)
except UnicodeDecodeError: except UnicodeDecodeError:
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
404, "The given query string could not be processed. Query " 404, 'The given query string could not be processed. Query '
"strings for this resource must be encoded with %r." % 'strings for this resource must be encoded with %r.' %
self.query_string_encoding) self.query_string_encoding)
# Python 2 only: keyword arguments must be byte strings (type 'str'). # Python 2 only: keyword arguments must be byte strings (type 'str').
if not py3k: if six.PY2:
for key, value in p.items(): for key, value in p.items():
if isinstance(key, unicode): if isinstance(key, six.text_type):
del p[key] del p[key]
p[key.encode(self.query_string_encoding)] = value p[key.encode(self.query_string_encoding)] = value
self.params.update(p) self.params.update(p)
@ -718,23 +722,16 @@ class Request(object):
name = name.title() name = name.title()
value = value.strip() value = value.strip()
# Warning: if there is more than one header entry for cookies headers[name] = httputil.decode_TEXT_maybe(value)
# (AFAIK, only Konqueror does that), only the last one will
# remain in headers (but they will be correctly stored in
# request.cookie).
if "=?" in value:
dict.__setitem__(headers, name, httputil.decode_TEXT(value))
else:
dict.__setitem__(headers, name, value)
# Handle cookies differently because on Konqueror, multiple # Some clients, notably Konquoror, supply multiple
# cookies come on different lines with the same key # cookies on different lines with the same key. To
# handle this case, store all cookies in self.cookie.
if name == 'Cookie': if name == 'Cookie':
try: try:
self.cookie.load(value) self.cookie.load(value)
except CookieError: except CookieError as exc:
msg = "Illegal cookie name %s" % value.split('=')[0] raise cherrypy.HTTPError(400, str(exc))
raise cherrypy.HTTPError(400, msg)
if not dict.__contains__(headers, 'Host'): if not dict.__contains__(headers, 'Host'):
# All Internet-based HTTP/1.1 servers MUST respond with a 400 # All Internet-based HTTP/1.1 servers MUST respond with a 400
@ -746,7 +743,7 @@ class Request(object):
host = dict.get(headers, 'Host') host = dict.get(headers, 'Host')
if not host: if not host:
host = self.local.name or self.local.ip host = self.local.name or self.local.ip
self.base = "%s://%s" % (self.scheme, host) self.base = '%s://%s' % (self.scheme, host)
def get_resource(self, path): def get_resource(self, path):
"""Call a dispatcher (which sets self.handler and .config). (Core)""" """Call a dispatcher (which sets self.handler and .config). (Core)"""
@ -754,7 +751,7 @@ class Request(object):
# dispatchers can only be specified in app.config, not in _cp_config # dispatchers can only be specified in app.config, not in _cp_config
# (since custom dispatchers may not even have an app.root). # (since custom dispatchers may not even have an app.root).
dispatch = self.app.find_config( dispatch = self.app.find_config(
path, "request.dispatch", self.dispatch) path, 'request.dispatch', self.dispatch)
# dispatch() should set self.handler and self.config # dispatch() should set self.handler and self.config
dispatch(path) dispatch(path)
@ -762,46 +759,23 @@ class Request(object):
def handle_error(self): def handle_error(self):
"""Handle the last unanticipated exception. (Core)""" """Handle the last unanticipated exception. (Core)"""
try: try:
self.hooks.run("before_error_response") self.hooks.run('before_error_response')
if self.error_response: if self.error_response:
self.error_response() self.error_response()
self.hooks.run("after_error_response") self.hooks.run('after_error_response')
cherrypy.serving.response.finalize() cherrypy.serving.response.finalize()
except cherrypy.HTTPRedirect: except cherrypy.HTTPRedirect:
inst = sys.exc_info()[1] inst = sys.exc_info()[1]
inst.set_response() inst.set_response()
cherrypy.serving.response.finalize() cherrypy.serving.response.finalize()
# ------------------------- Properties ------------------------- #
def _get_body_params(self):
warnings.warn(
"body_params is deprecated in CherryPy 3.2, will be removed in "
"CherryPy 3.3.",
DeprecationWarning
)
return self.body.params
body_params = property(_get_body_params,
doc="""
If the request Content-Type is 'application/x-www-form-urlencoded' or
multipart, this will be a dict of the params pulled from the entity
body; that is, it will be the portion of request.params that come
from the message body (sometimes called "POST params", although they
can be sent with various HTTP method verbs). This value is set between
the 'before_request_body' and 'before_handler' hooks (assuming that
process_request_body is True).
Deprecated in 3.2, will be removed for 3.3 in favor of
:attr:`request.body.params<cherrypy._cprequest.RequestBody.params>`.""")
class ResponseBody(object): class ResponseBody(object):
"""The body of the HTTP response (the response entity).""" """The body of the HTTP response (the response entity)."""
if py3k: unicode_err = ('Page handlers MUST return bytes. Use tools.encode '
unicode_err = ("Page handlers MUST return bytes. Use tools.encode " 'if you wish to return unicode.')
"if you wish to return unicode.")
def __get__(self, obj, objclass=None): def __get__(self, obj, objclass=None):
if obj is None: if obj is None:
@ -812,37 +786,21 @@ class ResponseBody(object):
def __set__(self, obj, value): def __set__(self, obj, value):
# Convert the given value to an iterable object. # Convert the given value to an iterable object.
if py3k and isinstance(value, str): if isinstance(value, six.text_type):
raise ValueError(self.unicode_err) raise ValueError(self.unicode_err)
elif isinstance(value, list):
if isinstance(value, basestring):
# strings get wrapped in a list because iterating over a single
# item list is much faster than iterating over every character
# in a long string.
if value:
value = [value]
else:
# [''] doesn't evaluate to False, so replace it with [].
value = []
elif py3k and isinstance(value, list):
# every item in a list must be bytes... # every item in a list must be bytes...
for i, item in enumerate(value): if any(isinstance(item, six.text_type) for item in value):
if isinstance(item, str): raise ValueError(self.unicode_err)
raise ValueError(self.unicode_err)
# Don't use isinstance here; io.IOBase which has an ABC takes obj._body = encoding.prepare_iter(value)
# 1000 times as long as, say, isinstance(value, str)
elif hasattr(value, 'read'):
value = file_generator(value)
elif value is None:
value = []
obj._body = value
class Response(object): class Response(object):
"""An HTTP Response, including status, headers, and body.""" """An HTTP Response, including status, headers, and body."""
status = "" status = ''
"""The HTTP Status-Code and Reason-Phrase.""" """The HTTP Status-Code and Reason-Phrase."""
header_list = [] header_list = []
@ -872,14 +830,6 @@ class Response(object):
time = None time = None
"""The value of time.time() when created. Use in HTTP dates.""" """The value of time.time() when created. Use in HTTP dates."""
timeout = 300
"""Seconds after which the response will be aborted."""
timed_out = False
"""
Flag to indicate the response should be aborted, because it has
exceeded its timeout."""
stream = False stream = False
"""If False, buffer the response body.""" """If False, buffer the response body."""
@ -893,27 +843,25 @@ class Response(object):
# Since we know all our keys are titled strings, we can # Since we know all our keys are titled strings, we can
# bypass HeaderMap.update and get a big speed boost. # bypass HeaderMap.update and get a big speed boost.
dict.update(self.headers, { dict.update(self.headers, {
"Content-Type": 'text/html', 'Content-Type': 'text/html',
"Server": "CherryPy/" + cherrypy.__version__, 'Server': 'CherryPy/' + cherrypy.__version__,
"Date": httputil.HTTPDate(self.time), 'Date': httputil.HTTPDate(self.time),
}) })
self.cookie = SimpleCookie() self.cookie = SimpleCookie()
def collapse_body(self): def collapse_body(self):
"""Collapse self.body to a single string; replace it and return it.""" """Collapse self.body to a single string; replace it and return it."""
if isinstance(self.body, basestring): new_body = b''.join(self.body)
return self.body self.body = new_body
return new_body
newbody = [] def _flush_body(self):
for chunk in self.body: """
if py3k and not isinstance(chunk, bytes): Discard self.body but consume any generator such that
raise TypeError("Chunk %s is not of type 'bytes'." % any finalization can occur, such as is required by
repr(chunk)) caching.tee_output().
newbody.append(chunk) """
newbody = ntob('').join(newbody) consume(iter(self.body))
self.body = newbody
return newbody
def finalize(self): def finalize(self):
"""Transform headers (and cookies) into self.header_list. (Core)""" """Transform headers (and cookies) into self.header_list. (Core)"""
@ -924,9 +872,9 @@ class Response(object):
headers = self.headers headers = self.headers
self.status = "%s %s" % (code, reason) self.status = '%s %s' % (code, reason)
self.output_status = ntob(str(code), 'ascii') + \ self.output_status = ntob(str(code), 'ascii') + \
ntob(" ") + headers.encode(reason) b' ' + headers.encode(reason)
if self.stream: if self.stream:
# The upshot: wsgiserver will chunk the response if # The upshot: wsgiserver will chunk the response if
@ -939,7 +887,8 @@ class Response(object):
# and 304 (not modified) responses MUST NOT # and 304 (not modified) responses MUST NOT
# include a message-body." # include a message-body."
dict.pop(headers, 'Content-Length', None) dict.pop(headers, 'Content-Length', None)
self.body = ntob("") self._flush_body()
self.body = b''
else: else:
# Responses which are not streamed should have a Content-Length, # Responses which are not streamed should have a Content-Length,
# but allow user code to set Content-Length if desired. # but allow user code to set Content-Length if desired.
@ -952,22 +901,30 @@ class Response(object):
cookie = self.cookie.output() cookie = self.cookie.output()
if cookie: if cookie:
for line in cookie.split("\n"): for line in cookie.split('\r\n'):
if line.endswith("\r"): name, value = line.split(': ', 1)
# Python 2.4 emits cookies joined by LF but 2.5+ by CRLF. if isinstance(name, six.text_type):
line = line[:-1] name = name.encode('ISO-8859-1')
name, value = line.split(": ", 1) if isinstance(value, six.text_type):
if isinstance(name, unicodestr):
name = name.encode("ISO-8859-1")
if isinstance(value, unicodestr):
value = headers.encode(value) value = headers.encode(value)
h.append((name, value)) h.append((name, value))
def check_timeout(self):
"""If now > self.time + self.timeout, set self.timed_out.
This purposefully sets a flag, rather than raising an error, class LazyUUID4(object):
so that a monitor thread can interrupt the Response thread. 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.
""" """
if time.time() > self.time + self.timeout: try:
self.timed_out = True self._uuid4
except AttributeError:
# evaluate on first access
self._uuid4 = uuid.uuid4()
return self._uuid4

View file

@ -1,18 +1,17 @@
"""Manage HTTP servers with CherryPy.""" """Manage HTTP servers with CherryPy."""
import warnings import six
import cherrypy import cherrypy
from cherrypy.lib import attributes from cherrypy.lib.reprconf import attributes
from cherrypy._cpcompat import basestring, py3k from cherrypy._cpcompat import text_or_bytes
from cherrypy.process.servers import ServerAdapter
# We import * because we want to export check_port
# et al as attributes of this module. __all__ = ('Server', )
from cherrypy.process.servers import *
class Server(ServerAdapter): class Server(ServerAdapter):
"""An adapter for an HTTP server. """An adapter for an HTTP server.
You can set attributes (like socket_host and socket_port) You can set attributes (like socket_host and socket_port)
@ -28,26 +27,26 @@ class Server(ServerAdapter):
_socket_host = '127.0.0.1' _socket_host = '127.0.0.1'
def _get_socket_host(self): @property
return self._socket_host def socket_host(self): # noqa: D401; irrelevant for properties
"""The hostname or IP address on which to listen for connections.
def _set_socket_host(self, value):
if value == '':
raise ValueError("The empty string ('') is not an allowed value. "
"Use '0.0.0.0' instead to listen on all active "
"interfaces (INADDR_ANY).")
self._socket_host = value
socket_host = property(
_get_socket_host,
_set_socket_host,
doc="""The hostname or IP address on which to listen for connections.
Host values may be any IPv4 or IPv6 address, or any valid hostname. Host values may be any IPv4 or IPv6 address, or any valid hostname.
The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if
your hosts file prefers IPv6). The string '0.0.0.0' is a special your hosts file prefers IPv6). The string '0.0.0.0' is a special
IPv4 entry meaning "any active interface" (INADDR_ANY), and '::' IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
is the similar IN6ADDR_ANY for IPv6. The empty string or None are is the similar IN6ADDR_ANY for IPv6. The empty string or None are
not allowed.""") not allowed.
"""
return self._socket_host
@socket_host.setter
def socket_host(self, value):
if value == '':
raise ValueError("The empty string ('') is not an allowed value. "
"Use '0.0.0.0' instead to listen on all active "
'interfaces (INADDR_ANY).')
self._socket_host = value
socket_file = None socket_file = None
"""If given, the name of the UNIX socket to use instead of TCP/IP. """If given, the name of the UNIX socket to use instead of TCP/IP.
@ -61,11 +60,11 @@ class Server(ServerAdapter):
socket_timeout = 10 socket_timeout = 10
"""The timeout in seconds for accepted connections (default 10).""" """The timeout in seconds for accepted connections (default 10)."""
accepted_queue_size = -1 accepted_queue_size = -1
"""The maximum number of requests which will be queued up before """The maximum number of requests which will be queued up before
the server refuses to accept it (default -1, meaning no limit).""" the server refuses to accept it (default -1, meaning no limit)."""
accepted_queue_timeout = 10 accepted_queue_timeout = 10
"""The timeout in seconds for attempting to add a request to the """The timeout in seconds for attempting to add a request to the
queue when the queue is full (default 10).""" queue when the queue is full (default 10)."""
@ -96,7 +95,8 @@ class Server(ServerAdapter):
instance = None instance = None
"""If not None, this should be an HTTP server instance (such as """If not None, this should be an HTTP server instance (such as
CPWSGIServer) which cherrypy.server will control. Use this when you need cheroot.wsgi.Server) which cherrypy.server will control.
Use this when you need
more control over object instantiation than is available in the various more control over object instantiation than is available in the various
configuration options.""" configuration options."""
@ -113,20 +113,23 @@ class Server(ServerAdapter):
ssl_private_key = None ssl_private_key = None
"""The filename of the private key to use with SSL.""" """The filename of the private key to use with SSL."""
if py3k: ssl_ciphers = None
"""The ciphers list of SSL."""
if six.PY3:
ssl_module = 'builtin' ssl_module = 'builtin'
"""The name of a registered SSL adaptation module to use with """The name of a registered SSL adaptation module to use with
the builtin WSGI server. Builtin options are: 'builtin' (to the builtin WSGI server. Builtin options are: 'builtin' (to
use the SSL library built into recent versions of Python). use the SSL library built into recent versions of Python).
You may also register your own classes in the You may also register your own classes in the
wsgiserver.ssl_adapters dict.""" cheroot.server.ssl_adapters dict."""
else: else:
ssl_module = 'pyopenssl' ssl_module = 'pyopenssl'
"""The name of a registered SSL adaptation module to use with the """The name of a registered SSL adaptation module to use with the
builtin WSGI server. Builtin options are 'builtin' (to use the SSL builtin WSGI server. Builtin options are 'builtin' (to use the SSL
library built into recent versions of Python) and 'pyopenssl' (to library built into recent versions of Python) and 'pyopenssl' (to
use the PyOpenSSL project, which you must install separately). You use the PyOpenSSL project, which you must install separately). You
may also register your own classes in the wsgiserver.ssl_adapters may also register your own classes in the cheroot.server.ssl_adapters
dict.""" dict."""
statistics = False statistics = False
@ -141,9 +144,29 @@ class Server(ServerAdapter):
which declares it covers WSGI version 1.0.1 but still mandates the which declares it covers WSGI version 1.0.1 but still mandates the
wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. wsgi.version (1, 0)] and ('u', 0), an experimental unicode version.
You may create and register your own experimental versions of the WSGI You may create and register your own experimental versions of the WSGI
protocol by adding custom classes to the wsgiserver.wsgi_gateways dict.""" protocol by adding custom classes to the cheroot.server.wsgi_gateways dict.
"""
peercreds = False
"""If True, peer cred lookup for UNIX domain socket will put to WSGI env.
This information will then be available through WSGI env vars:
* X_REMOTE_PID
* X_REMOTE_UID
* X_REMOTE_GID
"""
peercreds_resolve = False
"""If True, username/group will be looked up in the OS from peercreds.
This information will then be available through WSGI env vars:
* REMOTE_USER
* X_REMOTE_USER
* X_REMOTE_GROUP
"""
def __init__(self): def __init__(self):
"""Initialize Server instance."""
self.bus = cherrypy.engine self.bus = cherrypy.engine
self.httpserver = None self.httpserver = None
self.interrupt = None self.interrupt = None
@ -156,7 +179,7 @@ class Server(ServerAdapter):
if httpserver is None: if httpserver is None:
from cherrypy import _cpwsgi_server from cherrypy import _cpwsgi_server
httpserver = _cpwsgi_server.CPWSGIServer(self) httpserver = _cpwsgi_server.CPWSGIServer(self)
if isinstance(httpserver, basestring): if isinstance(httpserver, text_or_bytes):
# Is anyone using this? Can I add an arg? # Is anyone using this? Can I add an arg?
httpserver = attributes(httpserver)(self) httpserver = attributes(httpserver)(self)
return httpserver, self.bind_addr return httpserver, self.bind_addr
@ -165,22 +188,28 @@ class Server(ServerAdapter):
"""Start the HTTP server.""" """Start the HTTP server."""
if not self.httpserver: if not self.httpserver:
self.httpserver, self.bind_addr = self.httpserver_from_self() self.httpserver, self.bind_addr = self.httpserver_from_self()
ServerAdapter.start(self) super(Server, self).start()
start.priority = 75 start.priority = 75
def _get_bind_addr(self): @property
def bind_addr(self):
"""Return bind address.
A (host, port) tuple for TCP sockets or a str for Unix domain sockts.
"""
if self.socket_file: if self.socket_file:
return self.socket_file return self.socket_file
if self.socket_host is None and self.socket_port is None: if self.socket_host is None and self.socket_port is None:
return None return None
return (self.socket_host, self.socket_port) return (self.socket_host, self.socket_port)
def _set_bind_addr(self, value): @bind_addr.setter
def bind_addr(self, value):
if value is None: if value is None:
self.socket_file = None self.socket_file = None
self.socket_host = None self.socket_host = None
self.socket_port = None self.socket_port = None
elif isinstance(value, basestring): elif isinstance(value, text_or_bytes):
self.socket_file = value self.socket_file = value
self.socket_host = None self.socket_host = None
self.socket_port = None self.socket_port = None
@ -189,17 +218,14 @@ class Server(ServerAdapter):
self.socket_host, self.socket_port = value self.socket_host, self.socket_port = value
self.socket_file = None self.socket_file = None
except ValueError: except ValueError:
raise ValueError("bind_addr must be a (host, port) tuple " raise ValueError('bind_addr must be a (host, port) tuple '
"(for TCP sockets) or a string (for Unix " '(for TCP sockets) or a string (for Unix '
"domain sockets), not %r" % value) 'domain sockets), not %r' % value)
bind_addr = property(
_get_bind_addr,
_set_bind_addr,
doc='A (host, port) tuple for TCP sockets or '
'a str for Unix domain sockets.')
def base(self): def base(self):
"""Return the base (scheme://host[:port] or sock file) for this server. """Return the base for this server.
e.i. scheme://host[:port] or sock file
""" """
if self.socket_file: if self.socket_file:
return self.socket_file return self.socket_file
@ -215,12 +241,12 @@ class Server(ServerAdapter):
port = self.socket_port port = self.socket_port
if self.ssl_certificate: if self.ssl_certificate:
scheme = "https" scheme = 'https'
if port != 443: if port != 443:
host += ":%s" % port host += ':%s' % port
else: else:
scheme = "http" scheme = 'http'
if port != 80: if port != 80:
host += ":%s" % port host += ':%s' % port
return "%s://%s" % (scheme, host) return '%s://%s' % (scheme, host)

View file

@ -1,241 +0,0 @@
# This is a backport of Python-2.4's threading.local() implementation
"""Thread-local objects
(Note that this module provides a Python version of thread
threading.local class. Depending on the version of Python you're
using, there may be a faster one available. You should always import
the local class from threading.)
Thread-local objects support the management of thread-local data.
If you have data that you want to be local to a thread, simply create
a thread-local object and use its attributes:
>>> mydata = local()
>>> mydata.number = 42
>>> mydata.number
42
You can also access the local-object's dictionary:
>>> mydata.__dict__
{'number': 42}
>>> mydata.__dict__.setdefault('widgets', [])
[]
>>> mydata.widgets
[]
What's important about thread-local objects is that their data are
local to a thread. If we access the data in a different thread:
>>> log = []
>>> def f():
... items = mydata.__dict__.items()
... items.sort()
... log.append(items)
... mydata.number = 11
... log.append(mydata.number)
>>> import threading
>>> thread = threading.Thread(target=f)
>>> thread.start()
>>> thread.join()
>>> log
[[], 11]
we get different data. Furthermore, changes made in the other thread
don't affect data seen in this thread:
>>> mydata.number
42
Of course, values you get from a local object, including a __dict__
attribute, are for whatever thread was current at the time the
attribute was read. For that reason, you generally don't want to save
these values across threads, as they apply only to the thread they
came from.
You can create custom local objects by subclassing the local class:
>>> class MyLocal(local):
... number = 2
... initialized = False
... def __init__(self, **kw):
... if self.initialized:
... raise SystemError('__init__ called too many times')
... self.initialized = True
... self.__dict__.update(kw)
... def squared(self):
... return self.number ** 2
This can be useful to support default values, methods and
initialization. Note that if you define an __init__ method, it will be
called each time the local object is used in a separate thread. This
is necessary to initialize each thread's dictionary.
Now if we create a local object:
>>> mydata = MyLocal(color='red')
Now we have a default number:
>>> mydata.number
2
an initial color:
>>> mydata.color
'red'
>>> del mydata.color
And a method that operates on the data:
>>> mydata.squared()
4
As before, we can access the data in a separate thread:
>>> log = []
>>> thread = threading.Thread(target=f)
>>> thread.start()
>>> thread.join()
>>> log
[[('color', 'red'), ('initialized', True)], 11]
without affecting this thread's data:
>>> mydata.number
2
>>> mydata.color
Traceback (most recent call last):
...
AttributeError: 'MyLocal' object has no attribute 'color'
Note that subclasses can define slots, but they are not thread
local. They are shared across threads:
>>> class MyLocal(local):
... __slots__ = 'number'
>>> mydata = MyLocal()
>>> mydata.number = 42
>>> mydata.color = 'red'
So, the separate thread:
>>> thread = threading.Thread(target=f)
>>> thread.start()
>>> thread.join()
affects what we see:
>>> mydata.number
11
>>> del mydata
"""
# Threading import is at end
class _localbase(object):
__slots__ = '_local__key', '_local__args', '_local__lock'
def __new__(cls, *args, **kw):
self = object.__new__(cls)
key = 'thread.local.' + str(id(self))
object.__setattr__(self, '_local__key', key)
object.__setattr__(self, '_local__args', (args, kw))
object.__setattr__(self, '_local__lock', RLock())
if args or kw and (cls.__init__ is object.__init__):
raise TypeError("Initialization arguments are not supported")
# We need to create the thread dict in anticipation of
# __init__ being called, to make sure we don't call it
# again ourselves.
dict = object.__getattribute__(self, '__dict__')
currentThread().__dict__[key] = dict
return self
def _patch(self):
key = object.__getattribute__(self, '_local__key')
d = currentThread().__dict__.get(key)
if d is None:
d = {}
currentThread().__dict__[key] = d
object.__setattr__(self, '__dict__', d)
# we have a new instance dict, so call out __init__ if we have
# one
cls = type(self)
if cls.__init__ is not object.__init__:
args, kw = object.__getattribute__(self, '_local__args')
cls.__init__(self, *args, **kw)
else:
object.__setattr__(self, '__dict__', d)
class local(_localbase):
def __getattribute__(self, name):
lock = object.__getattribute__(self, '_local__lock')
lock.acquire()
try:
_patch(self)
return object.__getattribute__(self, name)
finally:
lock.release()
def __setattr__(self, name, value):
lock = object.__getattribute__(self, '_local__lock')
lock.acquire()
try:
_patch(self)
return object.__setattr__(self, name, value)
finally:
lock.release()
def __delattr__(self, name):
lock = object.__getattribute__(self, '_local__lock')
lock.acquire()
try:
_patch(self)
return object.__delattr__(self, name)
finally:
lock.release()
def __del__():
threading_enumerate = enumerate
__getattribute__ = object.__getattribute__
def __del__(self):
key = __getattribute__(self, '_local__key')
try:
threads = list(threading_enumerate())
except:
# if enumerate fails, as it seems to do during
# shutdown, we'll skip cleanup under the assumption
# that there is nothing to clean up
return
for thread in threads:
try:
__dict__ = thread.__dict__
except AttributeError:
# Thread is dying, rest in peace
continue
if key in __dict__:
try:
del __dict__[key]
except KeyError:
pass # didn't have anything in this thread
return __del__
__del__ = __del__()
from threading import currentThread, enumerate, RLock

View file

@ -22,17 +22,22 @@ Tools may be implemented as any object with a namespace. The builtins
are generally either modules or instances of the tools.Tool class. are generally either modules or instances of the tools.Tool class.
""" """
import sys import six
import warnings
import cherrypy import cherrypy
from cherrypy._helper import expose
from cherrypy.lib import cptools, encoding, static, jsontools
from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc
from cherrypy.lib import caching as _caching
from cherrypy.lib import auth_basic, auth_digest
def _getargs(func): def _getargs(func):
"""Return the names of all static arguments to the given function.""" """Return the names of all static arguments to the given function."""
# Use this instead of importing inspect for less mem overhead. # Use this instead of importing inspect for less mem overhead.
import types import types
if sys.version_info >= (3, 0): if six.PY3:
if isinstance(func, types.MethodType): if isinstance(func, types.MethodType):
func = func.__func__ func = func.__func__
co = func.__code__ co = func.__code__
@ -44,8 +49,8 @@ def _getargs(func):
_attr_error = ( _attr_error = (
"CherryPy Tools cannot be turned on directly. Instead, turn them " 'CherryPy Tools cannot be turned on directly. Instead, turn them '
"on via config, or use them as decorators on your page handlers." 'on via config, or use them as decorators on your page handlers.'
) )
@ -56,7 +61,7 @@ class Tool(object):
help(tool.callable) should give you more information about this Tool. help(tool.callable) should give you more information about this Tool.
""" """
namespace = "tools" namespace = 'tools'
def __init__(self, point, callable, name=None, priority=50): def __init__(self, point, callable, name=None, priority=50):
self._point = point self._point = point
@ -66,12 +71,13 @@ class Tool(object):
self.__doc__ = self.callable.__doc__ self.__doc__ = self.callable.__doc__
self._setargs() self._setargs()
def _get_on(self): @property
def on(self):
raise AttributeError(_attr_error) raise AttributeError(_attr_error)
def _set_on(self, value): @on.setter
def on(self, value):
raise AttributeError(_attr_error) raise AttributeError(_attr_error)
on = property(_get_on, _set_on)
def _setargs(self): def _setargs(self):
"""Copy func parameter names to obj attributes.""" """Copy func parameter names to obj attributes."""
@ -79,7 +85,7 @@ class Tool(object):
for arg in _getargs(self.callable): for arg in _getargs(self.callable):
setattr(self, arg, None) setattr(self, arg, None)
except (TypeError, AttributeError): except (TypeError, AttributeError):
if hasattr(self.callable, "__call__"): if hasattr(self.callable, '__call__'):
for arg in _getargs(self.callable.__call__): for arg in _getargs(self.callable.__call__):
setattr(self, arg, None) setattr(self, arg, None)
# IronPython 1.0 raises NotImplementedError because # IronPython 1.0 raises NotImplementedError because
@ -103,8 +109,8 @@ class Tool(object):
if self._name in tm: if self._name in tm:
conf.update(tm[self._name]) conf.update(tm[self._name])
if "on" in conf: if 'on' in conf:
del conf["on"] del conf['on']
return conf return conf
@ -113,21 +119,21 @@ class Tool(object):
For example:: For example::
@expose
@tools.proxy() @tools.proxy()
def whats_my_base(self): def whats_my_base(self):
return cherrypy.request.base return cherrypy.request.base
whats_my_base.exposed = True
""" """
if args: if args:
raise TypeError("The %r Tool does not accept positional " raise TypeError('The %r Tool does not accept positional '
"arguments; you must use keyword arguments." 'arguments; you must use keyword arguments.'
% self._name) % self._name)
def tool_decorator(f): def tool_decorator(f):
if not hasattr(f, "_cp_config"): if not hasattr(f, '_cp_config'):
f._cp_config = {} f._cp_config = {}
subspace = self.namespace + "." + self._name + "." subspace = self.namespace + '.' + self._name + '.'
f._cp_config[subspace + "on"] = True f._cp_config[subspace + 'on'] = True
for k, v in kwargs.items(): for k, v in kwargs.items():
f._cp_config[subspace + k] = v f._cp_config[subspace + k] = v
return f return f
@ -140,9 +146,9 @@ class Tool(object):
method when the tool is "turned on" in config. method when the tool is "turned on" in config.
""" """
conf = self._merged_args() conf = self._merged_args()
p = conf.pop("priority", None) p = conf.pop('priority', None)
if p is None: if p is None:
p = getattr(self.callable, "priority", self._priority) p = getattr(self.callable, 'priority', self._priority)
cherrypy.serving.request.hooks.attach(self._point, self.callable, cherrypy.serving.request.hooks.attach(self._point, self.callable,
priority=p, **conf) priority=p, **conf)
@ -171,12 +177,12 @@ class HandlerTool(Tool):
nav = tools.staticdir.handler(section="/nav", dir="nav", nav = tools.staticdir.handler(section="/nav", dir="nav",
root=absDir) root=absDir)
""" """
@expose
def handle_func(*a, **kw): def handle_func(*a, **kw):
handled = self.callable(*args, **self._merged_args(kwargs)) handled = self.callable(*args, **self._merged_args(kwargs))
if not handled: if not handled:
raise cherrypy.NotFound() raise cherrypy.NotFound()
return cherrypy.serving.response.body return cherrypy.serving.response.body
handle_func.exposed = True
return handle_func return handle_func
def _wrapper(self, **kwargs): def _wrapper(self, **kwargs):
@ -190,9 +196,9 @@ class HandlerTool(Tool):
method when the tool is "turned on" in config. method when the tool is "turned on" in config.
""" """
conf = self._merged_args() conf = self._merged_args()
p = conf.pop("priority", None) p = conf.pop('priority', None)
if p is None: if p is None:
p = getattr(self.callable, "priority", self._priority) p = getattr(self.callable, 'priority', self._priority)
cherrypy.serving.request.hooks.attach(self._point, self._wrapper, cherrypy.serving.request.hooks.attach(self._point, self._wrapper,
priority=p, **conf) priority=p, **conf)
@ -253,11 +259,6 @@ class ErrorTool(Tool):
# Builtin tools # # Builtin tools #
from cherrypy.lib import cptools, encoding, auth, static, jsontools
from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc
from cherrypy.lib import caching as _caching
from cherrypy.lib import auth_basic, auth_digest
class SessionTool(Tool): class SessionTool(Tool):
@ -271,7 +272,7 @@ class SessionTool(Tool):
body. This is off by default for safety reasons; for example, body. This is off by default for safety reasons; for example,
a large upload would block the session, denying an AJAX a large upload would block the session, denying an AJAX
progress meter progress meter
(`issue <https://bitbucket.org/cherrypy/cherrypy/issue/630>`_). (`issue <https://github.com/cherrypy/cherrypy/issues/630>`_).
When 'explicit' (or any other value), you need to call When 'explicit' (or any other value), you need to call
cherrypy.session.acquire_lock() yourself before using cherrypy.session.acquire_lock() yourself before using
@ -295,9 +296,9 @@ class SessionTool(Tool):
conf = self._merged_args() conf = self._merged_args()
p = conf.pop("priority", None) p = conf.pop('priority', None)
if p is None: if p is None:
p = getattr(self.callable, "priority", self._priority) p = getattr(self.callable, 'priority', self._priority)
hooks.attach(self._point, self.callable, priority=p, **conf) hooks.attach(self._point, self.callable, priority=p, **conf)
@ -321,9 +322,12 @@ class SessionTool(Tool):
sess.regenerate() sess.regenerate()
# Grab cookie-relevant tool args # Grab cookie-relevant tool args
conf = dict([(k, v) for k, v in self._merged_args().items() relevant = 'path', 'path_header', 'name', 'timeout', 'domain', 'secure'
if k in ('path', 'path_header', 'name', 'timeout', conf = dict(
'domain', 'secure')]) (k, v)
for k, v in self._merged_args().items()
if k in relevant
)
_sessions.set_response_cookie(**conf) _sessions.set_response_cookie(**conf)
@ -365,6 +369,7 @@ class XMLRPCController(object):
# would be if someone actually disabled the default_toolbox. Meh. # would be if someone actually disabled the default_toolbox. Meh.
_cp_config = {'tools.xmlrpc.on': True} _cp_config = {'tools.xmlrpc.on': True}
@expose
def default(self, *vpath, **params): def default(self, *vpath, **params):
rpcparams, rpcmethod = _xmlrpc.process_body() rpcparams, rpcmethod = _xmlrpc.process_body()
@ -372,30 +377,25 @@ class XMLRPCController(object):
for attr in str(rpcmethod).split('.'): for attr in str(rpcmethod).split('.'):
subhandler = getattr(subhandler, attr, None) subhandler = getattr(subhandler, attr, None)
if subhandler and getattr(subhandler, "exposed", False): if subhandler and getattr(subhandler, 'exposed', False):
body = subhandler(*(vpath + rpcparams), **params) body = subhandler(*(vpath + rpcparams), **params)
else: else:
# https://bitbucket.org/cherrypy/cherrypy/issue/533 # https://github.com/cherrypy/cherrypy/issues/533
# if a method is not found, an xmlrpclib.Fault should be returned # if a method is not found, an xmlrpclib.Fault should be returned
# raising an exception here will do that; see # raising an exception here will do that; see
# cherrypy.lib.xmlrpcutil.on_error # cherrypy.lib.xmlrpcutil.on_error
raise Exception('method "%s" is not supported' % attr) raise Exception('method "%s" is not supported' % attr)
conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {}) conf = cherrypy.serving.request.toolmaps['tools'].get('xmlrpc', {})
_xmlrpc.respond(body, _xmlrpc.respond(body,
conf.get('encoding', 'utf-8'), conf.get('encoding', 'utf-8'),
conf.get('allow_none', 0)) conf.get('allow_none', 0))
return cherrypy.serving.response.body return cherrypy.serving.response.body
default.exposed = True
class SessionAuthTool(HandlerTool): class SessionAuthTool(HandlerTool):
pass
def _setargs(self):
for name in dir(cptools.SessionAuth):
if not name.startswith("__"):
setattr(self, name, None)
class CachingTool(Tool): class CachingTool(Tool):
@ -410,14 +410,14 @@ class CachingTool(Tool):
if request.cacheable: if request.cacheable:
# Note the devious technique here of adding hooks on the fly # Note the devious technique here of adding hooks on the fly
request.hooks.attach('before_finalize', _caching.tee_output, request.hooks.attach('before_finalize', _caching.tee_output,
priority=90) priority=100)
_wrapper.priority = 20 _wrapper.priority = 90
def _setup(self): def _setup(self):
"""Hook caching into cherrypy.request.""" """Hook caching into cherrypy.request."""
conf = self._merged_args() conf = self._merged_args()
p = conf.pop("priority", None) p = conf.pop('priority', None)
cherrypy.serving.request.hooks.attach('before_handler', self._wrapper, cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
priority=p, **conf) priority=p, **conf)
@ -446,7 +446,7 @@ class Toolbox(object):
cherrypy.serving.request.toolmaps[self.namespace] = map = {} cherrypy.serving.request.toolmaps[self.namespace] = map = {}
def populate(k, v): def populate(k, v):
toolname, arg = k.split(".", 1) toolname, arg = k.split('.', 1)
bucket = map.setdefault(toolname, {}) bucket = map.setdefault(toolname, {})
bucket[arg] = v bucket[arg] = v
return populate return populate
@ -456,33 +456,24 @@ class Toolbox(object):
map = cherrypy.serving.request.toolmaps.get(self.namespace) map = cherrypy.serving.request.toolmaps.get(self.namespace)
if map: if map:
for name, settings in map.items(): for name, settings in map.items():
if settings.get("on", False): if settings.get('on', False):
tool = getattr(self, name) tool = getattr(self, name)
tool._setup() tool._setup()
def register(self, point, **kwargs):
class DeprecatedTool(Tool): """
Return a decorator which registers the function
_name = None at the given hook point.
warnmsg = "This Tool is deprecated." """
def decorator(func):
def __init__(self, point, warnmsg=None): attr_name = kwargs.get('name', func.__name__)
self.point = point tool = Tool(point, func, **kwargs)
if warnmsg is not None: setattr(self, attr_name, tool)
self.warnmsg = warnmsg return func
return decorator
def __call__(self, *args, **kwargs):
warnings.warn(self.warnmsg)
def tool_decorator(f):
return f
return tool_decorator
def _setup(self):
warnings.warn(self.warnmsg)
default_toolbox = _d = Toolbox("tools") default_toolbox = _d = Toolbox('tools')
_d.session_auth = SessionAuthTool(cptools.session_auth) _d.session_auth = SessionAuthTool(cptools.session_auth)
_d.allow = Tool('on_start_resource', cptools.allow) _d.allow = Tool('on_start_resource', cptools.allow)
_d.proxy = Tool('before_request_body', cptools.proxy, priority=30) _d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
@ -502,20 +493,8 @@ _d.sessions = SessionTool()
_d.xmlrpc = ErrorTool(_xmlrpc.on_error) _d.xmlrpc = ErrorTool(_xmlrpc.on_error)
_d.caching = CachingTool('before_handler', _caching.get, 'caching') _d.caching = CachingTool('before_handler', _caching.get, 'caching')
_d.expires = Tool('before_finalize', _caching.expires) _d.expires = Tool('before_finalize', _caching.expires)
_d.tidy = DeprecatedTool(
'before_finalize',
"The tidy tool has been removed from the standard distribution of "
"CherryPy. The most recent version can be found at "
"http://tools.cherrypy.org/browser.")
_d.nsgmls = DeprecatedTool(
'before_finalize',
"The nsgmls tool has been removed from the standard distribution of "
"CherryPy. The most recent version can be found at "
"http://tools.cherrypy.org/browser.")
_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers) _d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
_d.referer = Tool('before_request_body', cptools.referer) _d.referer = Tool('before_request_body', cptools.referer)
_d.basic_auth = Tool('on_start_resource', auth.basic_auth)
_d.digest_auth = Tool('on_start_resource', auth.digest_auth)
_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60) _d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60)
_d.flatten = Tool('before_finalize', cptools.flatten) _d.flatten = Tool('before_finalize', cptools.flatten)
_d.accept = Tool('on_start_resource', cptools.accept) _d.accept = Tool('on_start_resource', cptools.accept)
@ -525,5 +504,6 @@ _d.json_in = Tool('before_request_body', jsontools.json_in, priority=30)
_d.json_out = Tool('before_handler', jsontools.json_out, priority=30) _d.json_out = Tool('before_handler', jsontools.json_out, priority=30)
_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1) _d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1)
_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1) _d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1)
_d.params = Tool('before_handler', cptools.convert_params, priority=15)
del _d, cptools, encoding, auth, static del _d, cptools, encoding, static

View file

@ -2,14 +2,15 @@
import os import os
import six
import cherrypy import cherrypy
from cherrypy._cpcompat import ntou, py3k from cherrypy._cpcompat import ntou
from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
from cherrypy.lib import httputil from cherrypy.lib import httputil, reprconf
class Application(object): class Application(object):
"""A CherryPy Application. """A CherryPy Application.
Servers and gateways should not instantiate Request objects directly. Servers and gateways should not instantiate Request objects directly.
@ -30,7 +31,7 @@ class Application(object):
"""A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
of {key: value} pairs.""" of {key: value} pairs."""
namespaces = _cpconfig.NamespaceSet() namespaces = reprconf.NamespaceSet()
toolboxes = {'tools': cherrypy.tools} toolboxes = {'tools': cherrypy.tools}
log = None log = None
@ -44,22 +45,24 @@ class Application(object):
relative_urls = False relative_urls = False
def __init__(self, root, script_name="", config=None): def __init__(self, root, script_name='', config=None):
"""Initialize Application with given root."""
self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root) self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
self.root = root self.root = root
self.script_name = script_name self.script_name = script_name
self.wsgiapp = _cpwsgi.CPWSGIApp(self) self.wsgiapp = _cpwsgi.CPWSGIApp(self)
self.namespaces = self.namespaces.copy() self.namespaces = self.namespaces.copy()
self.namespaces["log"] = lambda k, v: setattr(self.log, k, v) self.namespaces['log'] = lambda k, v: setattr(self.log, k, v)
self.namespaces["wsgi"] = self.wsgiapp.namespace_handler self.namespaces['wsgi'] = self.wsgiapp.namespace_handler
self.config = self.__class__.config.copy() self.config = self.__class__.config.copy()
if config: if config:
self.merge(config) self.merge(config)
def __repr__(self): def __repr__(self):
return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__, """Generate a representation of the Application instance."""
return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__,
self.root, self.script_name) self.root, self.script_name)
script_name_doc = """The URI "mount point" for this app. A mount point script_name_doc = """The URI "mount point" for this app. A mount point
@ -78,42 +81,58 @@ class Application(object):
provided for each call from request.wsgi_environ['SCRIPT_NAME']. provided for each call from request.wsgi_environ['SCRIPT_NAME'].
""" """
def _get_script_name(self): @property
def script_name(self): # noqa: D401; irrelevant for properties
"""The URI "mount point" for this app.
A mount point is that portion of the URI which is constant for all URIs
that are serviced by this application; it does not include scheme,
host, or proxy ("virtual host") portions of the URI.
For example, if script_name is "/my/cool/app", then the URL
"http://www.example.com/my/cool/app/page1" might be handled by a
"page1" method on the root object.
The value of script_name MUST NOT end in a slash. If the script_name
refers to the root of the URI, it MUST be an empty string (not "/").
If script_name is explicitly set to None, then the script_name will be
provided for each call from request.wsgi_environ['SCRIPT_NAME'].
"""
if self._script_name is not None: if self._script_name is not None:
return self._script_name return self._script_name
# A `_script_name` with a value of None signals that the script name # A `_script_name` with a value of None signals that the script name
# should be pulled from WSGI environ. # should be pulled from WSGI environ.
return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/") return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/')
def _set_script_name(self, value): @script_name.setter
def script_name(self, value):
if value: if value:
value = value.rstrip("/") value = value.rstrip('/')
self._script_name = value self._script_name = value
script_name = property(fget=_get_script_name, fset=_set_script_name,
doc=script_name_doc)
def merge(self, config): def merge(self, config):
"""Merge the given config into self.config.""" """Merge the given config into self.config."""
_cpconfig.merge(self.config, config) _cpconfig.merge(self.config, config)
# Handle namespaces specified in config. # Handle namespaces specified in config.
self.namespaces(self.config.get("/", {})) self.namespaces(self.config.get('/', {}))
def find_config(self, path, key, default=None): def find_config(self, path, key, default=None):
"""Return the most-specific value for key along path, or default.""" """Return the most-specific value for key along path, or default."""
trail = path or "/" trail = path or '/'
while trail: while trail:
nodeconf = self.config.get(trail, {}) nodeconf = self.config.get(trail, {})
if key in nodeconf: if key in nodeconf:
return nodeconf[key] return nodeconf[key]
lastslash = trail.rfind("/") lastslash = trail.rfind('/')
if lastslash == -1: if lastslash == -1:
break break
elif lastslash == 0 and trail != "/": elif lastslash == 0 and trail != '/':
trail = "/" trail = '/'
else: else:
trail = trail[:lastslash] trail = trail[:lastslash]
@ -142,17 +161,17 @@ class Application(object):
try: try:
req.close() req.close()
except: except Exception:
cherrypy.log(traceback=True, severity=40) cherrypy.log(traceback=True, severity=40)
cherrypy.serving.clear() cherrypy.serving.clear()
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
"""Call a WSGI-callable."""
return self.wsgiapp(environ, start_response) return self.wsgiapp(environ, start_response)
class Tree(object): class Tree(object):
"""A registry of CherryPy applications, mounted at diverse points. """A registry of CherryPy applications, mounted at diverse points.
An instance of this class may also be used as a WSGI callable An instance of this class may also be used as a WSGI callable
@ -168,9 +187,10 @@ class Tree(object):
WSGI callable if you happen to be using a WSGI server).""" WSGI callable if you happen to be using a WSGI server)."""
def __init__(self): def __init__(self):
"""Initialize registry Tree."""
self.apps = {} self.apps = {}
def mount(self, root, script_name="", config=None): def mount(self, root, script_name='', config=None):
"""Mount a new app from a root object, script_name, and config. """Mount a new app from a root object, script_name, and config.
root root
@ -195,29 +215,36 @@ class Tree(object):
if script_name is None: if script_name is None:
raise TypeError( raise TypeError(
"The 'script_name' argument may not be None. Application " "The 'script_name' argument may not be None. Application "
"objects may, however, possess a script_name of None (in " 'objects may, however, possess a script_name of None (in '
"order to inpect the WSGI environ for SCRIPT_NAME upon each " 'order to inpect the WSGI environ for SCRIPT_NAME upon each '
"request). You cannot mount such Applications on this Tree; " 'request). You cannot mount such Applications on this Tree; '
"you must pass them to a WSGI server interface directly.") 'you must pass them to a WSGI server interface directly.')
# Next line both 1) strips trailing slash and 2) maps "/" -> "". # Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip("/") script_name = script_name.rstrip('/')
if isinstance(root, Application): if isinstance(root, Application):
app = root app = root
if script_name != "" and script_name != app.script_name: if script_name != '' and script_name != app.script_name:
raise ValueError( raise ValueError(
"Cannot specify a different script name and pass an " 'Cannot specify a different script name and pass an '
"Application instance to cherrypy.mount") 'Application instance to cherrypy.mount')
script_name = app.script_name script_name = app.script_name
else: else:
app = Application(root, script_name) app = Application(root, script_name)
# If mounted at "", add favicon.ico # If mounted at "", add favicon.ico
if (script_name == "" and root is not None needs_favicon = (
and not hasattr(root, "favicon_ico")): script_name == ''
favicon = os.path.join(os.getcwd(), os.path.dirname(__file__), and root is not None
"favicon.ico") and not hasattr(root, 'favicon_ico')
)
if needs_favicon:
favicon = os.path.join(
os.getcwd(),
os.path.dirname(__file__),
'favicon.ico',
)
root.favicon_ico = tools.staticfile.handler(favicon) root.favicon_ico = tools.staticfile.handler(favicon)
if config: if config:
@ -227,14 +254,14 @@ class Tree(object):
return app return app
def graft(self, wsgi_callable, script_name=""): def graft(self, wsgi_callable, script_name=''):
"""Mount a wsgi callable at the given script_name.""" """Mount a wsgi callable at the given script_name."""
# Next line both 1) strips trailing slash and 2) maps "/" -> "". # Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip("/") script_name = script_name.rstrip('/')
self.apps[script_name] = wsgi_callable self.apps[script_name] = wsgi_callable
def script_name(self, path=None): def script_name(self, path=None):
"""The script_name of the app at the given path, or None. """Return the script_name of the app at the given path, or None.
If path is None, cherrypy.request is used. If path is None, cherrypy.request is used.
""" """
@ -250,22 +277,23 @@ class Tree(object):
if path in self.apps: if path in self.apps:
return path return path
if path == "": if path == '':
return None return None
# Move one node up the tree and try again. # Move one node up the tree and try again.
path = path[:path.rfind("/")] path = path[:path.rfind('/')]
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
"""Pre-initialize WSGI env and call WSGI-callable."""
# If you're calling this, then you're probably setting SCRIPT_NAME # If you're calling this, then you're probably setting SCRIPT_NAME
# to '' (some WSGI servers always set SCRIPT_NAME to ''). # to '' (some WSGI servers always set SCRIPT_NAME to '').
# Try to look up the app using the full path. # Try to look up the app using the full path.
env1x = environ env1x = environ
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ) env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ)
path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
env1x.get('PATH_INFO', '')) env1x.get('PATH_INFO', ''))
sn = self.script_name(path or "/") sn = self.script_name(path or '/')
if sn is None: if sn is None:
start_response('404 Not Found', []) start_response('404 Not Found', [])
return [] return []
@ -274,26 +302,12 @@ class Tree(object):
# Correct the SCRIPT_NAME and PATH_INFO environ entries. # Correct the SCRIPT_NAME and PATH_INFO environ entries.
environ = environ.copy() environ = environ.copy()
if not py3k: if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): # Python 2/WSGI u.0: all strings MUST be of type unicode
# Python 2/WSGI u.0: all strings MUST be of type unicode enc = environ[ntou('wsgi.url_encoding')]
enc = environ[ntou('wsgi.url_encoding')] environ[ntou('SCRIPT_NAME')] = sn.decode(enc)
environ[ntou('SCRIPT_NAME')] = sn.decode(enc) environ[ntou('PATH_INFO')] = path[len(sn.rstrip('/')):].decode(enc)
environ[ntou('PATH_INFO')] = path[
len(sn.rstrip("/")):].decode(enc)
else:
# Python 2/WSGI 1.x: all strings MUST be of type str
environ['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
else: else:
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): environ['SCRIPT_NAME'] = sn
# Python 3/WSGI u.0: all strings MUST be full unicode environ['PATH_INFO'] = path[len(sn.rstrip('/')):]
environ['SCRIPT_NAME'] = sn
environ['PATH_INFO'] = path[len(sn.rstrip("/")):]
else:
# Python 3/WSGI 1.x: all strings MUST be ISO-8859-1 str
environ['SCRIPT_NAME'] = sn.encode(
'utf-8').decode('ISO-8859-1')
environ['PATH_INFO'] = path[
len(sn.rstrip("/")):].encode('utf-8').decode('ISO-8859-1')
return app(environ, start_response) return app(environ, start_response)

View file

@ -8,13 +8,17 @@ still be translatable to bytes via the Latin-1 encoding!"
""" """
import sys as _sys import sys as _sys
import io
import six
import cherrypy as _cherrypy import cherrypy as _cherrypy
from cherrypy._cpcompat import BytesIO, bytestr, ntob, ntou, py3k, unicodestr from cherrypy._cpcompat import ntou
from cherrypy import _cperror from cherrypy import _cperror
from cherrypy.lib import httputil from cherrypy.lib import httputil
from cherrypy.lib import is_closable_iterator from cherrypy.lib import is_closable_iterator
def downgrade_wsgi_ux_to_1x(environ): def downgrade_wsgi_ux_to_1x(environ):
"""Return a new environ dict for WSGI 1.x from the given WSGI u.x environ. """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.
""" """
@ -24,7 +28,7 @@ def downgrade_wsgi_ux_to_1x(environ):
for k, v in list(environ.items()): for k, v in list(environ.items()):
if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]: if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
v = v.encode(url_encoding) v = v.encode(url_encoding)
elif isinstance(v, unicodestr): elif isinstance(v, six.text_type):
v = v.encode('ISO-8859-1') v = v.encode('ISO-8859-1')
env1x[k.encode('ISO-8859-1')] = v env1x[k.encode('ISO-8859-1')] = v
@ -43,10 +47,13 @@ class VirtualHost(object):
Domain2App = cherrypy.Application(root) Domain2App = cherrypy.Application(root)
SecureApp = cherrypy.Application(Secure()) SecureApp = cherrypy.Application(Secure())
vhost = cherrypy._cpwsgi.VirtualHost(RootApp, vhost = cherrypy._cpwsgi.VirtualHost(
domains={'www.domain2.example': Domain2App, RootApp,
'www.domain2.example:443': SecureApp, domains={
}) 'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp,
},
)
cherrypy.tree.graft(vhost) cherrypy.tree.graft(vhost)
""" """
@ -75,7 +82,7 @@ class VirtualHost(object):
def __call__(self, environ, start_response): def __call__(self, environ, start_response):
domain = environ.get('HTTP_HOST', '') domain = environ.get('HTTP_HOST', '')
if self.use_x_forwarded_host: if self.use_x_forwarded_host:
domain = environ.get("HTTP_X_FORWARDED_HOST", domain) domain = environ.get('HTTP_X_FORWARDED_HOST', domain)
nextapp = self.domains.get(domain) nextapp = self.domains.get(domain)
if nextapp is None: if nextapp is None:
@ -106,7 +113,7 @@ class InternalRedirector(object):
# Add the *previous* path_info + qs to redirections. # Add the *previous* path_info + qs to redirections.
old_uri = sn + path old_uri = sn + path
if qs: if qs:
old_uri += "?" + qs old_uri += '?' + qs
redirections.append(old_uri) redirections.append(old_uri)
if not self.recursive: if not self.recursive:
@ -114,18 +121,20 @@ class InternalRedirector(object):
# already # already
new_uri = sn + ir.path new_uri = sn + ir.path
if ir.query_string: if ir.query_string:
new_uri += "?" + ir.query_string new_uri += '?' + ir.query_string
if new_uri in redirections: if new_uri in redirections:
ir.request.close() ir.request.close()
raise RuntimeError("InternalRedirector visited the " tmpl = (
"same URL twice: %r" % new_uri) 'InternalRedirector visited the same URL twice: %r'
)
raise RuntimeError(tmpl % new_uri)
# Munge the environment and try again. # Munge the environment and try again.
environ['REQUEST_METHOD'] = "GET" environ['REQUEST_METHOD'] = 'GET'
environ['PATH_INFO'] = ir.path environ['PATH_INFO'] = ir.path
environ['QUERY_STRING'] = ir.query_string environ['QUERY_STRING'] = ir.query_string
environ['wsgi.input'] = BytesIO() environ['wsgi.input'] = io.BytesIO()
environ['CONTENT_LENGTH'] = "0" environ['CONTENT_LENGTH'] = '0'
environ['cherrypy.previous_request'] = ir.request environ['cherrypy.previous_request'] = ir.request
@ -157,19 +166,20 @@ class _TrappedResponse(object):
self.throws = throws self.throws = throws
self.started_response = False self.started_response = False
self.response = self.trap( self.response = self.trap(
self.nextapp, self.environ, self.start_response) self.nextapp, self.environ, self.start_response,
)
self.iter_response = iter(self.response) self.iter_response = iter(self.response)
def __iter__(self): def __iter__(self):
self.started_response = True self.started_response = True
return self return self
if py3k: def __next__(self):
def __next__(self): return self.trap(next, self.iter_response)
return self.trap(next, self.iter_response)
else: # todo: https://pythonhosted.org/six/#six.Iterator
def next(self): if six.PY2:
return self.trap(self.iter_response.next) next = __next__
def close(self): def close(self):
if hasattr(self.response, 'close'): if hasattr(self.response, 'close'):
@ -182,18 +192,19 @@ class _TrappedResponse(object):
raise raise
except StopIteration: except StopIteration:
raise raise
except: except Exception:
tb = _cperror.format_exc() tb = _cperror.format_exc()
#print('trapped (started %s):' % self.started_response, tb)
_cherrypy.log(tb, severity=40) _cherrypy.log(tb, severity=40)
if not _cherrypy.request.show_tracebacks: if not _cherrypy.request.show_tracebacks:
tb = "" tb = ''
s, h, b = _cperror.bare_error(tb) s, h, b = _cperror.bare_error(tb)
if py3k: if six.PY3:
# What fun. # What fun.
s = s.decode('ISO-8859-1') s = s.decode('ISO-8859-1')
h = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) h = [
for k, v in h] (k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
for k, v in h
]
if self.started_response: if self.started_response:
# Empty our iterable (so future calls raise StopIteration) # Empty our iterable (so future calls raise StopIteration)
self.iter_response = iter([]) self.iter_response = iter([])
@ -202,7 +213,7 @@ class _TrappedResponse(object):
try: try:
self.start_response(s, h, _sys.exc_info()) self.start_response(s, h, _sys.exc_info())
except: except Exception:
# "The application must not trap any exceptions raised by # "The application must not trap any exceptions raised by
# start_response, if it called start_response with exc_info. # start_response, if it called start_response with exc_info.
# Instead, it should allow such exceptions to propagate # Instead, it should allow such exceptions to propagate
@ -212,7 +223,7 @@ class _TrappedResponse(object):
raise raise
if self.started_response: if self.started_response:
return ntob("").join(b) return b''.join(b)
else: else:
return b return b
@ -227,7 +238,7 @@ class AppResponse(object):
def __init__(self, environ, start_response, cpapp): def __init__(self, environ, start_response, cpapp):
self.cpapp = cpapp self.cpapp = cpapp
try: try:
if not py3k: if six.PY2:
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
environ = downgrade_wsgi_ux_to_1x(environ) environ = downgrade_wsgi_ux_to_1x(environ)
self.environ = environ self.environ = environ
@ -236,45 +247,47 @@ class AppResponse(object):
r = _cherrypy.serving.response r = _cherrypy.serving.response
outstatus = r.output_status outstatus = r.output_status
if not isinstance(outstatus, bytestr): if not isinstance(outstatus, bytes):
raise TypeError("response.output_status is not a byte string.") raise TypeError('response.output_status is not a byte string.')
outheaders = [] outheaders = []
for k, v in r.header_list: for k, v in r.header_list:
if not isinstance(k, bytestr): if not isinstance(k, bytes):
raise TypeError( tmpl = 'response.header_list key %r is not a byte string.'
"response.header_list key %r is not a byte string." % raise TypeError(tmpl % k)
k) if not isinstance(v, bytes):
if not isinstance(v, bytestr): tmpl = (
raise TypeError( 'response.header_list value %r is not a byte string.'
"response.header_list value %r is not a byte string." % )
v) raise TypeError(tmpl % v)
outheaders.append((k, v)) outheaders.append((k, v))
if py3k: if six.PY3:
# According to PEP 3333, when using Python 3, the response # According to PEP 3333, when using Python 3, the response
# status and headers must be bytes masquerading as unicode; # status and headers must be bytes masquerading as unicode;
# that is, they must be of type "str" but are restricted to # that is, they must be of type "str" but are restricted to
# code points in the "latin-1" set. # code points in the "latin-1" set.
outstatus = outstatus.decode('ISO-8859-1') outstatus = outstatus.decode('ISO-8859-1')
outheaders = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) outheaders = [
for k, v in outheaders] (k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
for k, v in outheaders
]
self.iter_response = iter(r.body) self.iter_response = iter(r.body)
self.write = start_response(outstatus, outheaders) self.write = start_response(outstatus, outheaders)
except: except BaseException:
self.close() self.close()
raise raise
def __iter__(self): def __iter__(self):
return self return self
if py3k: def __next__(self):
def __next__(self): return next(self.iter_response)
return next(self.iter_response)
else: # todo: https://pythonhosted.org/six/#six.Iterator
def next(self): if six.PY2:
return self.iter_response.next() next = __next__
def close(self): def close(self):
"""Close and de-reference the current request and response. (Core)""" """Close and de-reference the current request and response. (Core)"""
@ -296,14 +309,18 @@ class AppResponse(object):
"""Create a Request object using environ.""" """Create a Request object using environ."""
env = self.environ.get env = self.environ.get
local = httputil.Host('', local = httputil.Host(
int(env('SERVER_PORT', 80) or -1), '',
env('SERVER_NAME', '')) int(env('SERVER_PORT', 80) or -1),
remote = httputil.Host(env('REMOTE_ADDR', ''), env('SERVER_NAME', ''),
int(env('REMOTE_PORT', -1) or -1), )
env('REMOTE_HOST', '')) remote = httputil.Host(
env('REMOTE_ADDR', ''),
int(env('REMOTE_PORT', -1) or -1),
env('REMOTE_HOST', ''),
)
scheme = env('wsgi.url_scheme') scheme = env('wsgi.url_scheme')
sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1") sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1')
request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
# LOGON_USER is served by IIS, and is the name of the # LOGON_USER is served by IIS, and is the name of the
@ -317,44 +334,54 @@ class AppResponse(object):
meth = self.environ['REQUEST_METHOD'] meth = self.environ['REQUEST_METHOD']
path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''), path = httputil.urljoin(
self.environ.get('PATH_INFO', '')) self.environ.get('SCRIPT_NAME', ''),
self.environ.get('PATH_INFO', ''),
)
qs = self.environ.get('QUERY_STRING', '') qs = self.environ.get('QUERY_STRING', '')
if py3k: path, qs = self.recode_path_qs(path, qs) or (path, qs)
# This isn't perfect; if the given PATH_INFO is in the
# wrong encoding, it may fail to match the appropriate config
# section URI. But meh.
old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1')
new_enc = self.cpapp.find_config(self.environ.get('PATH_INFO', ''),
"request.uri_encoding", 'utf-8')
if new_enc.lower() != old_enc.lower():
# Even though the path and qs are unicode, the WSGI server
# is required by PEP 3333 to coerce them to ISO-8859-1
# masquerading as unicode. So we have to encode back to
# bytes and then decode again using the "correct" encoding.
try:
u_path = path.encode(old_enc).decode(new_enc)
u_qs = qs.encode(old_enc).decode(new_enc)
except (UnicodeEncodeError, UnicodeDecodeError):
# Just pass them through without transcoding and hope.
pass
else:
# Only set transcoded values if they both succeed.
path = u_path
qs = u_qs
rproto = self.environ.get('SERVER_PROTOCOL') rproto = self.environ.get('SERVER_PROTOCOL')
headers = self.translate_headers(self.environ) headers = self.translate_headers(self.environ)
rfile = self.environ['wsgi.input'] rfile = self.environ['wsgi.input']
request.run(meth, path, qs, rproto, headers, rfile) request.run(meth, path, qs, rproto, headers, rfile)
headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization', headerNames = {
'CONTENT_LENGTH': 'Content-Length', 'HTTP_CGI_AUTHORIZATION': 'Authorization',
'CONTENT_TYPE': 'Content-Type', 'CONTENT_LENGTH': 'Content-Length',
'REMOTE_HOST': 'Remote-Host', 'CONTENT_TYPE': 'Content-Type',
'REMOTE_ADDR': 'Remote-Addr', 'REMOTE_HOST': 'Remote-Host',
} 'REMOTE_ADDR': 'Remote-Addr',
}
def recode_path_qs(self, path, qs):
if not six.PY3:
return
# This isn't perfect; if the given PATH_INFO is in the
# wrong encoding, it may fail to match the appropriate config
# section URI. But meh.
old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1')
new_enc = self.cpapp.find_config(
self.environ.get('PATH_INFO', ''),
'request.uri_encoding', 'utf-8',
)
if new_enc.lower() == old_enc.lower():
return
# Even though the path and qs are unicode, the WSGI server
# is required by PEP 3333 to coerce them to ISO-8859-1
# masquerading as unicode. So we have to encode back to
# bytes and then decode again using the "correct" encoding.
try:
return (
path.encode(old_enc).decode(new_enc),
qs.encode(old_enc).decode(new_enc),
)
except (UnicodeEncodeError, UnicodeDecodeError):
# Just pass them through without transcoding and hope.
pass
def translate_headers(self, environ): def translate_headers(self, environ):
"""Translate CGI-environ header names to HTTP header names.""" """Translate CGI-environ header names to HTTP header names."""
@ -362,9 +389,9 @@ class AppResponse(object):
# We assume all incoming header keys are uppercase already. # We assume all incoming header keys are uppercase already.
if cgiName in self.headerNames: if cgiName in self.headerNames:
yield self.headerNames[cgiName], environ[cgiName] yield self.headerNames[cgiName], environ[cgiName]
elif cgiName[:5] == "HTTP_": elif cgiName[:5] == 'HTTP_':
# Hackish attempt at recovering original header names. # Hackish attempt at recovering original header names.
translatedHeader = cgiName[5:].replace("_", "-") translatedHeader = cgiName[5:].replace('_', '-')
yield translatedHeader, environ[cgiName] yield translatedHeader, environ[cgiName]
@ -372,9 +399,10 @@ class CPWSGIApp(object):
"""A WSGI application object for a CherryPy Application.""" """A WSGI application object for a CherryPy Application."""
pipeline = [('ExceptionTrapper', ExceptionTrapper), pipeline = [
('InternalRedirector', InternalRedirector), ('ExceptionTrapper', ExceptionTrapper),
] ('InternalRedirector', InternalRedirector),
]
"""A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
constructor that takes an initial, positional 'nextapp' argument, constructor that takes an initial, positional 'nextapp' argument,
plus optional keyword arguments, and returns a WSGI application plus optional keyword arguments, and returns a WSGI application
@ -424,16 +452,16 @@ class CPWSGIApp(object):
def namespace_handler(self, k, v): def namespace_handler(self, k, v):
"""Config handler for the 'wsgi' namespace.""" """Config handler for the 'wsgi' namespace."""
if k == "pipeline": if k == 'pipeline':
# Note this allows multiple 'wsgi.pipeline' config entries # Note this allows multiple 'wsgi.pipeline' config entries
# (but each entry will be processed in a 'random' order). # (but each entry will be processed in a 'random' order).
# It should also allow developers to set default middleware # It should also allow developers to set default middleware
# in code (passed to self.__init__) that deployers can add to # in code (passed to self.__init__) that deployers can add to
# (but not remove) via config. # (but not remove) via config.
self.pipeline.extend(v) self.pipeline.extend(v)
elif k == "response_class": elif k == 'response_class':
self.response_class = v self.response_class = v
else: else:
name, arg = k.split(".", 1) name, arg = k.split('.', 1)
bucket = self.config.setdefault(name, {}) bucket = self.config.setdefault(name, {})
bucket[arg] = v bucket[arg] = v

View file

@ -1,23 +1,55 @@
"""WSGI server interface (see PEP 333). This adds some CP-specific bits to """
the framework-agnostic wsgiserver package. WSGI server interface (see PEP 333).
This adds some CP-specific bits to the framework-agnostic cheroot package.
""" """
import sys import sys
import cheroot.wsgi
import cheroot.server
import cherrypy import cherrypy
from cherrypy import wsgiserver
class CPWSGIServer(wsgiserver.CherryPyWSGIServer): class CPWSGIHTTPRequest(cheroot.server.HTTPRequest):
"""Wrapper for cheroot.server.HTTPRequest.
"""Wrapper for wsgiserver.CherryPyWSGIServer. This is a layer, which preserves URI parsing mode like it which was
before Cheroot v5.8.0.
wsgiserver has been designed to not reference CherryPy in any way,
so that it can be used in other frameworks and applications. Therefore,
we wrap it here, so we can set our own mount points from cherrypy.tree
and apply some attributes from config -> cherrypy.server -> wsgiserver.
""" """
def __init__(self, server, conn):
"""Initialize HTTP request container instance.
Args:
server (cheroot.server.HTTPServer):
web server object receiving this request
conn (cheroot.server.HTTPConnection):
HTTP connection object for this request
"""
super(CPWSGIHTTPRequest, self).__init__(
server, conn, proxy_mode=True
)
class CPWSGIServer(cheroot.wsgi.Server):
"""Wrapper for cheroot.wsgi.Server.
cheroot has been designed to not reference CherryPy in any way,
so that it can be used in other frameworks and applications. Therefore,
we wrap it here, so we can set our own mount points from cherrypy.tree
and apply some attributes from config -> cherrypy.server -> wsgi.Server.
"""
fmt = 'CherryPy/{cherrypy.__version__} {cheroot.wsgi.Server.version}'
version = fmt.format(**globals())
def __init__(self, server_adapter=cherrypy.server): def __init__(self, server_adapter=cherrypy.server):
"""Initialize CPWSGIServer instance.
Args:
server_adapter (cherrypy._cpserver.Server): ...
"""
self.server_adapter = server_adapter self.server_adapter = server_adapter
self.max_request_header_size = ( self.max_request_header_size = (
self.server_adapter.max_request_header_size or 0 self.server_adapter.max_request_header_size or 0
@ -31,17 +63,22 @@ class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
None) None)
self.wsgi_version = self.server_adapter.wsgi_version self.wsgi_version = self.server_adapter.wsgi_version
s = wsgiserver.CherryPyWSGIServer
s.__init__(self, server_adapter.bind_addr, cherrypy.tree, super(CPWSGIServer, self).__init__(
self.server_adapter.thread_pool, server_adapter.bind_addr, cherrypy.tree,
server_name, self.server_adapter.thread_pool,
max=self.server_adapter.thread_pool_max, server_name,
request_queue_size=self.server_adapter.socket_queue_size, max=self.server_adapter.thread_pool_max,
timeout=self.server_adapter.socket_timeout, request_queue_size=self.server_adapter.socket_queue_size,
shutdown_timeout=self.server_adapter.shutdown_timeout, timeout=self.server_adapter.socket_timeout,
accepted_queue_size=self.server_adapter.accepted_queue_size, shutdown_timeout=self.server_adapter.shutdown_timeout,
accepted_queue_timeout=self.server_adapter.accepted_queue_timeout, accepted_queue_size=self.server_adapter.accepted_queue_size,
) accepted_queue_timeout=self.server_adapter.accepted_queue_timeout,
peercreds_enabled=self.server_adapter.peercreds,
peercreds_resolve_enabled=self.server_adapter.peercreds_resolve,
)
self.ConnectionClass.RequestHandlerClass = CPWSGIHTTPRequest
self.protocol = self.server_adapter.protocol_version self.protocol = self.server_adapter.protocol_version
self.nodelay = self.server_adapter.nodelay self.nodelay = self.server_adapter.nodelay
@ -50,21 +87,24 @@ class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
else: else:
ssl_module = self.server_adapter.ssl_module or 'pyopenssl' ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
if self.server_adapter.ssl_context: if self.server_adapter.ssl_context:
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
self.ssl_adapter = adapter_class( self.ssl_adapter = adapter_class(
self.server_adapter.ssl_certificate, self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key, self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain) self.server_adapter.ssl_certificate_chain,
self.server_adapter.ssl_ciphers)
self.ssl_adapter.context = self.server_adapter.ssl_context self.ssl_adapter.context = self.server_adapter.ssl_context
elif self.server_adapter.ssl_certificate: elif self.server_adapter.ssl_certificate:
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
self.ssl_adapter = adapter_class( self.ssl_adapter = adapter_class(
self.server_adapter.ssl_certificate, self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key, self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain) self.server_adapter.ssl_certificate_chain,
self.server_adapter.ssl_ciphers)
self.stats['Enabled'] = getattr( self.stats['Enabled'] = getattr(
self.server_adapter, 'statistics', False) self.server_adapter, 'statistics', False)
def error_log(self, msg="", level=20, traceback=False): def error_log(self, msg='', level=20, traceback=False):
"""Write given message to the error log."""
cherrypy.engine.log(msg, level, traceback) cherrypy.engine.log(msg, level, traceback)

344
lib/cherrypy/_helper.py Normal file
View file

@ -0,0 +1,344 @@
"""Helper functions for CP apps."""
import six
from six.moves import urllib
from cherrypy._cpcompat import text_or_bytes
import cherrypy
def expose(func=None, alias=None):
"""Expose the function or class.
Optionally provide an alias or set of aliases.
"""
def expose_(func):
func.exposed = True
if alias is not None:
if isinstance(alias, text_or_bytes):
parents[alias.replace('.', '_')] = func
else:
for a in alias:
parents[a.replace('.', '_')] = func
return func
import sys
import types
decoratable_types = types.FunctionType, types.MethodType, type,
if six.PY2:
# Old-style classes are type types.ClassType.
decoratable_types += types.ClassType,
if isinstance(func, decoratable_types):
if alias is None:
# @expose
func.exposed = True
return func
else:
# func = expose(func, alias)
parents = sys._getframe(1).f_locals
return expose_(func)
elif func is None:
if alias is None:
# @expose()
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose(alias="alias") or
# @expose(alias=["alias1", "alias2"])
parents = sys._getframe(1).f_locals
return expose_
else:
# @expose("alias") or
# @expose(["alias1", "alias2"])
parents = sys._getframe(1).f_locals
alias = func
return expose_
def popargs(*args, **kwargs):
"""Decorate _cp_dispatch.
(cherrypy.dispatch.Dispatcher.dispatch_method_name)
Optional keyword argument: handler=(Object or Function)
Provides a _cp_dispatch function that pops off path segments into
cherrypy.request.params under the names specified. The dispatch
is then forwarded on to the next vpath element.
Note that any existing (and exposed) member function of the class that
popargs is applied to will override that value of the argument. For
instance, if you have a method named "list" on the class decorated with
popargs, then accessing "/list" will call that function instead of popping
it off as the requested parameter. This restriction applies to all
_cp_dispatch functions. The only way around this restriction is to create
a "blank class" whose only function is to provide _cp_dispatch.
If there are path elements after the arguments, or more arguments
are requested than are available in the vpath, then the 'handler'
keyword argument specifies the next object to handle the parameterized
request. If handler is not specified or is None, then self is used.
If handler is a function rather than an instance, then that function
will be called with the args specified and the return value from that
function used as the next object INSTEAD of adding the parameters to
cherrypy.request.args.
This decorator may be used in one of two ways:
As a class decorator:
@cherrypy.popargs('year', 'month', 'day')
class Blog:
def index(self, year=None, month=None, day=None):
#Process the parameters here; any url like
#/, /2009, /2009/12, or /2009/12/31
#will fill in the appropriate parameters.
def create(self):
#This link will still be available at /create. Defined functions
#take precedence over arguments.
Or as a member of a class:
class Blog:
_cp_dispatch = cherrypy.popargs('year', 'month', 'day')
#...
The handler argument may be used to mix arguments with built in functions.
For instance, the following setup allows different activities at the
day, month, and year level:
class DayHandler:
def index(self, year, month, day):
#Do something with this day; probably list entries
def delete(self, year, month, day):
#Delete all entries for this day
@cherrypy.popargs('day', handler=DayHandler())
class MonthHandler:
def index(self, year, month):
#Do something with this month; probably list entries
def delete(self, year, month):
#Delete all entries for this month
@cherrypy.popargs('month', handler=MonthHandler())
class YearHandler:
def index(self, year):
#Do something with this year
#...
@cherrypy.popargs('year', handler=YearHandler())
class Root:
def index(self):
#...
"""
# Since keyword arg comes after *args, we have to process it ourselves
# for lower versions of python.
handler = None
handler_call = False
for k, v in kwargs.items():
if k == 'handler':
handler = v
else:
tm = "cherrypy.popargs() got an unexpected keyword argument '{0}'"
raise TypeError(tm.format(k))
import inspect
if handler is not None \
and (hasattr(handler, '__call__') or inspect.isclass(handler)):
handler_call = True
def decorated(cls_or_self=None, vpath=None):
if inspect.isclass(cls_or_self):
# cherrypy.popargs is a class decorator
cls = cls_or_self
name = cherrypy.dispatch.Dispatcher.dispatch_method_name
setattr(cls, name, decorated)
return cls
# We're in the actual function
self = cls_or_self
parms = {}
for arg in args:
if not vpath:
break
parms[arg] = vpath.pop(0)
if handler is not None:
if handler_call:
return handler(**parms)
else:
cherrypy.request.params.update(parms)
return handler
cherrypy.request.params.update(parms)
# If we are the ultimate handler, then to prevent our _cp_dispatch
# from being called again, we will resolve remaining elements through
# getattr() directly.
if vpath:
return getattr(self, vpath.pop(0), None)
else:
return self
return decorated
def url(path='', qs='', script_name=None, base=None, relative=None):
"""Create an absolute URL for the given path.
If 'path' starts with a slash ('/'), this will return
(base + script_name + path + qs).
If it does not start with a slash, this returns
(base + script_name [+ request.path_info] + path + qs).
If script_name is None, cherrypy.request will be used
to find a script_name, if available.
If base is None, cherrypy.request.base will be used (if available).
Note that you can use cherrypy.tools.proxy to change this.
Finally, note that this function can be used to obtain an absolute URL
for the current request path (minus the querystring) by passing no args.
If you call url(qs=cherrypy.request.query_string), you should get the
original browser URL (assuming no internal redirections).
If relative is None or not provided, request.app.relative_urls will
be used (if available, else False). If False, the output will be an
absolute URL (including the scheme, host, vhost, and script_name).
If True, the output will instead be a URL that is relative to the
current request path, perhaps including '..' atoms. If relative is
the string 'server', the output will instead be a URL that is
relative to the server root; i.e., it will start with a slash.
"""
if isinstance(qs, (tuple, list, dict)):
qs = urllib.parse.urlencode(qs)
if qs:
qs = '?' + qs
if cherrypy.request.app:
if not path.startswith('/'):
# Append/remove trailing slash from path_info as needed
# (this is to support mistyped URL's without redirecting;
# if you want to redirect, use tools.trailing_slash).
pi = cherrypy.request.path_info
if cherrypy.request.is_index is True:
if not pi.endswith('/'):
pi = pi + '/'
elif cherrypy.request.is_index is False:
if pi.endswith('/') and pi != '/':
pi = pi[:-1]
if path == '':
path = pi
else:
path = urllib.parse.urljoin(pi, path)
if script_name is None:
script_name = cherrypy.request.script_name
if base is None:
base = cherrypy.request.base
newurl = base + script_name + normalize_path(path) + qs
else:
# No request.app (we're being called outside a request).
# We'll have to guess the base from server.* attributes.
# This will produce very different results from the above
# if you're using vhosts or tools.proxy.
if base is None:
base = cherrypy.server.base()
path = (script_name or '') + path
newurl = base + normalize_path(path) + qs
# At this point, we should have a fully-qualified absolute URL.
if relative is None:
relative = getattr(cherrypy.request.app, 'relative_urls', False)
# See http://www.ietf.org/rfc/rfc2396.txt
if relative == 'server':
# "A relative reference beginning with a single slash character is
# termed an absolute-path reference, as defined by <abs_path>..."
# This is also sometimes called "server-relative".
newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
elif relative:
# "A relative reference that does not begin with a scheme name
# or a slash character is termed a relative-path reference."
old = url(relative=False).split('/')[:-1]
new = newurl.split('/')
while old and new:
a, b = old[0], new[0]
if a != b:
break
old.pop(0)
new.pop(0)
new = (['..'] * len(old)) + new
newurl = '/'.join(new)
return newurl
def normalize_path(path):
"""Resolve given path from relative into absolute form."""
if './' not in path:
return path
# Normalize the URL by removing ./ and ../
atoms = []
for atom in path.split('/'):
if atom == '.':
pass
elif atom == '..':
# Don't pop from empty list
# (i.e. ignore redundant '..')
if atoms:
atoms.pop()
elif atom:
atoms.append(atom)
newpath = '/'.join(atoms)
# Preserve leading '/'
if path.startswith('/'):
newpath = '/' + newpath
return newpath
####
# Inlined from jaraco.classes 1.4.3
# Ref #1673
class _ClassPropertyDescriptor(object):
"""Descript for read-only class-based property.
Turns a classmethod-decorated func into a read-only property of that class
type (means the value cannot be set).
"""
def __init__(self, fget, fset=None):
"""Initialize a class property descriptor.
Instantiated by ``_helper.classproperty``.
"""
self.fget = fget
self.fset = fset
def __get__(self, obj, klass=None):
"""Return property value."""
if klass is None:
klass = type(obj)
return self.fget.__get__(obj, klass)()
def classproperty(func): # noqa: D401; irrelevant for properties
"""Decorator like classmethod to implement a static class property."""
if not isinstance(func, (classmethod, staticmethod)):
func = classmethod(func)
return _ClassPropertyDescriptor(func)
####

View file

@ -13,7 +13,7 @@ def start(configfiles=None, daemonize=False, environment=None,
"""Subscribe all engine plugins and start the engine.""" """Subscribe all engine plugins and start the engine."""
sys.path = [''] + sys.path sys.path = [''] + sys.path
for i in imports or []: for i in imports or []:
exec("import %s" % i) exec('import %s' % i)
for c in configfiles or []: for c in configfiles or []:
cherrypy.config.update(c) cherrypy.config.update(c)
@ -37,18 +37,18 @@ def start(configfiles=None, daemonize=False, environment=None,
if pidfile: if pidfile:
plugins.PIDFile(engine, pidfile).subscribe() plugins.PIDFile(engine, pidfile).subscribe()
if hasattr(engine, "signal_handler"): if hasattr(engine, 'signal_handler'):
engine.signal_handler.subscribe() engine.signal_handler.subscribe()
if hasattr(engine, "console_control_handler"): if hasattr(engine, 'console_control_handler'):
engine.console_control_handler.subscribe() engine.console_control_handler.subscribe()
if (fastcgi and (scgi or cgi)) or (scgi and cgi): if (fastcgi and (scgi or cgi)) or (scgi and cgi):
cherrypy.log.error("You may only specify one of the cgi, fastcgi, and " cherrypy.log.error('You may only specify one of the cgi, fastcgi, and '
"scgi options.", 'ENGINE') 'scgi options.', 'ENGINE')
sys.exit(1) sys.exit(1)
elif fastcgi or scgi or cgi: elif fastcgi or scgi or cgi:
# Turn off autoreload when using *cgi. # Turn off autoreload when using *cgi.
cherrypy.config.update({'engine.autoreload_on': False}) cherrypy.config.update({'engine.autoreload.on': False})
# Turn off the default HTTP server (which is subscribed by default). # Turn off the default HTTP server (which is subscribed by default).
cherrypy.server.unsubscribe() cherrypy.server.unsubscribe()
@ -65,7 +65,7 @@ def start(configfiles=None, daemonize=False, environment=None,
# Always start the engine; this will start all other services # Always start the engine; this will start all other services
try: try:
engine.start() engine.start()
except: except Exception:
# Assume the error has been logged already via bus.log. # Assume the error has been logged already via bus.log.
sys.exit(1) sys.exit(1)
else: else:
@ -73,28 +73,29 @@ def start(configfiles=None, daemonize=False, environment=None,
def run(): def run():
"""Run cherryd CLI."""
from optparse import OptionParser from optparse import OptionParser
p = OptionParser() p = OptionParser()
p.add_option('-c', '--config', action="append", dest='config', p.add_option('-c', '--config', action='append', dest='config',
help="specify config file(s)") help='specify config file(s)')
p.add_option('-d', action="store_true", dest='daemonize', p.add_option('-d', action='store_true', dest='daemonize',
help="run the server as a daemon") help='run the server as a daemon')
p.add_option('-e', '--environment', dest='environment', default=None, p.add_option('-e', '--environment', dest='environment', default=None,
help="apply the given config environment") help='apply the given config environment')
p.add_option('-f', action="store_true", dest='fastcgi', p.add_option('-f', action='store_true', dest='fastcgi',
help="start a fastcgi server instead of the default HTTP " help='start a fastcgi server instead of the default HTTP '
"server") 'server')
p.add_option('-s', action="store_true", dest='scgi', p.add_option('-s', action='store_true', dest='scgi',
help="start a scgi server instead of the default HTTP server") help='start a scgi server instead of the default HTTP server')
p.add_option('-x', action="store_true", dest='cgi', p.add_option('-x', action='store_true', dest='cgi',
help="start a cgi server instead of the default HTTP server") help='start a cgi server instead of the default HTTP server')
p.add_option('-i', '--import', action="append", dest='imports', p.add_option('-i', '--import', action='append', dest='imports',
help="specify modules to import") help='specify modules to import')
p.add_option('-p', '--pidfile', dest='pidfile', default=None, p.add_option('-p', '--pidfile', dest='pidfile', default=None,
help="store the process id in the given file") help='store the process id in the given file')
p.add_option('-P', '--Path', action="append", dest='Path', p.add_option('-P', '--Path', action='append', dest='Path',
help="add the given paths to sys.path") help='add the given paths to sys.path')
options, args = p.parse_args() options, args = p.parse_args()
if options.Path: if options.Path:

BIN
lib/cherrypy/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,12 +1,14 @@
"""CherryPy Library""" """CherryPy Library."""
# Deprecated in CherryPy 3.2 -- remove in CherryPy 3.3
from cherrypy.lib.reprconf import unrepr, modules, attributes
def is_iterator(obj): def is_iterator(obj):
'''Returns a boolean indicating if the object provided implements """Detect if the object provided implements the iterator protocol.
the iterator protocol (i.e. like a generator). This will return
false for objects which iterable, but not iterators themselves.''' (i.e. like a generator).
This will return False for objects which are iterable,
but not iterators themselves.
"""
from types import GeneratorType from types import GeneratorType
if isinstance(obj, GeneratorType): if isinstance(obj, GeneratorType):
return True return True
@ -16,22 +18,23 @@ def is_iterator(obj):
# Types which implement the protocol must return themselves when # Types which implement the protocol must return themselves when
# invoking 'iter' upon them. # invoking 'iter' upon them.
return iter(obj) is obj return iter(obj) is obj
def is_closable_iterator(obj): def is_closable_iterator(obj):
"""Detect if the given object is both closable and iterator."""
# Not an iterator. # Not an iterator.
if not is_iterator(obj): if not is_iterator(obj):
return False return False
# A generator - the easiest thing to deal with. # A generator - the easiest thing to deal with.
import inspect import inspect
if inspect.isgenerator(obj): if inspect.isgenerator(obj):
return True return True
# A custom iterator. Look for a close method... # A custom iterator. Look for a close method...
if not (hasattr(obj, 'close') and callable(obj.close)): if not (hasattr(obj, 'close') and callable(obj.close)):
return False return False
# ... which doesn't require any arguments. # ... which doesn't require any arguments.
try: try:
inspect.getcallargs(obj.close) inspect.getcallargs(obj.close)
@ -40,18 +43,24 @@ def is_closable_iterator(obj):
else: else:
return True return True
class file_generator(object):
"""Yield the given input (a file object) in chunks (default 64k). (Core)""" class file_generator(object):
"""Yield the given input (a file object) in chunks (default 64k).
(Core)
"""
def __init__(self, input, chunkSize=65536): def __init__(self, input, chunkSize=65536):
"""Initialize file_generator with file ``input`` for chunked access."""
self.input = input self.input = input
self.chunkSize = chunkSize self.chunkSize = chunkSize
def __iter__(self): def __iter__(self):
"""Return iterator."""
return self return self
def __next__(self): def __next__(self):
"""Return next chunk of file."""
chunk = self.input.read(self.chunkSize) chunk = self.input.read(self.chunkSize)
if chunk: if chunk:
return chunk return chunk
@ -63,8 +72,10 @@ class file_generator(object):
def file_generator_limited(fileobj, count, chunk_size=65536): def file_generator_limited(fileobj, count, chunk_size=65536):
"""Yield the given file object in chunks, stopping after `count` """Yield the given file object in chunks.
bytes has been emitted. Default chunk size is 64kB. (Core)
Stopps after `count` bytes has been emitted.
Default chunk size is 64kB. (Core)
""" """
remaining = count remaining = count
while remaining > 0: while remaining > 0:
@ -77,9 +88,9 @@ def file_generator_limited(fileobj, count, chunk_size=65536):
def set_vary_header(response, header_name): def set_vary_header(response, header_name):
"Add a Vary header to a response" """Add a Vary header to a response."""
varies = response.headers.get("Vary", "") varies = response.headers.get('Vary', '')
varies = [x.strip() for x in varies.split(",") if x.strip()] varies = [x.strip() for x in varies.split(',') if x.strip()]
if header_name not in varies: if header_name not in varies:
varies.append(header_name) varies.append(header_name)
response.headers['Vary'] = ", ".join(varies) response.headers['Vary'] = ', '.join(varies)

View file

@ -1,97 +0,0 @@
import cherrypy
from cherrypy.lib import httpauth
def check_auth(users, encrypt=None, realm=None):
"""If an authorization header contains credentials, return True or False.
"""
request = cherrypy.serving.request
if 'authorization' in request.headers:
# make sure the provided credentials are correctly set
ah = httpauth.parseAuthorization(request.headers['authorization'])
if ah is None:
raise cherrypy.HTTPError(400, 'Bad Request')
if not encrypt:
encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
if hasattr(users, '__call__'):
try:
# backward compatibility
users = users() # expect it to return a dictionary
if not isinstance(users, dict):
raise ValueError(
"Authentication users must be a dictionary")
# fetch the user password
password = users.get(ah["username"], None)
except TypeError:
# returns a password (encrypted or clear text)
password = users(ah["username"])
else:
if not isinstance(users, dict):
raise ValueError("Authentication users must be a dictionary")
# fetch the user password
password = users.get(ah["username"], None)
# validate the authorization by re-computing it here
# and compare it with what the user-agent provided
if httpauth.checkResponse(ah, password, method=request.method,
encrypt=encrypt, realm=realm):
request.login = ah["username"]
return True
request.login = False
return False
def basic_auth(realm, users, encrypt=None, debug=False):
"""If auth fails, raise 401 with a basic authentication header.
realm
A string containing the authentication realm.
users
A dict of the form: {username: password} or a callable returning
a dict.
encrypt
callable used to encrypt the password returned from the user-agent.
if None it defaults to a md5 encryption.
"""
if check_auth(users, encrypt):
if debug:
cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH')
return
# inform the user-agent this path is protected
cherrypy.serving.response.headers[
'www-authenticate'] = httpauth.basicAuth(realm)
raise cherrypy.HTTPError(
401, "You are not authorized to access that resource")
def digest_auth(realm, users, debug=False):
"""If auth fails, raise 401 with a digest authentication header.
realm
A string containing the authentication realm.
users
A dict of the form: {username: password} or a callable returning
a dict.
"""
if check_auth(users, realm=realm):
if debug:
cherrypy.log('Auth successful', 'TOOLS.DIGEST_AUTH')
return
# inform the user-agent this path is protected
cherrypy.serving.response.headers[
'www-authenticate'] = httpauth.digestAuth(realm)
raise cherrypy.HTTPError(
401, "You are not authorized to access that resource")

View file

@ -1,8 +1,9 @@
# This file is part of CherryPy <http://www.cherrypy.org/> # This file is part of CherryPy <http://www.cherrypy.org/>
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 # vim:ts=4:sw=4:expandtab:fileencoding=utf-8
"""HTTP Basic Authentication tool.
__doc__ = """This module provides a CherryPy 3.x tool which implements This module provides a CherryPy 3.x tool which implements
the server-side of HTTP Basic Access Authentication, as described in the server-side of HTTP Basic Access Authentication, as described in
:rfc:`2617`. :rfc:`2617`.
@ -14,18 +15,23 @@ as the credentials store::
basic_auth = {'tools.auth_basic.on': True, basic_auth = {'tools.auth_basic.on': True,
'tools.auth_basic.realm': 'earth', 'tools.auth_basic.realm': 'earth',
'tools.auth_basic.checkpassword': checkpassword, 'tools.auth_basic.checkpassword': checkpassword,
'tools.auth_basic.accept_charset': 'UTF-8',
} }
app_config = { '/' : basic_auth } app_config = { '/' : basic_auth }
""" """
import binascii
import unicodedata
import base64
import cherrypy
from cherrypy._cpcompat import ntou, tonative
__author__ = 'visteya' __author__ = 'visteya'
__date__ = 'April 2009' __date__ = 'April 2009'
import binascii
from cherrypy._cpcompat import base64_decode
import cherrypy
def checkpassword_dict(user_password_dict): def checkpassword_dict(user_password_dict):
"""Returns a checkpassword function which checks credentials """Returns a checkpassword function which checks credentials
@ -42,9 +48,10 @@ def checkpassword_dict(user_password_dict):
return checkpassword return checkpassword
def basic_auth(realm, checkpassword, debug=False): def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'):
"""A CherryPy tool which hooks at before_handler to perform """A CherryPy tool which hooks at before_handler to perform
HTTP Basic Access Authentication, as specified in :rfc:`2617`. HTTP Basic Access Authentication, as specified in :rfc:`2617`
and :rfc:`7617`.
If the request has an 'authorization' header with a 'Basic' scheme, this If the request has an 'authorization' header with a 'Basic' scheme, this
tool attempts to authenticate the credentials supplied in that header. If tool attempts to authenticate the credentials supplied in that header. If
@ -64,27 +71,50 @@ def basic_auth(realm, checkpassword, debug=False):
""" """
fallback_charset = 'ISO-8859-1'
if '"' in realm: if '"' in realm:
raise ValueError('Realm cannot contain the " (quote) character.') raise ValueError('Realm cannot contain the " (quote) character.')
request = cherrypy.serving.request request = cherrypy.serving.request
auth_header = request.headers.get('authorization') auth_header = request.headers.get('authorization')
if auth_header is not None: if auth_header is not None:
try: # split() error, base64.decodestring() error
msg = 'Bad Request'
with cherrypy.HTTPError.handle((ValueError, binascii.Error), 400, msg):
scheme, params = auth_header.split(' ', 1) scheme, params = auth_header.split(' ', 1)
if scheme.lower() == 'basic': if scheme.lower() == 'basic':
username, password = base64_decode(params).split(':', 1) charsets = accept_charset, fallback_charset
decoded_params = base64.b64decode(params.encode('ascii'))
decoded_params = _try_decode(decoded_params, charsets)
decoded_params = ntou(decoded_params)
decoded_params = unicodedata.normalize('NFC', decoded_params)
decoded_params = tonative(decoded_params)
username, password = decoded_params.split(':', 1)
if checkpassword(realm, username, password): if checkpassword(realm, username, password):
if debug: if debug:
cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC') cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')
request.login = username request.login = username
return # successful authentication return # successful authentication
# split() error, base64.decodestring() error
except (ValueError, binascii.Error):
raise cherrypy.HTTPError(400, 'Bad Request')
charset = accept_charset.upper()
charset_declaration = (
(', charset="%s"' % charset)
if charset != fallback_charset
else ''
)
# Respond with 401 status and a WWW-Authenticate header # Respond with 401 status and a WWW-Authenticate header
cherrypy.serving.response.headers[ cherrypy.serving.response.headers['www-authenticate'] = (
'www-authenticate'] = 'Basic realm="%s"' % realm 'Basic realm="%s"%s' % (realm, charset_declaration)
)
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
401, "You are not authorized to access that resource") 401, 'You are not authorized to access that resource')
def _try_decode(subject, charsets):
for charset in charsets[:-1]:
try:
return tonative(subject, charset)
except ValueError:
pass
return tonative(subject, charsets[-1])

View file

@ -1,8 +1,9 @@
# This file is part of CherryPy <http://www.cherrypy.org/> # This file is part of CherryPy <http://www.cherrypy.org/>
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 # vim:ts=4:sw=4:expandtab:fileencoding=utf-8
"""HTTP Digest Authentication tool.
__doc__ = """An implementation of the server-side of HTTP Digest Access An implementation of the server-side of HTTP Digest Access
Authentication, which is described in :rfc:`2617`. Authentication, which is described in :rfc:`2617`.
Example usage, using the built-in get_ha1_dict_plain function which uses a dict Example usage, using the built-in get_ha1_dict_plain function which uses a dict
@ -14,21 +15,28 @@ of plaintext passwords as the credentials store::
'tools.auth_digest.realm': 'wonderland', 'tools.auth_digest.realm': 'wonderland',
'tools.auth_digest.get_ha1': get_ha1, 'tools.auth_digest.get_ha1': get_ha1,
'tools.auth_digest.key': 'a565c27146791cfb', 'tools.auth_digest.key': 'a565c27146791cfb',
'tools.auth_digest.accept_charset': 'UTF-8',
} }
app_config = { '/' : digest_auth } app_config = { '/' : digest_auth }
""" """
import time
import functools
from hashlib import md5
from six.moves.urllib.request import parse_http_list, parse_keqv_list
import cherrypy
from cherrypy._cpcompat import ntob, tonative
__author__ = 'visteya' __author__ = 'visteya'
__date__ = 'April 2009' __date__ = 'April 2009'
import time def md5_hex(s):
from hashlib import md5 return md5(ntob(s, 'utf-8')).hexdigest()
from cherrypy._cpcompat import parse_http_list, parse_keqv_list
import cherrypy
from cherrypy._cpcompat import ntob
md5_hex = lambda s: md5(ntob(s)).hexdigest()
qop_auth = 'auth' qop_auth = 'auth'
qop_auth_int = 'auth-int' qop_auth_int = 'auth-int'
@ -36,6 +44,9 @@ valid_qops = (qop_auth, qop_auth_int)
valid_algorithms = ('MD5', 'MD5-sess') valid_algorithms = ('MD5', 'MD5-sess')
FALLBACK_CHARSET = 'ISO-8859-1'
DEFAULT_CHARSET = 'UTF-8'
def TRACE(msg): def TRACE(msg):
cherrypy.log(msg, context='TOOLS.AUTH_DIGEST') cherrypy.log(msg, context='TOOLS.AUTH_DIGEST')
@ -130,24 +141,47 @@ def H(s):
return md5_hex(s) return md5_hex(s)
class HttpDigestAuthorization (object): def _try_decode_header(header, charset):
global FALLBACK_CHARSET
"""Class to parse a Digest Authorization header and perform re-calculation for enc in (charset, FALLBACK_CHARSET):
of the digest. try:
return tonative(ntob(tonative(header, 'latin1'), 'latin1'), enc)
except ValueError as ve:
last_err = ve
else:
raise last_err
class HttpDigestAuthorization(object):
""" """
Parses a Digest Authorization header and performs
re-calculation of the digest.
"""
scheme = 'digest'
def errmsg(self, s): def errmsg(self, s):
return 'Digest Authorization header: %s' % s return 'Digest Authorization header: %s' % s
def __init__(self, auth_header, http_method, debug=False): @classmethod
def matches(cls, header):
scheme, _, _ = header.partition(' ')
return scheme.lower() == cls.scheme
def __init__(
self, auth_header, http_method,
debug=False, accept_charset=DEFAULT_CHARSET[:],
):
self.http_method = http_method self.http_method = http_method
self.debug = debug self.debug = debug
scheme, params = auth_header.split(" ", 1)
self.scheme = scheme.lower() if not self.matches(auth_header):
if self.scheme != 'digest':
raise ValueError('Authorization scheme is not "Digest"') raise ValueError('Authorization scheme is not "Digest"')
self.auth_header = auth_header self.auth_header = _try_decode_header(auth_header, accept_charset)
scheme, params = self.auth_header.split(' ', 1)
# make a dict of the params # make a dict of the params
items = parse_http_list(params) items = parse_http_list(params)
@ -180,7 +214,7 @@ class HttpDigestAuthorization (object):
) )
if not has_reqd: if not has_reqd:
raise ValueError( raise ValueError(
self.errmsg("Not all required parameters are present.")) self.errmsg('Not all required parameters are present.'))
if self.qop: if self.qop:
if self.qop not in valid_qops: if self.qop not in valid_qops:
@ -188,13 +222,13 @@ class HttpDigestAuthorization (object):
self.errmsg("Unsupported value for qop: '%s'" % self.qop)) self.errmsg("Unsupported value for qop: '%s'" % self.qop))
if not (self.cnonce and self.nc): if not (self.cnonce and self.nc):
raise ValueError( raise ValueError(
self.errmsg("If qop is sent then " self.errmsg('If qop is sent then '
"cnonce and nc MUST be present")) 'cnonce and nc MUST be present'))
else: else:
if self.cnonce or self.nc: if self.cnonce or self.nc:
raise ValueError( raise ValueError(
self.errmsg("If qop is not sent, " self.errmsg('If qop is not sent, '
"neither cnonce nor nc can be present")) 'neither cnonce nor nc can be present'))
def __str__(self): def __str__(self):
return 'authorization : %s' % self.auth_header return 'authorization : %s' % self.auth_header
@ -239,7 +273,7 @@ class HttpDigestAuthorization (object):
except ValueError: # int() error except ValueError: # int() error
pass pass
if self.debug: if self.debug:
TRACE("nonce is stale") TRACE('nonce is stale')
return True return True
def HA2(self, entity_body=''): def HA2(self, entity_body=''):
@ -251,14 +285,14 @@ class HttpDigestAuthorization (object):
# #
# If the "qop" value is "auth-int", then A2 is: # If the "qop" value is "auth-int", then A2 is:
# A2 = method ":" digest-uri-value ":" H(entity-body) # A2 = method ":" digest-uri-value ":" H(entity-body)
if self.qop is None or self.qop == "auth": if self.qop is None or self.qop == 'auth':
a2 = '%s:%s' % (self.http_method, self.uri) a2 = '%s:%s' % (self.http_method, self.uri)
elif self.qop == "auth-int": elif self.qop == 'auth-int':
a2 = "%s:%s:%s" % (self.http_method, self.uri, H(entity_body)) a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body))
else: else:
# in theory, this should never happen, since I validate qop in # in theory, this should never happen, since I validate qop in
# __init__() # __init__()
raise ValueError(self.errmsg("Unrecognized value for qop!")) raise ValueError(self.errmsg('Unrecognized value for qop!'))
return H(a2) return H(a2)
def request_digest(self, ha1, entity_body=''): def request_digest(self, ha1, entity_body=''):
@ -279,10 +313,10 @@ class HttpDigestAuthorization (object):
ha2 = self.HA2(entity_body) ha2 = self.HA2(entity_body)
# Request-Digest -- RFC 2617 3.2.2.1 # Request-Digest -- RFC 2617 3.2.2.1
if self.qop: if self.qop:
req = "%s:%s:%s:%s:%s" % ( req = '%s:%s:%s:%s:%s' % (
self.nonce, self.nc, self.cnonce, self.qop, ha2) self.nonce, self.nc, self.cnonce, self.qop, ha2)
else: else:
req = "%s:%s" % (self.nonce, ha2) req = '%s:%s' % (self.nonce, ha2)
# RFC 2617 3.2.2.2 # RFC 2617 3.2.2.2
# #
@ -302,25 +336,44 @@ class HttpDigestAuthorization (object):
return digest return digest
def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, def _get_charset_declaration(charset):
stale=False): global FALLBACK_CHARSET
charset = charset.upper()
return (
(', charset="%s"' % charset)
if charset != FALLBACK_CHARSET
else ''
)
def www_authenticate(
realm, key, algorithm='MD5', nonce=None, qop=qop_auth,
stale=False, accept_charset=DEFAULT_CHARSET[:],
):
"""Constructs a WWW-Authenticate header for Digest authentication.""" """Constructs a WWW-Authenticate header for Digest authentication."""
if qop not in valid_qops: if qop not in valid_qops:
raise ValueError("Unsupported value for qop: '%s'" % qop) raise ValueError("Unsupported value for qop: '%s'" % qop)
if algorithm not in valid_algorithms: if algorithm not in valid_algorithms:
raise ValueError("Unsupported value for algorithm: '%s'" % algorithm) raise ValueError("Unsupported value for algorithm: '%s'" % algorithm)
HEADER_PATTERN = (
'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"%s%s'
)
if nonce is None: if nonce is None:
nonce = synthesize_nonce(realm, key) nonce = synthesize_nonce(realm, key)
s = 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
realm, nonce, algorithm, qop) stale_param = ', stale="true"' if stale else ''
if stale:
s += ', stale="true"' charset_declaration = _get_charset_declaration(accept_charset)
return s
return HEADER_PATTERN % (
realm, nonce, algorithm, qop, stale_param, charset_declaration,
)
def digest_auth(realm, get_ha1, key, debug=False): def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'):
"""A CherryPy tool which hooks at before_handler to perform """A CherryPy tool that hooks at before_handler to perform
HTTP Digest Access Authentication, as specified in :rfc:`2617`. HTTP Digest Access Authentication, as specified in :rfc:`2617`.
If the request has an 'authorization' header with a 'Digest' scheme, If the request has an 'authorization' header with a 'Digest' scheme,
@ -333,7 +386,7 @@ def digest_auth(realm, get_ha1, key, debug=False):
A string containing the authentication realm. A string containing the authentication realm.
get_ha1 get_ha1
A callable which looks up a username in a credentials store A callable that looks up a username in a credentials store
and returns the HA1 string, which is defined in the RFC to be and returns the HA1 string, which is defined in the RFC to be
MD5(username : realm : password). The function's signature is: MD5(username : realm : password). The function's signature is:
``get_ha1(realm, username)`` ``get_ha1(realm, username)``
@ -349,43 +402,63 @@ def digest_auth(realm, get_ha1, key, debug=False):
request = cherrypy.serving.request request = cherrypy.serving.request
auth_header = request.headers.get('authorization') auth_header = request.headers.get('authorization')
nonce_is_stale = False
if auth_header is not None:
try:
auth = HttpDigestAuthorization(
auth_header, request.method, debug=debug)
except ValueError:
raise cherrypy.HTTPError(
400, "The Authorization header could not be parsed.")
if debug: respond_401 = functools.partial(
TRACE(str(auth)) _respond_401, realm, key, accept_charset, debug)
if auth.validate_nonce(realm, key): if not HttpDigestAuthorization.matches(auth_header or ''):
ha1 = get_ha1(realm, auth.username) respond_401()
if ha1 is not None:
# note that for request.body to be available we need to
# hook in at before_handler, not on_start_resource like
# 3.1.x digest_auth does.
digest = auth.request_digest(ha1, entity_body=request.body)
if digest == auth.response: # authenticated
if debug:
TRACE("digest matches auth.response")
# Now check if nonce is stale.
# The choice of ten minutes' lifetime for nonce is somewhat
# arbitrary
nonce_is_stale = auth.is_nonce_stale(max_age_seconds=600)
if not nonce_is_stale:
request.login = auth.username
if debug:
TRACE("authentication of %s successful" %
auth.username)
return
# Respond with 401 status and a WWW-Authenticate header msg = 'The Authorization header could not be parsed.'
header = www_authenticate(realm, key, stale=nonce_is_stale) with cherrypy.HTTPError.handle(ValueError, 400, msg):
auth = HttpDigestAuthorization(
auth_header, request.method,
debug=debug, accept_charset=accept_charset,
)
if debug:
TRACE(str(auth))
if not auth.validate_nonce(realm, key):
respond_401()
ha1 = get_ha1(realm, auth.username)
if ha1 is None:
respond_401()
# note that for request.body to be available we need to
# hook in at before_handler, not on_start_resource like
# 3.1.x digest_auth does.
digest = auth.request_digest(ha1, entity_body=request.body)
if digest != auth.response:
respond_401()
# authenticated
if debug:
TRACE('digest matches auth.response')
# Now check if nonce is stale.
# The choice of ten minutes' lifetime for nonce is somewhat
# arbitrary
if auth.is_nonce_stale(max_age_seconds=600):
respond_401(stale=True)
request.login = auth.username
if debug:
TRACE('authentication of %s successful' % auth.username)
def _respond_401(realm, key, accept_charset, debug, **kwargs):
"""
Respond with 401 status and a WWW-Authenticate header
"""
header = www_authenticate(
realm, key,
accept_charset=accept_charset,
**kwargs
)
if debug: if debug:
TRACE(header) TRACE(header)
cherrypy.serving.response.headers['WWW-Authenticate'] = header cherrypy.serving.response.headers['WWW-Authenticate'] = header
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
401, "You are not authorized to access that resource") 401, 'You are not authorized to access that resource')

View file

@ -37,9 +37,11 @@ import sys
import threading import threading
import time import time
import six
import cherrypy import cherrypy
from cherrypy.lib import cptools, httputil from cherrypy.lib import cptools, httputil
from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted, Event from cherrypy._cpcompat import Event
class Cache(object): class Cache(object):
@ -48,19 +50,19 @@ class Cache(object):
def get(self): def get(self):
"""Return the current variant if in the cache, else None.""" """Return the current variant if in the cache, else None."""
raise NotImplemented raise NotImplementedError
def put(self, obj, size): def put(self, obj, size):
"""Store the current variant in the cache.""" """Store the current variant in the cache."""
raise NotImplemented raise NotImplementedError
def delete(self): def delete(self):
"""Remove ALL cached variants of the current resource.""" """Remove ALL cached variants of the current resource."""
raise NotImplemented raise NotImplementedError
def clear(self): def clear(self):
"""Reset the cache to its initial, empty state.""" """Reset the cache to its initial, empty state."""
raise NotImplemented raise NotImplementedError
# ------------------------------ Memory Cache ------------------------------- # # ------------------------------ Memory Cache ------------------------------- #
@ -170,7 +172,7 @@ class MemoryCache(Cache):
# Run self.expire_cache in a separate daemon thread. # Run self.expire_cache in a separate daemon thread.
t = threading.Thread(target=self.expire_cache, name='expire_cache') t = threading.Thread(target=self.expire_cache, name='expire_cache')
self.expiration_thread = t self.expiration_thread = t
set_daemon(t, True) t.daemon = True
t.start() t.start()
def clear(self): def clear(self):
@ -197,7 +199,8 @@ class MemoryCache(Cache):
now = time.time() now = time.time()
# Must make a copy of expirations so it doesn't change size # Must make a copy of expirations so it doesn't change size
# during iteration # during iteration
for expiration_time, objects in copyitems(self.expirations): items = list(six.iteritems(self.expirations))
for expiration_time, objects in items:
if expiration_time <= now: if expiration_time <= now:
for obj_size, uri, sel_header_values in objects: for obj_size, uri, sel_header_values in objects:
try: try:
@ -265,7 +268,7 @@ class MemoryCache(Cache):
self.store.pop(uri, None) self.store.pop(uri, None)
def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs): def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs):
"""Try to obtain cached output. If fresh enough, raise HTTPError(304). """Try to obtain cached output. If fresh enough, raise HTTPError(304).
If POST, PUT, or DELETE: If POST, PUT, or DELETE:
@ -291,9 +294,9 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
if not hasattr(cherrypy, "_cache"): if not hasattr(cherrypy, '_cache'):
# Make a process-wide Cache object. # Make a process-wide Cache object.
cherrypy._cache = kwargs.pop("cache_class", MemoryCache)() cherrypy._cache = kwargs.pop('cache_class', MemoryCache)()
# Take all remaining kwargs and set them on the Cache object. # Take all remaining kwargs and set them on the Cache object.
for k, v in kwargs.items(): for k, v in kwargs.items():
@ -328,7 +331,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
if directive == 'max-age': if directive == 'max-age':
if len(atoms) != 1 or not atoms[0].isdigit(): if len(atoms) != 1 or not atoms[0].isdigit():
raise cherrypy.HTTPError( raise cherrypy.HTTPError(
400, "Invalid Cache-Control header") 400, 'Invalid Cache-Control header')
max_age = int(atoms[0]) max_age = int(atoms[0])
break break
elif directive == 'no-cache': elif directive == 'no-cache':
@ -353,13 +356,13 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
return False return False
# Copy the response headers. See # Copy the response headers. See
# https://bitbucket.org/cherrypy/cherrypy/issue/721. # https://github.com/cherrypy/cherrypy/issues/721.
response.headers = rh = httputil.HeaderMap() response.headers = rh = httputil.HeaderMap()
for k in h: for k in h:
dict.__setitem__(rh, k, dict.__getitem__(h, k)) dict.__setitem__(rh, k, dict.__getitem__(h, k))
# Add the required Age header # Add the required Age header
response.headers["Age"] = str(age) response.headers['Age'] = str(age)
try: try:
# Note that validate_since depends on a Last-Modified header; # Note that validate_since depends on a Last-Modified header;
@ -402,10 +405,19 @@ def tee_output():
output.append(chunk) output.append(chunk)
yield chunk yield chunk
# save the cache data # Save the cache data, but only if the body isn't empty.
body = ntob('').join(output) # e.g. a 304 Not Modified on a static file response will
cherrypy._cache.put((response.status, response.headers or {}, # have an empty body.
body, response.time), len(body)) # If the body is empty, delete the cache because it
# contains a stale Threading._Event object that will
# stall all consecutive requests until the _Event times
# out
body = b''.join(output)
if not body:
cherrypy._cache.delete()
else:
cherrypy._cache.put((response.status, response.headers or {},
body, response.time), len(body))
response = cherrypy.serving.response response = cherrypy.serving.response
response.body = tee(response.body) response.body = tee(response.body)
@ -457,14 +469,14 @@ def expires(secs=0, force=False, debug=False):
secs = (86400 * secs.days) + secs.seconds secs = (86400 * secs.days) + secs.seconds
if secs == 0: if secs == 0:
if force or ("Pragma" not in headers): if force or ('Pragma' not in headers):
headers["Pragma"] = "no-cache" headers['Pragma'] = 'no-cache'
if cherrypy.serving.request.protocol >= (1, 1): if cherrypy.serving.request.protocol >= (1, 1):
if force or "Cache-Control" not in headers: if force or 'Cache-Control' not in headers:
headers["Cache-Control"] = "no-cache, must-revalidate" headers['Cache-Control'] = 'no-cache, must-revalidate'
# Set an explicit Expires date in the past. # Set an explicit Expires date in the past.
expiry = httputil.HTTPDate(1169942400.0) expiry = httputil.HTTPDate(1169942400.0)
else: else:
expiry = httputil.HTTPDate(response.time + secs) expiry = httputil.HTTPDate(response.time + secs)
if force or "Expires" not in headers: if force or 'Expires' not in headers:
headers["Expires"] = expiry headers['Expires'] = expiry

View file

@ -23,10 +23,15 @@ it will call ``serve()`` for you.
import re import re
import sys import sys
import cgi import cgi
from cherrypy._cpcompat import quote_plus
import os import os
import os.path import os.path
localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
from six.moves import urllib
import cherrypy
localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache')
the_coverage = None the_coverage = None
try: try:
@ -42,8 +47,8 @@ except ImportError:
import warnings import warnings
warnings.warn( warnings.warn(
"No code coverage will be performed; " 'No code coverage will be performed; '
"coverage.py could not be imported.") 'coverage.py could not be imported.')
def start(): def start():
pass pass
@ -193,7 +198,7 @@ def _percent(statements, missing):
return 0 return 0
def _show_branch(root, base, path, pct=0, showpct=False, exclude="", def _show_branch(root, base, path, pct=0, showpct=False, exclude='',
coverage=the_coverage): coverage=the_coverage):
# Show the directory name and any of our children # Show the directory name and any of our children
@ -204,11 +209,11 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
if newpath.lower().startswith(base): if newpath.lower().startswith(base):
relpath = newpath[len(base):] relpath = newpath[len(base):]
yield "| " * relpath.count(os.sep) yield '| ' * relpath.count(os.sep)
yield ( yield (
"<a class='directory' " "<a class='directory' "
"href='menu?base=%s&exclude=%s'>%s</a>\n" % "href='menu?base=%s&exclude=%s'>%s</a>\n" %
(newpath, quote_plus(exclude), name) (newpath, urllib.parse.quote_plus(exclude), name)
) )
for chunk in _show_branch( for chunk in _show_branch(
@ -225,22 +230,22 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
for name in files: for name in files:
newpath = os.path.join(path, name) newpath = os.path.join(path, name)
pc_str = "" pc_str = ''
if showpct: if showpct:
try: try:
_, statements, _, missing, _ = coverage.analysis2(newpath) _, statements, _, missing, _ = coverage.analysis2(newpath)
except: except Exception:
# Yes, we really want to pass on all errors. # Yes, we really want to pass on all errors.
pass pass
else: else:
pc = _percent(statements, missing) pc = _percent(statements, missing)
pc_str = ("%3d%% " % pc).replace(' ', '&nbsp;') pc_str = ('%3d%% ' % pc).replace(' ', '&nbsp;')
if pc < float(pct) or pc == -1: if pc < float(pct) or pc == -1:
pc_str = "<span class='fail'>%s</span>" % pc_str pc_str = "<span class='fail'>%s</span>" % pc_str
else: else:
pc_str = "<span class='pass'>%s</span>" % pc_str pc_str = "<span class='pass'>%s</span>" % pc_str
yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1), yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1),
pc_str, newpath, name) pc_str, newpath, name)
@ -260,8 +265,8 @@ def _graft(path, tree):
break break
atoms.append(tail) atoms.append(tail)
atoms.append(p) atoms.append(p)
if p != "/": if p != '/':
atoms.append("/") atoms.append('/')
atoms.reverse() atoms.reverse()
for node in atoms: for node in atoms:
@ -286,15 +291,15 @@ class CoverStats(object):
if root is None: if root is None:
# Guess initial depth. Files outside this path will not be # Guess initial depth. Files outside this path will not be
# reachable from the web interface. # reachable from the web interface.
import cherrypy
root = os.path.dirname(cherrypy.__file__) root = os.path.dirname(cherrypy.__file__)
self.root = root self.root = root
@cherrypy.expose
def index(self): def index(self):
return TEMPLATE_FRAMESET % self.root.lower() return TEMPLATE_FRAMESET % self.root.lower()
index.exposed = True
def menu(self, base="/", pct="50", showpct="", @cherrypy.expose
def menu(self, base='/', pct='50', showpct='',
exclude=r'python\d\.\d|test|tut\d|tutorial'): exclude=r'python\d\.\d|test|tut\d|tutorial'):
# The coverage module uses all-lower-case names. # The coverage module uses all-lower-case names.
@ -305,37 +310,36 @@ class CoverStats(object):
# Start by showing links for parent paths # Start by showing links for parent paths
yield "<div id='crumbs'>" yield "<div id='crumbs'>"
path = "" path = ''
atoms = base.split(os.sep) atoms = base.split(os.sep)
atoms.pop() atoms.pop()
for atom in atoms: for atom in atoms:
path += atom + os.sep path += atom + os.sep
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s" yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
% (path, quote_plus(exclude), atom, os.sep)) % (path, urllib.parse.quote_plus(exclude), atom, os.sep))
yield "</div>" yield '</div>'
yield "<div id='tree'>" yield "<div id='tree'>"
# Then display the tree # Then display the tree
tree = get_tree(base, exclude, self.coverage) tree = get_tree(base, exclude, self.coverage)
if not tree: if not tree:
yield "<p>No modules covered.</p>" yield '<p>No modules covered.</p>'
else: else:
for chunk in _show_branch(tree, base, "/", pct, for chunk in _show_branch(tree, base, '/', pct,
showpct == 'checked', exclude, showpct == 'checked', exclude,
coverage=self.coverage): coverage=self.coverage):
yield chunk yield chunk
yield "</div>" yield '</div>'
yield "</body></html>" yield '</body></html>'
menu.exposed = True
def annotated_file(self, filename, statements, excluded, missing): def annotated_file(self, filename, statements, excluded, missing):
source = open(filename, 'r') source = open(filename, 'r')
buffer = [] buffer = []
for lineno, line in enumerate(source.readlines()): for lineno, line in enumerate(source.readlines()):
lineno += 1 lineno += 1
line = line.strip("\n\r") line = line.strip('\n\r')
empty_the_buffer = True empty_the_buffer = True
if lineno in excluded: if lineno in excluded:
template = TEMPLATE_LOC_EXCLUDED template = TEMPLATE_LOC_EXCLUDED
@ -352,6 +356,7 @@ class CoverStats(object):
buffer = [] buffer = []
yield template % (lineno, cgi.escape(line)) yield template % (lineno, cgi.escape(line))
@cherrypy.expose
def report(self, name): def report(self, name):
filename, statements, excluded, missing, _ = self.coverage.analysis2( filename, statements, excluded, missing, _ = self.coverage.analysis2(
name) name)
@ -366,22 +371,21 @@ class CoverStats(object):
yield '</table>' yield '</table>'
yield '</body>' yield '</body>'
yield '</html>' yield '</html>'
report.exposed = True
def serve(path=localFile, port=8080, root=None): def serve(path=localFile, port=8080, root=None):
if coverage is None: if coverage is None:
raise ImportError("The coverage module could not be imported.") raise ImportError('The coverage module could not be imported.')
from coverage import coverage from coverage import coverage
cov = coverage(data_file=path) cov = coverage(data_file=path)
cov.load() cov.load()
import cherrypy
cherrypy.config.update({'server.socket_port': int(port), cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10, 'server.thread_pool': 10,
'environment': "production", 'environment': 'production',
}) })
cherrypy.quickstart(CoverStats(cov, root)) cherrypy.quickstart(CoverStats(cov, root))
if __name__ == "__main__":
if __name__ == '__main__':
serve(*tuple(sys.argv[1:])) serve(*tuple(sys.argv[1:]))

View file

@ -187,9 +187,19 @@ To format statistics reports::
""" """
import logging
import os
import sys
import threading
import time
import six
import cherrypy
from cherrypy._cpcompat import json
# ------------------------------- Statistics -------------------------------- # # ------------------------------- Statistics -------------------------------- #
import logging
if not hasattr(logging, 'statistics'): if not hasattr(logging, 'statistics'):
logging.statistics = {} logging.statistics = {}
@ -210,12 +220,6 @@ def extrapolate_statistics(scope):
# -------------------- CherryPy Applications Statistics --------------------- # # -------------------- CherryPy Applications Statistics --------------------- #
import sys
import threading
import time
import cherrypy
appstats = logging.statistics.setdefault('CherryPy Applications', {}) appstats = logging.statistics.setdefault('CherryPy Applications', {})
appstats.update({ appstats.update({
'Enabled': True, 'Enabled': True,
@ -246,7 +250,9 @@ appstats.update({
'Requests': {}, 'Requests': {},
}) })
proc_time = lambda s: time.time() - s['Start Time']
def proc_time(s):
return time.time() - s['Start Time']
class ByteCountWrapper(object): class ByteCountWrapper(object):
@ -292,7 +298,8 @@ class ByteCountWrapper(object):
return data return data
average_uriset_time = lambda s: s['Count'] and (s['Sum'] / s['Count']) or 0 def average_uriset_time(s):
return s['Count'] and (s['Sum'] / s['Count']) or 0
def _get_threading_ident(): def _get_threading_ident():
@ -300,6 +307,7 @@ def _get_threading_ident():
return threading.get_ident() return threading.get_ident()
return threading._get_ident() return threading._get_ident()
class StatsTool(cherrypy.Tool): class StatsTool(cherrypy.Tool):
"""Record various information about the current request.""" """Record various information about the current request."""
@ -390,28 +398,22 @@ class StatsTool(cherrypy.Tool):
sq.pop(0) sq.pop(0)
import cherrypy
cherrypy.tools.cpstats = StatsTool() cherrypy.tools.cpstats = StatsTool()
# ---------------------- CherryPy Statistics Reporting ---------------------- # # ---------------------- CherryPy Statistics Reporting ---------------------- #
import os
thisdir = os.path.abspath(os.path.dirname(__file__)) thisdir = os.path.abspath(os.path.dirname(__file__))
try:
import json
except ImportError:
try:
import simplejson as json
except ImportError:
json = None
missing = object() missing = object()
locale_date = lambda v: time.strftime('%c', time.gmtime(v))
iso_format = lambda v: time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v)) def locale_date(v):
return time.strftime('%c', time.gmtime(v))
def iso_format(v):
return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
def pause_resume(ns): def pause_resume(ns):
@ -475,6 +477,7 @@ class StatsPage(object):
}, },
} }
@cherrypy.expose
def index(self): def index(self):
# Transform the raw data into pretty output for HTML # Transform the raw data into pretty output for HTML
yield """ yield """
@ -578,7 +581,6 @@ table.stats2 th {
</body> </body>
</html> </html>
""" """
index.exposed = True
def get_namespaces(self): def get_namespaces(self):
"""Yield (title, scalars, collections) for each namespace.""" """Yield (title, scalars, collections) for each namespace."""
@ -611,12 +613,7 @@ table.stats2 th {
"""Return ([headers], [rows]) for the given collection.""" """Return ([headers], [rows]) for the given collection."""
# E.g., the 'Requests' dict. # E.g., the 'Requests' dict.
headers = [] headers = []
try: vals = six.itervalues(v)
# python2
vals = v.itervalues()
except AttributeError:
# python3
vals = v.values()
for record in vals: for record in vals:
for k3 in record: for k3 in record:
format = formatting.get(k3, missing) format = formatting.get(k3, missing)
@ -678,22 +675,22 @@ table.stats2 th {
return headers, subrows return headers, subrows
if json is not None: if json is not None:
@cherrypy.expose
def data(self): def data(self):
s = extrapolate_statistics(logging.statistics) s = extrapolate_statistics(logging.statistics)
cherrypy.response.headers['Content-Type'] = 'application/json' cherrypy.response.headers['Content-Type'] = 'application/json'
return json.dumps(s, sort_keys=True, indent=4) return json.dumps(s, sort_keys=True, indent=4)
data.exposed = True
@cherrypy.expose
def pause(self, namespace): def pause(self, namespace):
logging.statistics.get(namespace, {})['Enabled'] = False logging.statistics.get(namespace, {})['Enabled'] = False
raise cherrypy.HTTPRedirect('./') raise cherrypy.HTTPRedirect('./')
pause.exposed = True
pause.cp_config = {'tools.allow.on': True, pause.cp_config = {'tools.allow.on': True,
'tools.allow.methods': ['POST']} 'tools.allow.methods': ['POST']}
@cherrypy.expose
def resume(self, namespace): def resume(self, namespace):
logging.statistics.get(namespace, {})['Enabled'] = True logging.statistics.get(namespace, {})['Enabled'] = True
raise cherrypy.HTTPRedirect('./') raise cherrypy.HTTPRedirect('./')
resume.exposed = True
resume.cp_config = {'tools.allow.on': True, resume.cp_config = {'tools.allow.on': True,
'tools.allow.methods': ['POST']} 'tools.allow.methods': ['POST']}

View file

@ -4,8 +4,11 @@ import logging
import re import re
from hashlib import md5 from hashlib import md5
import six
from six.moves import urllib
import cherrypy import cherrypy
from cherrypy._cpcompat import basestring, unicodestr from cherrypy._cpcompat import text_or_bytes
from cherrypy.lib import httputil as _httputil from cherrypy.lib import httputil as _httputil
from cherrypy.lib import is_iterator from cherrypy.lib import is_iterator
@ -31,7 +34,7 @@ def validate_etags(autotags=False, debug=False):
response = cherrypy.serving.response response = cherrypy.serving.response
# Guard against being run twice. # Guard against being run twice.
if hasattr(response, "ETag"): if hasattr(response, 'ETag'):
return return
status, reason, msg = _httputil.valid_status(response.status) status, reason, msg = _httputil.valid_status(response.status)
@ -70,24 +73,24 @@ def validate_etags(autotags=False, debug=False):
if debug: if debug:
cherrypy.log('If-Match conditions: %s' % repr(conditions), cherrypy.log('If-Match conditions: %s' % repr(conditions),
'TOOLS.ETAGS') 'TOOLS.ETAGS')
if conditions and not (conditions == ["*"] or etag in conditions): if conditions and not (conditions == ['*'] or etag in conditions):
raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did " raise cherrypy.HTTPError(412, 'If-Match failed: ETag %r did '
"not match %r" % (etag, conditions)) 'not match %r' % (etag, conditions))
conditions = request.headers.elements('If-None-Match') or [] conditions = request.headers.elements('If-None-Match') or []
conditions = [str(x) for x in conditions] conditions = [str(x) for x in conditions]
if debug: if debug:
cherrypy.log('If-None-Match conditions: %s' % repr(conditions), cherrypy.log('If-None-Match conditions: %s' % repr(conditions),
'TOOLS.ETAGS') 'TOOLS.ETAGS')
if conditions == ["*"] or etag in conditions: if conditions == ['*'] or etag in conditions:
if debug: if debug:
cherrypy.log('request.method: %s' % cherrypy.log('request.method: %s' %
request.method, 'TOOLS.ETAGS') request.method, 'TOOLS.ETAGS')
if request.method in ("GET", "HEAD"): if request.method in ('GET', 'HEAD'):
raise cherrypy.HTTPRedirect([], 304) raise cherrypy.HTTPRedirect([], 304)
else: else:
raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r " raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r '
"matched %r" % (etag, conditions)) 'matched %r' % (etag, conditions))
def validate_since(): def validate_since():
@ -111,7 +114,7 @@ def validate_since():
since = request.headers.get('If-Modified-Since') since = request.headers.get('If-Modified-Since')
if since and since == lastmod: if since and since == lastmod:
if (status >= 200 and status <= 299) or status == 304: if (status >= 200 and status <= 299) or status == 304:
if request.method in ("GET", "HEAD"): if request.method in ('GET', 'HEAD'):
raise cherrypy.HTTPRedirect([], 304) raise cherrypy.HTTPRedirect([], 304)
else: else:
raise cherrypy.HTTPError(412) raise cherrypy.HTTPError(412)
@ -184,7 +187,7 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
# This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https' # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https'
scheme = s scheme = s
if not scheme: if not scheme:
scheme = request.base[:request.base.find("://")] scheme = request.base[:request.base.find('://')]
if local: if local:
lbase = request.headers.get(local, None) lbase = request.headers.get(local, None)
@ -193,14 +196,12 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
if lbase is not None: if lbase is not None:
base = lbase.split(',')[0] base = lbase.split(',')[0]
if not base: if not base:
base = request.headers.get('Host', '127.0.0.1') default = urllib.parse.urlparse(request.base).netloc
port = request.local.port base = request.headers.get('Host', default)
if port != 80 and not base.endswith(':%s' % port):
base += ':%s' % port
if base.find("://") == -1: if base.find('://') == -1:
# add http:// or https:// if needed # add http:// or https:// if needed
base = scheme + "://" + base base = scheme + '://' + base
request.base = base request.base = base
@ -210,8 +211,8 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY') cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
if xff: if xff:
if remote == 'X-Forwarded-For': if remote == 'X-Forwarded-For':
#Bug #1268 # Grab the first IP in a comma-separated list. Ref #1268.
xff = xff.split(',')[0].strip() xff = next(ip.strip() for ip in xff.split(','))
request.remote.ip = xff request.remote.ip = xff
@ -238,6 +239,8 @@ def response_headers(headers=None, debug=False):
'TOOLS.RESPONSE_HEADERS') 'TOOLS.RESPONSE_HEADERS')
for name, value in (headers or []): for name, value in (headers or []):
cherrypy.serving.response.headers[name] = value cherrypy.serving.response.headers[name] = value
response_headers.failsafe = True response_headers.failsafe = True
@ -283,7 +286,7 @@ class SessionAuth(object):
"""Assert that the user is logged in.""" """Assert that the user is logged in."""
session_key = "username" session_key = 'username'
debug = False debug = False
def check_username_and_password(self, username, password): def check_username_and_password(self, username, password):
@ -304,7 +307,7 @@ class SessionAuth(object):
def login_screen(self, from_page='..', username='', error_msg='', def login_screen(self, from_page='..', username='', error_msg='',
**kwargs): **kwargs):
return (unicodestr("""<html><body> return (six.text_type("""<html><body>
Message: %(error_msg)s Message: %(error_msg)s
<form method="post" action="do_login"> <form method="post" action="do_login">
Login: <input type="text" name="username" value="%(username)s" size="10" /> Login: <input type="text" name="username" value="%(username)s" size="10" />
@ -315,7 +318,7 @@ Message: %(error_msg)s
<br /> <br />
<input type="submit" /> <input type="submit" />
</form> </form>
</body></html>""") % vars()).encode("utf-8") </body></html>""") % vars()).encode('utf-8')
def do_login(self, username, password, from_page='..', **kwargs): def do_login(self, username, password, from_page='..', **kwargs):
"""Login. May raise redirect, or return True if request handled.""" """Login. May raise redirect, or return True if request handled."""
@ -324,15 +327,15 @@ Message: %(error_msg)s
if error_msg: if error_msg:
body = self.login_screen(from_page, username, error_msg) body = self.login_screen(from_page, username, error_msg)
response.body = body response.body = body
if "Content-Length" in response.headers: if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it. # Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"] del response.headers['Content-Length']
return True return True
else: else:
cherrypy.serving.request.login = username cherrypy.serving.request.login = username
cherrypy.session[self.session_key] = username cherrypy.session[self.session_key] = username
self.on_login(username) self.on_login(username)
raise cherrypy.HTTPRedirect(from_page or "/") raise cherrypy.HTTPRedirect(from_page or '/')
def do_logout(self, from_page='..', **kwargs): def do_logout(self, from_page='..', **kwargs):
"""Logout. May raise redirect, or return True if request handled.""" """Logout. May raise redirect, or return True if request handled."""
@ -362,9 +365,9 @@ Message: %(error_msg)s
locals(), locals(),
) )
response.body = self.login_screen(url) response.body = self.login_screen(url)
if "Content-Length" in response.headers: if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it. # Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"] del response.headers['Content-Length']
return True return True
self._debug_message('Setting request.login to %(username)r', locals()) self._debug_message('Setting request.login to %(username)r', locals())
request.login = username request.login = username
@ -386,14 +389,14 @@ Message: %(error_msg)s
return True return True
elif path.endswith('do_login'): elif path.endswith('do_login'):
if request.method != 'POST': if request.method != 'POST':
response.headers['Allow'] = "POST" response.headers['Allow'] = 'POST'
self._debug_message('do_login requires POST') self._debug_message('do_login requires POST')
raise cherrypy.HTTPError(405) raise cherrypy.HTTPError(405)
self._debug_message('routing %(path)r to do_login', locals()) self._debug_message('routing %(path)r to do_login', locals())
return self.do_login(**request.params) return self.do_login(**request.params)
elif path.endswith('do_logout'): elif path.endswith('do_logout'):
if request.method != 'POST': if request.method != 'POST':
response.headers['Allow'] = "POST" response.headers['Allow'] = 'POST'
raise cherrypy.HTTPError(405) raise cherrypy.HTTPError(405)
self._debug_message('routing %(path)r to do_logout', locals()) self._debug_message('routing %(path)r to do_logout', locals())
return self.do_logout(**request.params) return self.do_logout(**request.params)
@ -407,24 +410,28 @@ def session_auth(**kwargs):
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(sa, k, v) setattr(sa, k, v)
return sa.run() return sa.run()
session_auth.__doc__ = """Session authentication hook.
Any attribute of the SessionAuth class may be overridden via a keyword arg
to this function:
""" + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__) session_auth.__doc__ = (
for k in dir(SessionAuth) if not k.startswith("__")]) """Session authentication hook.
Any attribute of the SessionAuth class may be overridden via a keyword arg
to this function:
""" + '\n'.join(['%s: %s' % (k, type(getattr(SessionAuth, k)).__name__)
for k in dir(SessionAuth) if not k.startswith('__')])
)
def log_traceback(severity=logging.ERROR, debug=False): def log_traceback(severity=logging.ERROR, debug=False):
"""Write the last error's traceback to the cherrypy error log.""" """Write the last error's traceback to the cherrypy error log."""
cherrypy.log("", "HTTP", severity=severity, traceback=True) cherrypy.log('', 'HTTP', severity=severity, traceback=True)
def log_request_headers(debug=False): def log_request_headers(debug=False):
"""Write request headers to the cherrypy error log.""" """Write request headers to the cherrypy error log."""
h = [" %s: %s" % (k, v) for k, v in cherrypy.serving.request.header_list] h = [' %s: %s' % (k, v) for k, v in cherrypy.serving.request.header_list]
cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP") cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), 'HTTP')
def log_hooks(debug=False): def log_hooks(debug=False):
@ -440,13 +447,13 @@ def log_hooks(debug=False):
points.append(k) points.append(k)
for k in points: for k in points:
msg.append(" %s:" % k) msg.append(' %s:' % k)
v = request.hooks.get(k, []) v = request.hooks.get(k, [])
v.sort() v.sort()
for h in v: for h in v:
msg.append(" %r" % h) msg.append(' %r' % h)
cherrypy.log('\nRequest Hooks for ' + cherrypy.url() + cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
':\n' + '\n'.join(msg), "HTTP") ':\n' + '\n'.join(msg), 'HTTP')
def redirect(url='', internal=True, debug=False): def redirect(url='', internal=True, debug=False):
@ -531,7 +538,7 @@ def accept(media=None, debug=False):
""" """
if not media: if not media:
return return
if isinstance(media, basestring): if isinstance(media, text_or_bytes):
media = [media] media = [media]
request = cherrypy.serving.request request = cherrypy.serving.request
@ -547,12 +554,12 @@ def accept(media=None, debug=False):
# Note that 'ranges' is sorted in order of preference # Note that 'ranges' is sorted in order of preference
for element in ranges: for element in ranges:
if element.qvalue > 0: if element.qvalue > 0:
if element.value == "*/*": if element.value == '*/*':
# Matches any type or subtype # Matches any type or subtype
if debug: if debug:
cherrypy.log('Match due to */*', 'TOOLS.ACCEPT') cherrypy.log('Match due to */*', 'TOOLS.ACCEPT')
return media[0] return media[0]
elif element.value.endswith("/*"): elif element.value.endswith('/*'):
# Matches any subtype # Matches any subtype
mtype = element.value[:-1] # Keep the slash mtype = element.value[:-1] # Keep the slash
for m in media: for m in media:
@ -572,36 +579,23 @@ def accept(media=None, debug=False):
# No suitable media-range found. # No suitable media-range found.
ah = request.headers.get('Accept') ah = request.headers.get('Accept')
if ah is None: if ah is None:
msg = "Your client did not send an Accept header." msg = 'Your client did not send an Accept header.'
else: else:
msg = "Your client sent this Accept header: %s." % ah msg = 'Your client sent this Accept header: %s.' % ah
msg += (" But this resource only emits these media types: %s." % msg += (' But this resource only emits these media types: %s.' %
", ".join(media)) ', '.join(media))
raise cherrypy.HTTPError(406, msg) raise cherrypy.HTTPError(406, msg)
class MonitoredHeaderMap(_httputil.HeaderMap): class MonitoredHeaderMap(_httputil.HeaderMap):
def transform_key(self, key):
self.accessed_headers.add(key)
return super(MonitoredHeaderMap, self).transform_key(key)
def __init__(self): def __init__(self):
self.accessed_headers = set() self.accessed_headers = set()
super(MonitoredHeaderMap, self).__init__()
def __getitem__(self, key):
self.accessed_headers.add(key)
return _httputil.HeaderMap.__getitem__(self, key)
def __contains__(self, key):
self.accessed_headers.add(key)
return _httputil.HeaderMap.__contains__(self, key)
def get(self, key, default=None):
self.accessed_headers.add(key)
return _httputil.HeaderMap.get(self, key, default=default)
if hasattr({}, 'has_key'):
# Python 2
def has_key(self, key):
self.accessed_headers.add(key)
return _httputil.HeaderMap.has_key(self, key)
def autovary(ignore=None, debug=False): def autovary(ignore=None, debug=False):
@ -628,3 +622,19 @@ def autovary(ignore=None, debug=False):
v.sort() v.sort()
resp_h['Vary'] = ', '.join(v) resp_h['Vary'] = ', '.join(v)
request.hooks.attach('before_finalize', set_response_header, 95) request.hooks.attach('before_finalize', set_response_header, 95)
def convert_params(exception=ValueError, error=400):
"""Convert request params based on function annotations, with error handling.
exception
Exception class to catch.
status
The HTTP error code to return to the client on failure.
"""
request = cherrypy.serving.request
types = request.handler.callable.__annotations__
with cherrypy.HTTPError.handle(exception, error):
for key in set(types).intersection(request.params):
request.params[key] = types[key](request.params[key])

View file

@ -1,8 +1,11 @@
import struct import struct
import time import time
import io
import six
import cherrypy import cherrypy
from cherrypy._cpcompat import basestring, BytesIO, ntob, unicodestr from cherrypy._cpcompat import text_or_bytes
from cherrypy.lib import file_generator from cherrypy.lib import file_generator
from cherrypy.lib import is_closable_iterator from cherrypy.lib import is_closable_iterator
from cherrypy.lib import set_vary_header from cherrypy.lib import set_vary_header
@ -34,6 +37,7 @@ def decode(encoding=None, default_encoding='utf-8'):
default_encoding = [default_encoding] default_encoding = [default_encoding]
body.attempt_charsets = body.attempt_charsets + default_encoding body.attempt_charsets = body.attempt_charsets + default_encoding
class UTF8StreamEncoder: class UTF8StreamEncoder:
def __init__(self, iterator): def __init__(self, iterator):
self._iterator = iterator self._iterator = iterator
@ -46,7 +50,7 @@ class UTF8StreamEncoder:
def __next__(self): def __next__(self):
res = next(self._iterator) res = next(self._iterator)
if isinstance(res, unicodestr): if isinstance(res, six.text_type):
res = res.encode('utf-8') res = res.encode('utf-8')
return res return res
@ -63,7 +67,7 @@ class UTF8StreamEncoder:
class ResponseEncoder: class ResponseEncoder:
default_encoding = 'utf-8' default_encoding = 'utf-8'
failmsg = "Response body could not be encoded with %r." failmsg = 'Response body could not be encoded with %r.'
encoding = None encoding = None
errors = 'strict' errors = 'strict'
text_only = True text_only = True
@ -95,7 +99,7 @@ class ResponseEncoder:
def encoder(body): def encoder(body):
for chunk in body: for chunk in body:
if isinstance(chunk, unicodestr): if isinstance(chunk, six.text_type):
chunk = chunk.encode(encoding, self.errors) chunk = chunk.encode(encoding, self.errors)
yield chunk yield chunk
self.body = encoder(self.body) self.body = encoder(self.body)
@ -108,7 +112,7 @@ class ResponseEncoder:
self.attempted_charsets.add(encoding) self.attempted_charsets.add(encoding)
body = [] body = []
for chunk in self.body: for chunk in self.body:
if isinstance(chunk, unicodestr): if isinstance(chunk, six.text_type):
try: try:
chunk = chunk.encode(encoding, self.errors) chunk = chunk.encode(encoding, self.errors)
except (LookupError, UnicodeError): except (LookupError, UnicodeError):
@ -128,7 +132,7 @@ class ResponseEncoder:
encoder = self.encode_stream encoder = self.encode_stream
else: else:
encoder = self.encode_string encoder = self.encode_string
if "Content-Length" in response.headers: if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it. # Delete Content-Length header so finalize() recalcs it.
# Encoded strings may be of different lengths from their # Encoded strings may be of different lengths from their
# unicode equivalents, and even from each other. For example: # unicode equivalents, and even from each other. For example:
@ -139,7 +143,7 @@ class ResponseEncoder:
# 6 # 6
# >>> len(t.encode("utf7")) # >>> len(t.encode("utf7"))
# 8 # 8
del response.headers["Content-Length"] del response.headers['Content-Length']
# Parse the Accept-Charset request header, and try to provide one # Parse the Accept-Charset request header, and try to provide one
# of the requested charsets (in order of user preference). # of the requested charsets (in order of user preference).
@ -154,7 +158,7 @@ class ResponseEncoder:
if self.debug: if self.debug:
cherrypy.log('Specified encoding %r' % cherrypy.log('Specified encoding %r' %
encoding, 'TOOLS.ENCODE') encoding, 'TOOLS.ENCODE')
if (not charsets) or "*" in charsets or encoding in charsets: if (not charsets) or '*' in charsets or encoding in charsets:
if self.debug: if self.debug:
cherrypy.log('Attempting encoding %r' % cherrypy.log('Attempting encoding %r' %
encoding, 'TOOLS.ENCODE') encoding, 'TOOLS.ENCODE')
@ -174,7 +178,7 @@ class ResponseEncoder:
else: else:
for element in encs: for element in encs:
if element.qvalue > 0: if element.qvalue > 0:
if element.value == "*": if element.value == '*':
# Matches any charset. Try our default. # Matches any charset. Try our default.
if self.debug: if self.debug:
cherrypy.log('Attempting default encoding due ' cherrypy.log('Attempting default encoding due '
@ -189,7 +193,7 @@ class ResponseEncoder:
if encoder(encoding): if encoder(encoding):
return encoding return encoding
if "*" not in charsets: if '*' not in charsets:
# If no "*" is present in an Accept-Charset field, then all # If no "*" is present in an Accept-Charset field, then all
# character sets not explicitly mentioned get a quality # character sets not explicitly mentioned get a quality
# value of 0, except for ISO-8859-1, which gets a quality # value of 0, except for ISO-8859-1, which gets a quality
@ -205,39 +209,27 @@ class ResponseEncoder:
# No suitable encoding found. # No suitable encoding found.
ac = request.headers.get('Accept-Charset') ac = request.headers.get('Accept-Charset')
if ac is None: if ac is None:
msg = "Your client did not send an Accept-Charset header." msg = 'Your client did not send an Accept-Charset header.'
else: else:
msg = "Your client sent this Accept-Charset header: %s." % ac msg = 'Your client sent this Accept-Charset header: %s.' % ac
_charsets = ", ".join(sorted(self.attempted_charsets)) _charsets = ', '.join(sorted(self.attempted_charsets))
msg += " We tried these charsets: %s." % (_charsets,) msg += ' We tried these charsets: %s.' % (_charsets,)
raise cherrypy.HTTPError(406, msg) raise cherrypy.HTTPError(406, msg)
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
response = cherrypy.serving.response response = cherrypy.serving.response
self.body = self.oldhandler(*args, **kwargs) self.body = self.oldhandler(*args, **kwargs)
if isinstance(self.body, basestring): self.body = prepare_iter(self.body)
# strings get wrapped in a list because iterating over a single
# item list is much faster than iterating over every character
# in a long string.
if self.body:
self.body = [self.body]
else:
# [''] doesn't evaluate to False, so replace it with [].
self.body = []
elif hasattr(self.body, 'read'):
self.body = file_generator(self.body)
elif self.body is None:
self.body = []
ct = response.headers.elements("Content-Type") ct = response.headers.elements('Content-Type')
if self.debug: if self.debug:
cherrypy.log('Content-Type: %r' % [str(h) cherrypy.log('Content-Type: %r' % [str(h)
for h in ct], 'TOOLS.ENCODE') for h in ct], 'TOOLS.ENCODE')
if ct and self.add_charset: if ct and self.add_charset:
ct = ct[0] ct = ct[0]
if self.text_only: if self.text_only:
if ct.value.lower().startswith("text/"): if ct.value.lower().startswith('text/'):
if self.debug: if self.debug:
cherrypy.log( cherrypy.log(
'Content-Type %s starts with "text/"' % ct, 'Content-Type %s starts with "text/"' % ct,
@ -261,10 +253,33 @@ class ResponseEncoder:
if self.debug: if self.debug:
cherrypy.log('Setting Content-Type %s' % ct, cherrypy.log('Setting Content-Type %s' % ct,
'TOOLS.ENCODE') 'TOOLS.ENCODE')
response.headers["Content-Type"] = str(ct) response.headers['Content-Type'] = str(ct)
return self.body return self.body
def prepare_iter(value):
"""
Ensure response body is iterable and resolves to False when empty.
"""
if isinstance(value, text_or_bytes):
# strings get wrapped in a list because iterating over a single
# item list is much faster than iterating over every character
# in a long string.
if value:
value = [value]
else:
# [''] doesn't evaluate to False, so replace it with [].
value = []
# Don't use isinstance here; io.IOBase which has an ABC takes
# 1000 times as long as, say, isinstance(value, str)
elif hasattr(value, 'read'):
value = file_generator(value)
elif value is None:
value = []
return value
# GZIP # GZIP
@ -273,15 +288,15 @@ def compress(body, compress_level):
import zlib import zlib
# See http://www.gzip.org/zlib/rfc-gzip.html # See http://www.gzip.org/zlib/rfc-gzip.html
yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker yield b'\x1f\x8b' # ID1 and ID2: gzip marker
yield ntob('\x08') # CM: compression method yield b'\x08' # CM: compression method
yield ntob('\x00') # FLG: none set yield b'\x00' # FLG: none set
# MTIME: 4 bytes # MTIME: 4 bytes
yield struct.pack("<L", int(time.time()) & int('FFFFFFFF', 16)) yield struct.pack('<L', int(time.time()) & int('FFFFFFFF', 16))
yield ntob('\x02') # XFL: max compression, slowest algo yield b'\x02' # XFL: max compression, slowest algo
yield ntob('\xff') # OS: unknown yield b'\xff' # OS: unknown
crc = zlib.crc32(ntob("")) crc = zlib.crc32(b'')
size = 0 size = 0
zobj = zlib.compressobj(compress_level, zobj = zlib.compressobj(compress_level,
zlib.DEFLATED, -zlib.MAX_WBITS, zlib.DEFLATED, -zlib.MAX_WBITS,
@ -293,15 +308,15 @@ def compress(body, compress_level):
yield zobj.flush() yield zobj.flush()
# CRC32: 4 bytes # CRC32: 4 bytes
yield struct.pack("<L", crc & int('FFFFFFFF', 16)) yield struct.pack('<L', crc & int('FFFFFFFF', 16))
# ISIZE: 4 bytes # ISIZE: 4 bytes
yield struct.pack("<L", size & int('FFFFFFFF', 16)) yield struct.pack('<L', size & int('FFFFFFFF', 16))
def decompress(body): def decompress(body):
import gzip import gzip
zbuf = BytesIO() zbuf = io.BytesIO()
zbuf.write(body) zbuf.write(body)
zbuf.seek(0) zbuf.seek(0)
zfile = gzip.GzipFile(mode='rb', fileobj=zbuf) zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)
@ -318,9 +333,9 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
values in the mime_types arg before calling this function. values in the mime_types arg before calling this function.
The provided list of mime-types must be of one of the following form: The provided list of mime-types must be of one of the following form:
* type/subtype * `type/subtype`
* type/* * `type/*`
* type/*+subtype * `type/*+subtype`
No compression is performed if any of the following hold: No compression is performed if any of the following hold:
* The client sends no Accept-Encoding request header * The client sends no Accept-Encoding request header
@ -332,7 +347,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
set_vary_header(response, "Accept-Encoding") set_vary_header(response, 'Accept-Encoding')
if not response.body: if not response.body:
# Response body is empty (might be a 304 for instance) # Response body is empty (might be a 304 for instance)
@ -342,7 +357,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
# If returning cached content (which should already have been gzipped), # If returning cached content (which should already have been gzipped),
# don't re-zip. # don't re-zip.
if getattr(request, "cached", False): if getattr(request, 'cached', False):
if debug: if debug:
cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP') cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
return return
@ -410,12 +425,12 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
# Return a generator that compresses the page # Return a generator that compresses the page
response.headers['Content-Encoding'] = 'gzip' response.headers['Content-Encoding'] = 'gzip'
response.body = compress(response.body, compress_level) response.body = compress(response.body, compress_level)
if "Content-Length" in response.headers: if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it. # Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"] del response.headers['Content-Length']
return return
if debug: if debug:
cherrypy.log('No acceptable encoding found.', context='GZIP') cherrypy.log('No acceptable encoding found.', context='GZIP')
cherrypy.HTTPError(406, "identity, gzip").set_response() cherrypy.HTTPError(406, 'identity, gzip').set_response()

View file

@ -1,6 +1,5 @@
import gc import gc
import inspect import inspect
import os
import sys import sys
import time import time
@ -36,7 +35,7 @@ class ReferrerTree(object):
refs = gc.get_referrers(obj) refs = gc.get_referrers(obj)
self.ignore.append(refs) self.ignore.append(refs)
if len(refs) > self.maxparents: if len(refs) > self.maxparents:
return [("[%s referrers]" % len(refs), [])] return [('[%s referrers]' % len(refs), [])]
try: try:
ascendcode = self.ascend.__code__ ascendcode = self.ascend.__code__
@ -72,20 +71,20 @@ class ReferrerTree(object):
return self.peek(repr(obj)) return self.peek(repr(obj))
if isinstance(obj, dict): if isinstance(obj, dict):
return "{" + ", ".join(["%s: %s" % (self._format(k, descend=False), return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False),
self._format(v, descend=False)) self._format(v, descend=False))
for k, v in obj.items()]) + "}" for k, v in obj.items()]) + '}'
elif isinstance(obj, list): elif isinstance(obj, list):
return "[" + ", ".join([self._format(item, descend=False) return '[' + ', '.join([self._format(item, descend=False)
for item in obj]) + "]" for item in obj]) + ']'
elif isinstance(obj, tuple): elif isinstance(obj, tuple):
return "(" + ", ".join([self._format(item, descend=False) return '(' + ', '.join([self._format(item, descend=False)
for item in obj]) + ")" for item in obj]) + ')'
r = self.peek(repr(obj)) r = self.peek(repr(obj))
if isinstance(obj, (str, int, float)): if isinstance(obj, (str, int, float)):
return r return r
return "%s: %s" % (type(obj), r) return '%s: %s' % (type(obj), r)
def format(self, tree): def format(self, tree):
"""Return a list of string reprs from a nested list of referrers.""" """Return a list of string reprs from a nested list of referrers."""
@ -93,7 +92,7 @@ class ReferrerTree(object):
def ascend(branch, depth=1): def ascend(branch, depth=1):
for parent, grandparents in branch: for parent, grandparents in branch:
output.append((" " * depth) + self._format(parent)) output.append((' ' * depth) + self._format(parent))
if grandparents: if grandparents:
ascend(grandparents, depth + 1) ascend(grandparents, depth + 1)
ascend(tree) ascend(tree)
@ -114,20 +113,22 @@ class RequestCounter(SimplePlugin):
def after_request(self): def after_request(self):
self.count -= 1 self.count -= 1
request_counter = RequestCounter(cherrypy.engine) request_counter = RequestCounter(cherrypy.engine)
request_counter.subscribe() request_counter.subscribe()
def get_context(obj): def get_context(obj):
if isinstance(obj, _cprequest.Request): if isinstance(obj, _cprequest.Request):
return "path=%s;stage=%s" % (obj.path_info, obj.stage) return 'path=%s;stage=%s' % (obj.path_info, obj.stage)
elif isinstance(obj, _cprequest.Response): elif isinstance(obj, _cprequest.Response):
return "status=%s" % obj.status return 'status=%s' % obj.status
elif isinstance(obj, _cpwsgi.AppResponse): elif isinstance(obj, _cpwsgi.AppResponse):
return "PATH_INFO=%s" % obj.environ.get('PATH_INFO', '') return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '')
elif hasattr(obj, "tb_lineno"): elif hasattr(obj, 'tb_lineno'):
return "tb_lineno=%s" % obj.tb_lineno return 'tb_lineno=%s' % obj.tb_lineno
return "" return ''
class GCRoot(object): class GCRoot(object):
@ -136,26 +137,27 @@ class GCRoot(object):
classes = [ classes = [
(_cprequest.Request, 2, 2, (_cprequest.Request, 2, 2,
"Should be 1 in this request thread and 1 in the main thread."), 'Should be 1 in this request thread and 1 in the main thread.'),
(_cprequest.Response, 2, 2, (_cprequest.Response, 2, 2,
"Should be 1 in this request thread and 1 in the main thread."), 'Should be 1 in this request thread and 1 in the main thread.'),
(_cpwsgi.AppResponse, 1, 1, (_cpwsgi.AppResponse, 1, 1,
"Should be 1 in this request thread only."), 'Should be 1 in this request thread only.'),
] ]
@cherrypy.expose
def index(self): def index(self):
return "Hello, world!" return 'Hello, world!'
index.exposed = True
@cherrypy.expose
def stats(self): def stats(self):
output = ["Statistics:"] output = ['Statistics:']
for trial in range(10): for trial in range(10):
if request_counter.count > 0: if request_counter.count > 0:
break break
time.sleep(0.5) time.sleep(0.5)
else: else:
output.append("\nNot all requests closed properly.") output.append('\nNot all requests closed properly.')
# gc_collect isn't perfectly synchronous, because it may # gc_collect isn't perfectly synchronous, because it may
# break reference cycles that then take time to fully # break reference cycles that then take time to fully
@ -173,11 +175,11 @@ class GCRoot(object):
for x in gc.garbage: for x in gc.garbage:
trash[type(x)] = trash.get(type(x), 0) + 1 trash[type(x)] = trash.get(type(x), 0) + 1
if trash: if trash:
output.insert(0, "\n%s unreachable objects:" % unreachable) output.insert(0, '\n%s unreachable objects:' % unreachable)
trash = [(v, k) for k, v in trash.items()] trash = [(v, k) for k, v in trash.items()]
trash.sort() trash.sort()
for pair in trash: for pair in trash:
output.append(" " + repr(pair)) output.append(' ' + repr(pair))
# Check declared classes to verify uncollected instances. # Check declared classes to verify uncollected instances.
# These don't have to be part of a cycle; they can be # These don't have to be part of a cycle; they can be
@ -193,25 +195,24 @@ class GCRoot(object):
if lenobj < minobj or lenobj > maxobj: if lenobj < minobj or lenobj > maxobj:
if minobj == maxobj: if minobj == maxobj:
output.append( output.append(
"\nExpected %s %r references, got %s." % '\nExpected %s %r references, got %s.' %
(minobj, cls, lenobj)) (minobj, cls, lenobj))
else: else:
output.append( output.append(
"\nExpected %s to %s %r references, got %s." % '\nExpected %s to %s %r references, got %s.' %
(minobj, maxobj, cls, lenobj)) (minobj, maxobj, cls, lenobj))
for obj in objs: for obj in objs:
if objgraph is not None: if objgraph is not None:
ig = [id(objs), id(inspect.currentframe())] ig = [id(objs), id(inspect.currentframe())]
fname = "graph_%s_%s.png" % (cls.__name__, id(obj)) fname = 'graph_%s_%s.png' % (cls.__name__, id(obj))
objgraph.show_backrefs( objgraph.show_backrefs(
obj, extra_ignore=ig, max_depth=4, too_many=20, obj, extra_ignore=ig, max_depth=4, too_many=20,
filename=fname, extra_info=get_context) filename=fname, extra_info=get_context)
output.append("\nReferrers for %s (refcount=%s):" % output.append('\nReferrers for %s (refcount=%s):' %
(repr(obj), sys.getrefcount(obj))) (repr(obj), sys.getrefcount(obj)))
t = ReferrerTree(ignore=[objs], maxdepth=3) t = ReferrerTree(ignore=[objs], maxdepth=3)
tree = t.ascend(obj) tree = t.ascend(obj)
output.extend(t.format(tree)) output.extend(t.format(tree))
return "\n".join(output) return '\n'.join(output)
stats.exposed = True

View file

@ -1,6 +0,0 @@
import warnings
warnings.warn('cherrypy.lib.http has been deprecated and will be removed '
'in CherryPy 3.3 use cherrypy.lib.httputil instead.',
DeprecationWarning)
from cherrypy.lib.httputil import *

View file

@ -1,373 +0,0 @@
"""
This module defines functions to implement HTTP Digest Authentication
(:rfc:`2617`).
This has full compliance with 'Digest' and 'Basic' authentication methods. In
'Digest' it supports both MD5 and MD5-sess algorithms.
Usage:
First use 'doAuth' to request the client authentication for a
certain resource. You should send an httplib.UNAUTHORIZED response to the
client so he knows he has to authenticate itself.
Then use 'parseAuthorization' to retrieve the 'auth_map' used in
'checkResponse'.
To use 'checkResponse' you must have already verified the password
associated with the 'username' key in 'auth_map' dict. Then you use the
'checkResponse' function to verify if the password matches the one sent
by the client.
SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms
SUPPORTED_QOP - list of supported 'Digest' 'qop'.
"""
__version__ = 1, 0, 1
__author__ = "Tiago Cogumbreiro <cogumbreiro@users.sf.net>"
__credits__ = """
Peter van Kampen for its recipe which implement most of Digest
authentication:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378
"""
__license__ = """
Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net>
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 Sylvain Hellegouarch nor the names of his
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 OWNER 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.
"""
__all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse",
"parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey",
"calculateNonce", "SUPPORTED_QOP")
##########################################################################
import time
from hashlib import md5
from cherrypy._cpcompat import base64_decode, ntob
from cherrypy._cpcompat import parse_http_list, parse_keqv_list
MD5 = "MD5"
MD5_SESS = "MD5-sess"
AUTH = "auth"
AUTH_INT = "auth-int"
SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
SUPPORTED_QOP = (AUTH, AUTH_INT)
##########################################################################
# doAuth
#
DIGEST_AUTH_ENCODERS = {
MD5: lambda val: md5(ntob(val)).hexdigest(),
MD5_SESS: lambda val: md5(ntob(val)).hexdigest(),
# SHA: lambda val: sha.new(ntob(val)).hexdigest (),
}
def calculateNonce(realm, algorithm=MD5):
"""This is an auxaliary function that calculates 'nonce' value. It is used
to handle sessions."""
global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS
assert algorithm in SUPPORTED_ALGORITHM
try:
encoder = DIGEST_AUTH_ENCODERS[algorithm]
except KeyError:
raise NotImplementedError("The chosen algorithm (%s) does not have "
"an implementation yet" % algorithm)
return encoder("%d:%s" % (time.time(), realm))
def digestAuth(realm, algorithm=MD5, nonce=None, qop=AUTH):
"""Challenges the client for a Digest authentication."""
global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP
assert algorithm in SUPPORTED_ALGORITHM
assert qop in SUPPORTED_QOP
if nonce is None:
nonce = calculateNonce(realm, algorithm)
return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
realm, nonce, algorithm, qop
)
def basicAuth(realm):
"""Challengenes the client for a Basic authentication."""
assert '"' not in realm, "Realms cannot contain the \" (quote) character."
return 'Basic realm="%s"' % realm
def doAuth(realm):
"""'doAuth' function returns the challenge string b giving priority over
Digest and fallback to Basic authentication when the browser doesn't
support the first one.
This should be set in the HTTP header under the key 'WWW-Authenticate'."""
return digestAuth(realm) + " " + basicAuth(realm)
##########################################################################
# Parse authorization parameters
#
def _parseDigestAuthorization(auth_params):
# Convert the auth params to a dict
items = parse_http_list(auth_params)
params = parse_keqv_list(items)
# Now validate the params
# Check for required parameters
required = ["username", "realm", "nonce", "uri", "response"]
for k in required:
if k not in params:
return None
# If qop is sent then cnonce and nc MUST be present
if "qop" in params and not ("cnonce" in params
and "nc" in params):
return None
# If qop is not sent, neither cnonce nor nc can be present
if ("cnonce" in params or "nc" in params) and \
"qop" not in params:
return None
return params
def _parseBasicAuthorization(auth_params):
username, password = base64_decode(auth_params).split(":", 1)
return {"username": username, "password": password}
AUTH_SCHEMES = {
"basic": _parseBasicAuthorization,
"digest": _parseDigestAuthorization,
}
def parseAuthorization(credentials):
"""parseAuthorization will convert the value of the 'Authorization' key in
the HTTP header to a map itself. If the parsing fails 'None' is returned.
"""
global AUTH_SCHEMES
auth_scheme, auth_params = credentials.split(" ", 1)
auth_scheme = auth_scheme.lower()
parser = AUTH_SCHEMES[auth_scheme]
params = parser(auth_params)
if params is None:
return
assert "auth_scheme" not in params
params["auth_scheme"] = auth_scheme
return params
##########################################################################
# Check provided response for a valid password
#
def md5SessionKey(params, password):
"""
If the "algorithm" directive's value is "MD5-sess", then A1
[the session key] is calculated only once - on the first request by the
client following receipt of a WWW-Authenticate challenge from the server.
This creates a 'session key' for the authentication of subsequent
requests and responses which is different for each "authentication
session", thus limiting the amount of material hashed with any one
key.
Because the server need only use the hash of the user
credentials in order to create the A1 value, this construction could
be used in conjunction with a third party authentication service so
that the web server would not need the actual password value. The
specification of such a protocol is beyond the scope of this
specification.
"""
keys = ("username", "realm", "nonce", "cnonce")
params_copy = {}
for key in keys:
params_copy[key] = params[key]
params_copy["algorithm"] = MD5_SESS
return _A1(params_copy, password)
def _A1(params, password):
algorithm = params.get("algorithm", MD5)
H = DIGEST_AUTH_ENCODERS[algorithm]
if algorithm == MD5:
# If the "algorithm" directive's value is "MD5" or is
# unspecified, then A1 is:
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
return "%s:%s:%s" % (params["username"], params["realm"], password)
elif algorithm == MD5_SESS:
# This is A1 if qop is set
# A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
# ":" unq(nonce-value) ":" unq(cnonce-value)
h_a1 = H("%s:%s:%s" % (params["username"], params["realm"], password))
return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"])
def _A2(params, method, kwargs):
# If the "qop" directive's value is "auth" or is unspecified, then A2 is:
# A2 = Method ":" digest-uri-value
qop = params.get("qop", "auth")
if qop == "auth":
return method + ":" + params["uri"]
elif qop == "auth-int":
# If the "qop" value is "auth-int", then A2 is:
# A2 = Method ":" digest-uri-value ":" H(entity-body)
entity_body = kwargs.get("entity_body", "")
H = kwargs["H"]
return "%s:%s:%s" % (
method,
params["uri"],
H(entity_body)
)
else:
raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
def _computeDigestResponse(auth_map, password, method="GET", A1=None,
**kwargs):
"""
Generates a response respecting the algorithm defined in RFC 2617
"""
params = auth_map
algorithm = params.get("algorithm", MD5)
H = DIGEST_AUTH_ENCODERS[algorithm]
KD = lambda secret, data: H(secret + ":" + data)
qop = params.get("qop", None)
H_A2 = H(_A2(params, method, kwargs))
if algorithm == MD5_SESS and A1 is not None:
H_A1 = H(A1)
else:
H_A1 = H(_A1(params, password))
if qop in ("auth", "auth-int"):
# If the "qop" value is "auth" or "auth-int":
# request-digest = <"> < KD ( H(A1), unq(nonce-value)
# ":" nc-value
# ":" unq(cnonce-value)
# ":" unq(qop-value)
# ":" H(A2)
# ) <">
request = "%s:%s:%s:%s:%s" % (
params["nonce"],
params["nc"],
params["cnonce"],
params["qop"],
H_A2,
)
elif qop is None:
# If the "qop" directive is not present (this construction is
# for compatibility with RFC 2069):
# request-digest =
# <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
request = "%s:%s" % (params["nonce"], H_A2)
return KD(H_A1, request)
def _checkDigestResponse(auth_map, password, method="GET", A1=None, **kwargs):
"""This function is used to verify the response given by the client when
he tries to authenticate.
Optional arguments:
entity_body - when 'qop' is set to 'auth-int' you MUST provide the
raw data you are going to send to the client (usually the
HTML page.
request_uri - the uri from the request line compared with the 'uri'
directive of the authorization map. They must represent
the same resource (unused at this time).
"""
if auth_map['realm'] != kwargs.get('realm', None):
return False
response = _computeDigestResponse(
auth_map, password, method, A1, **kwargs)
return response == auth_map["response"]
def _checkBasicResponse(auth_map, password, method='GET', encrypt=None,
**kwargs):
# Note that the Basic response doesn't provide the realm value so we cannot
# test it
pass_through = lambda password, username=None: password
encrypt = encrypt or pass_through
try:
candidate = encrypt(auth_map["password"], auth_map["username"])
except TypeError:
# if encrypt only takes one parameter, it's the password
candidate = encrypt(auth_map["password"])
return candidate == password
AUTH_RESPONSES = {
"basic": _checkBasicResponse,
"digest": _checkDigestResponse,
}
def checkResponse(auth_map, password, method="GET", encrypt=None, **kwargs):
"""'checkResponse' compares the auth_map with the password and optionally
other arguments that each implementation might need.
If the response is of type 'Basic' then the function has the following
signature::
checkBasicResponse(auth_map, password) -> bool
If the response is of type 'Digest' then the function has the following
signature::
checkDigestResponse(auth_map, password, method='GET', A1=None) -> bool
The 'A1' argument is only used in MD5_SESS algorithm based responses.
Check md5SessionKey() for more info.
"""
checker = AUTH_RESPONSES[auth_map["auth_scheme"]]
return checker(auth_map, password, method=method, encrypt=encrypt,
**kwargs)

View file

@ -7,13 +7,24 @@ FuManChu will personally hang you up by your thumbs and submit you
to a public caning. to a public caning.
""" """
import functools
import email.utils
import re
from binascii import b2a_base64 from binascii import b2a_base64
from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou from cgi import parse_header
from cherrypy._cpcompat import basestring, bytestr, iteritems, nativestr from email.header import decode_header
from cherrypy._cpcompat import reversed, sorted, unicodestr, unquote_qs
import six
from six.moves import range, builtins, map
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
import cherrypy
from cherrypy._cpcompat import ntob, ntou
from cherrypy._cpcompat import unquote_plus
response_codes = BaseHTTPRequestHandler.responses.copy() response_codes = BaseHTTPRequestHandler.responses.copy()
# From https://bitbucket.org/cherrypy/cherrypy/issue/361 # From https://github.com/cherrypy/cherrypy/issues/361
response_codes[500] = ('Internal Server Error', response_codes[500] = ('Internal Server Error',
'The server encountered an unexpected condition ' 'The server encountered an unexpected condition '
'which prevented it from fulfilling the request.') 'which prevented it from fulfilling the request.')
@ -22,34 +33,34 @@ response_codes[503] = ('Service Unavailable',
'request due to a temporary overloading or ' 'request due to a temporary overloading or '
'maintenance of the server.') 'maintenance of the server.')
import re
import urllib HTTPDate = functools.partial(email.utils.formatdate, usegmt=True)
def urljoin(*atoms): def urljoin(*atoms):
"""Return the given path \*atoms, joined into a single URL. r"""Return the given path \*atoms, joined into a single URL.
This will correctly join a SCRIPT_NAME and PATH_INFO into the This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank. original URL, even if either atom is blank.
""" """
url = "/".join([x for x in atoms if x]) url = '/'.join([x for x in atoms if x])
while "//" in url: while '//' in url:
url = url.replace("//", "/") url = url.replace('//', '/')
# Special-case the final url of "", and return "/" instead. # Special-case the final url of "", and return "/" instead.
return url or "/" return url or '/'
def urljoin_bytes(*atoms): def urljoin_bytes(*atoms):
"""Return the given path *atoms, joined into a single URL. """Return the given path `*atoms`, joined into a single URL.
This will correctly join a SCRIPT_NAME and PATH_INFO into the This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank. original URL, even if either atom is blank.
""" """
url = ntob("/").join([x for x in atoms if x]) url = b'/'.join([x for x in atoms if x])
while ntob("//") in url: while b'//' in url:
url = url.replace(ntob("//"), ntob("/")) url = url.replace(b'//', b'/')
# Special-case the final url of "", and return "/" instead. # Special-case the final url of "", and return "/" instead.
return url or ntob("/") return url or b'/'
def protocol_from_http(protocol_str): def protocol_from_http(protocol_str):
@ -72,9 +83,9 @@ def get_ranges(headervalue, content_length):
return None return None
result = [] result = []
bytesunit, byteranges = headervalue.split("=", 1) bytesunit, byteranges = headervalue.split('=', 1)
for brange in byteranges.split(","): for brange in byteranges.split(','):
start, stop = [x.strip() for x in brange.split("-", 1)] start, stop = [x.strip() for x in brange.split('-', 1)]
if start: if start:
if not stop: if not stop:
stop = content_length - 1 stop = content_length - 1
@ -108,9 +119,9 @@ def get_ranges(headervalue, content_length):
# If the entity is shorter than the specified suffix-length, # If the entity is shorter than the specified suffix-length,
# the entire entity-body is used. # the entire entity-body is used.
if int(stop) > content_length: if int(stop) > content_length:
result.append((0, content_length)) result.append((0, content_length))
else: else:
result.append((content_length - int(stop), content_length)) result.append((content_length - int(stop), content_length))
return result return result
@ -126,14 +137,14 @@ class HeaderElement(object):
self.params = params self.params = params
def __cmp__(self, other): def __cmp__(self, other):
return cmp(self.value, other.value) return builtins.cmp(self.value, other.value)
def __lt__(self, other): def __lt__(self, other):
return self.value < other.value return self.value < other.value
def __str__(self): def __str__(self):
p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)] p = [';%s=%s' % (k, v) for k, v in six.iteritems(self.params)]
return str("%s%s" % (self.value, "".join(p))) return str('%s%s' % (self.value, ''.join(p)))
def __bytes__(self): def __bytes__(self):
return ntob(self.__str__()) return ntob(self.__str__())
@ -141,32 +152,17 @@ class HeaderElement(object):
def __unicode__(self): def __unicode__(self):
return ntou(self.__str__()) return ntou(self.__str__())
@staticmethod
def parse(elementstr): def parse(elementstr):
"""Transform 'token;key=val' to ('token', {'key': 'val'}).""" """Transform 'token;key=val' to ('token', {'key': 'val'})."""
# Split the element into a value and parameters. The 'value' may initial_value, params = parse_header(elementstr)
# be of the form, "token=token", but we don't split that here.
atoms = [x.strip() for x in elementstr.split(";") if x.strip()]
if not atoms:
initial_value = ''
else:
initial_value = atoms.pop(0).strip()
params = {}
for atom in atoms:
atom = [x.strip() for x in atom.split("=", 1) if x.strip()]
key = atom.pop(0)
if atom:
val = atom[0]
else:
val = ""
params[key] = val
return initial_value, params return initial_value, params
parse = staticmethod(parse)
@classmethod
def from_str(cls, elementstr): def from_str(cls, elementstr):
"""Construct an instance from a string of the form 'token;key=val'.""" """Construct an instance from a string of the form 'token;key=val'."""
ival, params = cls.parse(elementstr) ival, params = cls.parse(elementstr)
return cls(ival, params) return cls(ival, params)
from_str = classmethod(from_str)
q_separator = re.compile(r'; *q *=') q_separator = re.compile(r'; *q *=')
@ -183,6 +179,7 @@ class AcceptElement(HeaderElement):
have been the other way around, but it's too late to fix now. have been the other way around, but it's too late to fix now.
""" """
@classmethod
def from_str(cls, elementstr): def from_str(cls, elementstr):
qvalue = None qvalue = None
# The first "q" parameter (if any) separates the initial # The first "q" parameter (if any) separates the initial
@ -196,21 +193,35 @@ class AcceptElement(HeaderElement):
media_type, params = cls.parse(media_range) media_type, params = cls.parse(media_range)
if qvalue is not None: if qvalue is not None:
params["q"] = qvalue params['q'] = qvalue
return cls(media_type, params) return cls(media_type, params)
from_str = classmethod(from_str)
@property
def qvalue(self): def qvalue(self):
val = self.params.get("q", "1") 'The qvalue, or priority, of this value.'
val = self.params.get('q', '1')
if isinstance(val, HeaderElement): if isinstance(val, HeaderElement):
val = val.value val = val.value
return float(val) try:
qvalue = property(qvalue, doc="The qvalue, or priority, of this value.") return float(val)
except ValueError as val_err:
"""Fail client requests with invalid quality value.
Ref: https://github.com/cherrypy/cherrypy/issues/1370
"""
six.raise_from(
cherrypy.HTTPError(
400,
'Malformed HTTP header: `{}`'.
format(str(self)),
),
val_err,
)
def __cmp__(self, other): def __cmp__(self, other):
diff = cmp(self.qvalue, other.qvalue) diff = builtins.cmp(self.qvalue, other.qvalue)
if diff == 0: if diff == 0:
diff = cmp(str(self), str(other)) diff = builtins.cmp(str(self), str(other))
return diff return diff
def __lt__(self, other): def __lt__(self, other):
@ -219,7 +230,10 @@ class AcceptElement(HeaderElement):
else: else:
return self.qvalue < other.qvalue return self.qvalue < other.qvalue
RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)') RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)')
def header_elements(fieldname, fieldvalue): def header_elements(fieldname, fieldvalue):
"""Return a sorted HeaderElement list from a comma-separated header string. """Return a sorted HeaderElement list from a comma-separated header string.
""" """
@ -228,7 +242,7 @@ def header_elements(fieldname, fieldvalue):
result = [] result = []
for element in RE_HEADER_SPLIT.split(fieldvalue): for element in RE_HEADER_SPLIT.split(fieldvalue):
if fieldname.startswith("Accept") or fieldname == 'TE': if fieldname.startswith('Accept') or fieldname == 'TE':
hv = AcceptElement.from_str(element) hv = AcceptElement.from_str(element)
else: else:
hv = HeaderElement.from_str(element) hv = HeaderElement.from_str(element)
@ -238,14 +252,14 @@ def header_elements(fieldname, fieldvalue):
def decode_TEXT(value): def decode_TEXT(value):
r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr").""" r"""
try: Decode :rfc:`2047` TEXT
# Python 3
from email.header import decode_header >>> decode_TEXT("=?utf-8?q?f=C3=BCr?=") == b'f\xfcr'.decode('latin-1')
except ImportError: True
from email.Header import decode_header """
atoms = decode_header(value) atoms = decode_header(value)
decodedvalue = "" decodedvalue = ''
for atom, charset in atoms: for atom, charset in atoms:
if charset is not None: if charset is not None:
atom = atom.decode(charset) atom = atom.decode(charset)
@ -253,41 +267,51 @@ def decode_TEXT(value):
return decodedvalue return decodedvalue
def decode_TEXT_maybe(value):
"""
Decode the text but only if '=?' appears in it.
"""
return decode_TEXT(value) if '=?' in value else value
def valid_status(status): def valid_status(status):
"""Return legal HTTP status Code, Reason-phrase and Message. """Return legal HTTP status Code, Reason-phrase and Message.
The status arg must be an int, or a str that begins with an int. The status arg must be an int, a str that begins with an int
or the constant from ``http.client`` stdlib module.
If status is an int, or a str and no reason-phrase is supplied, If status has no reason-phrase is supplied, a default reason-
a default reason-phrase will be provided. phrase will be provided.
>>> from six.moves import http_client
>>> from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
>>> valid_status(http_client.ACCEPTED) == (
... int(http_client.ACCEPTED),
... ) + BaseHTTPRequestHandler.responses[http_client.ACCEPTED]
True
""" """
if not status: if not status:
status = 200 status = 200
status = str(status) code, reason = status, None
parts = status.split(" ", 1) if isinstance(status, six.string_types):
if len(parts) == 1: code, _, reason = status.partition(' ')
# No reason supplied. reason = reason.strip() or None
code, = parts
reason = None
else:
code, reason = parts
reason = reason.strip()
try: try:
code = int(code) code = int(code)
except ValueError: except (TypeError, ValueError):
raise ValueError("Illegal response status from server " raise ValueError('Illegal response status from server '
"(%s is non-numeric)." % repr(code)) '(%s is non-numeric).' % repr(code))
if code < 100 or code > 599: if code < 100 or code > 599:
raise ValueError("Illegal response status from server " raise ValueError('Illegal response status from server '
"(%s is out of range)." % repr(code)) '(%s is out of range).' % repr(code))
if code not in response_codes: if code not in response_codes:
# code is unknown but not illegal # code is unknown but not illegal
default_reason, message = "", "" default_reason, message = '', ''
else: else:
default_reason, message = response_codes[code] default_reason, message = response_codes[code]
@ -328,15 +352,15 @@ def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
nv = name_value.split('=', 1) nv = name_value.split('=', 1)
if len(nv) != 2: if len(nv) != 2:
if strict_parsing: if strict_parsing:
raise ValueError("bad query field: %r" % (name_value,)) raise ValueError('bad query field: %r' % (name_value,))
# Handle case of a control-name with no equal sign # Handle case of a control-name with no equal sign
if keep_blank_values: if keep_blank_values:
nv.append('') nv.append('')
else: else:
continue continue
if len(nv[1]) or keep_blank_values: if len(nv[1]) or keep_blank_values:
name = unquote_qs(nv[0], encoding) name = unquote_plus(nv[0], encoding, errors='strict')
value = unquote_qs(nv[1], encoding) value = unquote_plus(nv[1], encoding, errors='strict')
if name in d: if name in d:
if not isinstance(d[name], list): if not isinstance(d[name], list):
d[name] = [d[name]] d[name] = [d[name]]
@ -346,7 +370,7 @@ def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
return d return d
image_map_pattern = re.compile(r"[0-9]+,[0-9]+") image_map_pattern = re.compile(r'[0-9]+,[0-9]+')
def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'): def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
@ -359,60 +383,84 @@ def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
if image_map_pattern.match(query_string): if image_map_pattern.match(query_string):
# Server-side image map. Map the coords to 'x' and 'y' # Server-side image map. Map the coords to 'x' and 'y'
# (like CGI::Request does). # (like CGI::Request does).
pm = query_string.split(",") pm = query_string.split(',')
pm = {'x': int(pm[0]), 'y': int(pm[1])} pm = {'x': int(pm[0]), 'y': int(pm[1])}
else: else:
pm = _parse_qs(query_string, keep_blank_values, encoding=encoding) pm = _parse_qs(query_string, keep_blank_values, encoding=encoding)
return pm return pm
class CaseInsensitiveDict(dict): ####
# Inlined from jaraco.collections 1.5.2
# Ref #1673
class KeyTransformingDict(dict):
"""
A dict subclass that transforms the keys before they're used.
Subclasses may override the default transform_key to customize behavior.
"""
@staticmethod
def transform_key(key):
return key
def __init__(self, *args, **kargs):
super(KeyTransformingDict, self).__init__()
# build a dictionary using the default constructs
d = dict(*args, **kargs)
# build this dictionary using transformed keys.
for item in d.items():
self.__setitem__(*item)
def __setitem__(self, key, val):
key = self.transform_key(key)
super(KeyTransformingDict, self).__setitem__(key, val)
def __getitem__(self, key):
key = self.transform_key(key)
return super(KeyTransformingDict, self).__getitem__(key)
def __contains__(self, key):
key = self.transform_key(key)
return super(KeyTransformingDict, self).__contains__(key)
def __delitem__(self, key):
key = self.transform_key(key)
return super(KeyTransformingDict, self).__delitem__(key)
def get(self, key, *args, **kwargs):
key = self.transform_key(key)
return super(KeyTransformingDict, self).get(key, *args, **kwargs)
def setdefault(self, key, *args, **kwargs):
key = self.transform_key(key)
return super(KeyTransformingDict, self).setdefault(
key, *args, **kwargs)
def pop(self, key, *args, **kwargs):
key = self.transform_key(key)
return super(KeyTransformingDict, self).pop(key, *args, **kwargs)
def matching_key_for(self, key):
"""
Given a key, return the actual key stored in self that matches.
Raise KeyError if the key isn't found.
"""
try:
return next(e_key for e_key in self.keys() if e_key == key)
except StopIteration:
raise KeyError(key)
####
class CaseInsensitiveDict(KeyTransformingDict):
"""A case-insensitive dict subclass. """A case-insensitive dict subclass.
Each key is changed on entry to str(key).title(). Each key is changed on entry to str(key).title().
""" """
def __getitem__(self, key): @staticmethod
return dict.__getitem__(self, str(key).title()) def transform_key(key):
return str(key).title()
def __setitem__(self, key, value):
dict.__setitem__(self, str(key).title(), value)
def __delitem__(self, key):
dict.__delitem__(self, str(key).title())
def __contains__(self, key):
return dict.__contains__(self, str(key).title())
def get(self, key, default=None):
return dict.get(self, str(key).title(), default)
if hasattr({}, 'has_key'):
def has_key(self, key):
return str(key).title() in self
def update(self, E):
for k in E.keys():
self[str(k).title()] = E[k]
def fromkeys(cls, seq, value=None):
newdict = cls()
for k in seq:
newdict[str(k).title()] = value
return newdict
fromkeys = classmethod(fromkeys)
def setdefault(self, key, x=None):
key = str(key).title()
try:
return self[key]
except KeyError:
self[key] = x
return x
def pop(self, key, default):
return dict.pop(self, str(key).title(), default)
# TEXT = <any OCTET except CTLs, but including LWS> # TEXT = <any OCTET except CTLs, but including LWS>
@ -420,10 +468,10 @@ class CaseInsensitiveDict(dict):
# A CRLF is allowed in the definition of TEXT only as part of a header # A CRLF is allowed in the definition of TEXT only as part of a header
# field continuation. It is expected that the folding LWS will be # field continuation. It is expected that the folding LWS will be
# replaced with a single SP before interpretation of the TEXT value." # replaced with a single SP before interpretation of the TEXT value."
if nativestr == bytestr: if str == bytes:
header_translate_table = ''.join([chr(i) for i in xrange(256)]) header_translate_table = ''.join([chr(i) for i in range(256)])
header_translate_deletechars = ''.join( header_translate_deletechars = ''.join(
[chr(i) for i in xrange(32)]) + chr(127) [chr(i) for i in range(32)]) + chr(127)
else: else:
header_translate_table = None header_translate_table = None
header_translate_deletechars = bytes(range(32)) + bytes([127]) header_translate_deletechars = bytes(range(32)) + bytes([127])
@ -440,7 +488,7 @@ class HeaderMap(CaseInsensitiveDict):
""" """
protocol = (1, 1) protocol = (1, 1)
encodings = ["ISO-8859-1"] encodings = ['ISO-8859-1']
# Someday, when http-bis is done, this will probably get dropped # Someday, when http-bis is done, this will probably get dropped
# since few servers, clients, or intermediaries do it. But until then, # since few servers, clients, or intermediaries do it. But until then,
@ -463,31 +511,30 @@ class HeaderMap(CaseInsensitiveDict):
"""Transform self into a list of (name, value) tuples.""" """Transform self into a list of (name, value) tuples."""
return list(self.encode_header_items(self.items())) return list(self.encode_header_items(self.items()))
@classmethod
def encode_header_items(cls, header_items): def encode_header_items(cls, header_items):
""" """
Prepare the sequence of name, value tuples into a form suitable for Prepare the sequence of name, value tuples into a form suitable for
transmitting on the wire for HTTP. transmitting on the wire for HTTP.
""" """
for k, v in header_items: for k, v in header_items:
if isinstance(k, unicodestr): if not isinstance(v, six.string_types) and \
k = cls.encode(k) not isinstance(v, six.binary_type):
v = six.text_type(v)
if not isinstance(v, basestring): yield tuple(map(cls.encode_header_item, (k, v)))
v = str(v)
if isinstance(v, unicodestr): @classmethod
v = cls.encode(v) def encode_header_item(cls, item):
if isinstance(item, six.text_type):
item = cls.encode(item)
# See header_translate_* constants above. # See header_translate_* constants above.
# Replace only if you really know what you're doing. # Replace only if you really know what you're doing.
k = k.translate(header_translate_table, return item.translate(
header_translate_deletechars) header_translate_table, header_translate_deletechars)
v = v.translate(header_translate_table,
header_translate_deletechars)
yield (k, v)
encode_header_items = classmethod(encode_header_items)
@classmethod
def encode(cls, v): def encode(cls, v):
"""Return the given header name or value, encoded for HTTP output.""" """Return the given header name or value, encoded for HTTP output."""
for enc in cls.encodings: for enc in cls.encodings:
@ -503,12 +550,11 @@ class HeaderMap(CaseInsensitiveDict):
# because we never want to fold lines--folding has # because we never want to fold lines--folding has
# been deprecated by the HTTP working group. # been deprecated by the HTTP working group.
v = b2a_base64(v.encode('utf-8')) v = b2a_base64(v.encode('utf-8'))
return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?=')) return (b'=?utf-8?b?' + v.strip(b'\n') + b'?=')
raise ValueError("Could not encode header part %r using " raise ValueError('Could not encode header part %r using '
"any of the encodings %r." % 'any of the encodings %r.' %
(v, cls.encodings)) (v, cls.encodings))
encode = classmethod(encode)
class Host(object): class Host(object):
@ -521,9 +567,9 @@ class Host(object):
""" """
ip = "0.0.0.0" ip = '0.0.0.0'
port = 80 port = 80
name = "unknown.tld" name = 'unknown.tld'
def __init__(self, ip, port, name=None): def __init__(self, ip, port, name=None):
self.ip = ip self.ip = ip
@ -533,4 +579,4 @@ class Host(object):
self.name = name self.name = name
def __repr__(self): def __repr__(self):
return "httputil.Host(%r, %r, %r)" % (self.ip, self.port, self.name) return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name)

View file

@ -1,17 +1,15 @@
import cherrypy import cherrypy
from cherrypy._cpcompat import basestring, ntou, json_encode, json_decode from cherrypy._cpcompat import text_or_bytes, ntou, json_encode, json_decode
def json_processor(entity): def json_processor(entity):
"""Read application/json data into request.json.""" """Read application/json data into request.json."""
if not entity.headers.get(ntou("Content-Length"), ntou("")): if not entity.headers.get(ntou('Content-Length'), ntou('')):
raise cherrypy.HTTPError(411) raise cherrypy.HTTPError(411)
body = entity.fp.read() body = entity.fp.read()
try: with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'):
cherrypy.serving.request.json = json_decode(body.decode('utf-8')) cherrypy.serving.request.json = json_decode(body.decode('utf-8'))
except ValueError:
raise cherrypy.HTTPError(400, 'Invalid JSON document')
def json_in(content_type=[ntou('application/json'), ntou('text/javascript')], def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
@ -36,12 +34,9 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
request header, or it will raise "411 Length Required". If for any request header, or it will raise "411 Length Required". If for any
other reason the request entity cannot be deserialized from JSON, other reason the request entity cannot be deserialized from JSON,
it will raise "400 Bad Request: Invalid JSON document". it will raise "400 Bad Request: Invalid JSON document".
You must be using Python 2.6 or greater, or have the 'simplejson'
package importable; otherwise, ValueError is raised during processing.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
if isinstance(content_type, basestring): if isinstance(content_type, text_or_bytes):
content_type = [content_type] content_type = [content_type]
if force: if force:
@ -74,9 +69,6 @@ def json_out(content_type='application/json', debug=False,
Provide your own handler to use a custom encoder. For example Provide your own handler to use a custom encoder. For example
cherrypy.config['tools.json_out.handler'] = <function>, or cherrypy.config['tools.json_out.handler'] = <function>, or
@json_out(handler=function). @json_out(handler=function).
You must be using Python 2.6 or greater, or have the 'simplejson'
package importable; otherwise, ValueError is raised during processing.
""" """
request = cherrypy.serving.request request = cherrypy.serving.request
# request.handler may be set to None by e.g. the caching tool # request.handler may be set to None by e.g. the caching tool

View file

@ -1,147 +0,0 @@
"""
Platform-independent file locking. Inspired by and modeled after zc.lockfile.
"""
import os
try:
import msvcrt
except ImportError:
pass
try:
import fcntl
except ImportError:
pass
class LockError(Exception):
"Could not obtain a lock"
msg = "Unable to lock %r"
def __init__(self, path):
super(LockError, self).__init__(self.msg % path)
class UnlockError(LockError):
"Could not release a lock"
msg = "Unable to unlock %r"
# first, a default, naive locking implementation
class LockFile(object):
"""
A default, naive locking implementation. Always fails if the file
already exists.
"""
def __init__(self, path):
self.path = path
try:
fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
except OSError:
raise LockError(self.path)
os.close(fd)
def release(self):
os.remove(self.path)
def remove(self):
pass
class SystemLockFile(object):
"""
An abstract base class for platform-specific locking.
"""
def __init__(self, path):
self.path = path
try:
# Open lockfile for writing without truncation:
self.fp = open(path, 'r+')
except IOError:
# If the file doesn't exist, IOError is raised; Use a+ instead.
# Note that there may be a race here. Multiple processes
# could fail on the r+ open and open the file a+, but only
# one will get the the lock and write a pid.
self.fp = open(path, 'a+')
try:
self._lock_file()
except:
self.fp.seek(1)
self.fp.close()
del self.fp
raise
self.fp.write(" %s\n" % os.getpid())
self.fp.truncate()
self.fp.flush()
def release(self):
if not hasattr(self, 'fp'):
return
self._unlock_file()
self.fp.close()
del self.fp
def remove(self):
"""
Attempt to remove the file
"""
try:
os.remove(self.path)
except:
pass
#@abc.abstract_method
# def _lock_file(self):
# """Attempt to obtain the lock on self.fp. Raise LockError if not
# acquired."""
def _unlock_file(self):
"""Attempt to obtain the lock on self.fp. Raise UnlockError if not
released."""
class WindowsLockFile(SystemLockFile):
def _lock_file(self):
# Lock just the first byte
try:
msvcrt.locking(self.fp.fileno(), msvcrt.LK_NBLCK, 1)
except IOError:
raise LockError(self.fp.name)
def _unlock_file(self):
try:
self.fp.seek(0)
msvcrt.locking(self.fp.fileno(), msvcrt.LK_UNLCK, 1)
except IOError:
raise UnlockError(self.fp.name)
if 'msvcrt' in globals():
LockFile = WindowsLockFile
class UnixLockFile(SystemLockFile):
def _lock_file(self):
flags = fcntl.LOCK_EX | fcntl.LOCK_NB
try:
fcntl.flock(self.fp.fileno(), flags)
except IOError:
raise LockError(self.fp.name)
# no need to implement _unlock_file, it will be unlocked on close()
if 'fcntl' in globals():
LockFile = UnixLockFile

View file

@ -11,7 +11,7 @@ class Timer(object):
A simple timer that will indicate when an expiration time has passed. A simple timer that will indicate when an expiration time has passed.
""" """
def __init__(self, expiration): def __init__(self, expiration):
"Create a timer that expires at `expiration` (UTC datetime)" 'Create a timer that expires at `expiration` (UTC datetime)'
self.expiration = expiration self.expiration = expiration
@classmethod @classmethod
@ -26,7 +26,7 @@ class Timer(object):
class LockTimeout(Exception): class LockTimeout(Exception):
"An exception when a lock could not be acquired before a timeout period" 'An exception when a lock could not be acquired before a timeout period'
class LockChecker(object): class LockChecker(object):
@ -43,5 +43,5 @@ class LockChecker(object):
def expired(self): def expired(self):
if self.timer.expired(): if self.timer.expired():
raise LockTimeout( raise LockTimeout(
"Timeout acquiring lock for %(session_id)s" % vars(self)) 'Timeout acquiring lock for %(session_id)s' % vars(self))
return False return False

View file

@ -10,9 +10,9 @@ You can profile any of your pages as follows::
class Root: class Root:
p = profiler.Profiler("/path/to/profile/dir") p = profiler.Profiler("/path/to/profile/dir")
@cherrypy.expose
def index(self): def index(self):
self.p.run(self._index) self.p.run(self._index)
index.exposed = True
def _index(self): def _index(self):
return "Hello, world!" return "Hello, world!"
@ -33,29 +33,36 @@ module from the command line, it will call ``serve()`` for you.
""" """
import io
def new_func_strip_path(func_name):
"""Make profiler output more readable by adding `__init__` modules' parents
"""
filename, line, name = func_name
if filename.endswith("__init__.py"):
return os.path.basename(filename[:-12]) + filename[-12:], line, name
return os.path.basename(filename), line, name
try:
import profile
import pstats
pstats.func_strip_path = new_func_strip_path
except ImportError:
profile = None
pstats = None
import os import os
import os.path import os.path
import sys import sys
import warnings import warnings
from cherrypy._cpcompat import StringIO import cherrypy
try:
import profile
import pstats
def new_func_strip_path(func_name):
"""Make profiler output more readable by adding `__init__` modules' parents
"""
filename, line, name = func_name
if filename.endswith('__init__.py'):
return (
os.path.basename(filename[:-12]) + filename[-12:],
line,
name,
)
return os.path.basename(filename), line, name
pstats.func_strip_path = new_func_strip_path
except ImportError:
profile = None
pstats = None
_count = 0 _count = 0
@ -64,7 +71,7 @@ class Profiler(object):
def __init__(self, path=None): def __init__(self, path=None):
if not path: if not path:
path = os.path.join(os.path.dirname(__file__), "profile") path = os.path.join(os.path.dirname(__file__), 'profile')
self.path = path self.path = path
if not os.path.exists(path): if not os.path.exists(path):
os.makedirs(path) os.makedirs(path)
@ -73,7 +80,7 @@ class Profiler(object):
"""Dump profile data into self.path.""" """Dump profile data into self.path."""
global _count global _count
c = _count = _count + 1 c = _count = _count + 1
path = os.path.join(self.path, "cp_%04d.prof" % c) path = os.path.join(self.path, 'cp_%04d.prof' % c)
prof = profile.Profile() prof = profile.Profile()
result = prof.runcall(func, *args, **params) result = prof.runcall(func, *args, **params)
prof.dump_stats(path) prof.dump_stats(path)
@ -83,12 +90,12 @@ class Profiler(object):
""":rtype: list of available profiles. """:rtype: list of available profiles.
""" """
return [f for f in os.listdir(self.path) return [f for f in os.listdir(self.path)
if f.startswith("cp_") and f.endswith(".prof")] if f.startswith('cp_') and f.endswith('.prof')]
def stats(self, filename, sortby='cumulative'): def stats(self, filename, sortby='cumulative'):
""":rtype stats(index): output of print_stats() for the given profile. """:rtype stats(index): output of print_stats() for the given profile.
""" """
sio = StringIO() sio = io.StringIO()
if sys.version_info >= (2, 5): if sys.version_info >= (2, 5):
s = pstats.Stats(os.path.join(self.path, filename), stream=sio) s = pstats.Stats(os.path.join(self.path, filename), stream=sio)
s.strip_dirs() s.strip_dirs()
@ -110,6 +117,7 @@ class Profiler(object):
sio.close() sio.close()
return response return response
@cherrypy.expose
def index(self): def index(self):
return """<html> return """<html>
<head><title>CherryPy profile data</title></head> <head><title>CherryPy profile data</title></head>
@ -119,23 +127,21 @@ class Profiler(object):
</frameset> </frameset>
</html> </html>
""" """
index.exposed = True
@cherrypy.expose
def menu(self): def menu(self):
yield "<h2>Profiling runs</h2>" yield '<h2>Profiling runs</h2>'
yield "<p>Click on one of the runs below to see profiling data.</p>" yield '<p>Click on one of the runs below to see profiling data.</p>'
runs = self.statfiles() runs = self.statfiles()
runs.sort() runs.sort()
for i in runs: for i in runs:
yield "<a href='report?filename=%s' target='main'>%s</a><br />" % ( yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (
i, i) i, i)
menu.exposed = True
@cherrypy.expose
def report(self, filename): def report(self, filename):
import cherrypy
cherrypy.response.headers['Content-Type'] = 'text/plain' cherrypy.response.headers['Content-Type'] = 'text/plain'
return self.stats(filename) return self.stats(filename)
report.exposed = True
class ProfileAggregator(Profiler): class ProfileAggregator(Profiler):
@ -147,7 +153,7 @@ class ProfileAggregator(Profiler):
self.profiler = profile.Profile() self.profiler = profile.Profile()
def run(self, func, *args, **params): def run(self, func, *args, **params):
path = os.path.join(self.path, "cp_%04d.prof" % self.count) path = os.path.join(self.path, 'cp_%04d.prof' % self.count)
result = self.profiler.runcall(func, *args, **params) result = self.profiler.runcall(func, *args, **params)
self.profiler.dump_stats(path) self.profiler.dump_stats(path)
return result return result
@ -172,11 +178,11 @@ class make_app:
""" """
if profile is None or pstats is None: if profile is None or pstats is None:
msg = ("Your installation of Python does not have a profile " msg = ('Your installation of Python does not have a profile '
"module. If you're on Debian, try " "module. If you're on Debian, try "
"`sudo apt-get install python-profiler`. " '`sudo apt-get install python-profiler`. '
"See http://www.cherrypy.org/wiki/ProfilingOnDebian " 'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
"for details.") 'for details.')
warnings.warn(msg) warnings.warn(msg)
self.nextapp = nextapp self.nextapp = nextapp
@ -197,20 +203,19 @@ class make_app:
def serve(path=None, port=8080): def serve(path=None, port=8080):
if profile is None or pstats is None: if profile is None or pstats is None:
msg = ("Your installation of Python does not have a profile module. " msg = ('Your installation of Python does not have a profile module. '
"If you're on Debian, try " "If you're on Debian, try "
"`sudo apt-get install python-profiler`. " '`sudo apt-get install python-profiler`. '
"See http://www.cherrypy.org/wiki/ProfilingOnDebian " 'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
"for details.") 'for details.')
warnings.warn(msg) warnings.warn(msg)
import cherrypy
cherrypy.config.update({'server.socket_port': int(port), cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10, 'server.thread_pool': 10,
'environment': "production", 'environment': 'production',
}) })
cherrypy.quickstart(Profiler(path)) cherrypy.quickstart(Profiler(path))
if __name__ == "__main__": if __name__ == '__main__':
serve(*tuple(sys.argv[1:])) serve(*tuple(sys.argv[1:]))

View file

@ -18,42 +18,14 @@ by adding a named handler to Config.namespaces. The name can be any string,
and the handler must be either a callable or a context manager. and the handler must be either a callable or a context manager.
""" """
try: from cherrypy._cpcompat import text_or_bytes
# Python 3.0+ from six.moves import configparser
from configparser import ConfigParser from six.moves import builtins
except ImportError:
from ConfigParser import ConfigParser
try: import operator
set
except NameError:
from sets import Set as set
try:
basestring
except NameError:
basestring = str
try:
# Python 3
import builtins
except ImportError:
# Python 2
import __builtin__ as builtins
import operator as _operator
import sys import sys
def as_dict(config):
"""Return a dict from 'config' whether it is a dict, file, or filename."""
if isinstance(config, basestring):
config = Parser().dict_from_file(config)
elif hasattr(config, 'read'):
config = Parser().dict_from_file(config)
return config
class NamespaceSet(dict): class NamespaceSet(dict):
"""A dict of config namespace names and handlers. """A dict of config namespace names and handlers.
@ -83,19 +55,19 @@ class NamespaceSet(dict):
# Separate the given config into namespaces # Separate the given config into namespaces
ns_confs = {} ns_confs = {}
for k in config: for k in config:
if "." in k: if '.' in k:
ns, name = k.split(".", 1) ns, name = k.split('.', 1)
bucket = ns_confs.setdefault(ns, {}) bucket = ns_confs.setdefault(ns, {})
bucket[name] = config[k] bucket[name] = config[k]
# I chose __enter__ and __exit__ so someday this could be # I chose __enter__ and __exit__ so someday this could be
# rewritten using Python 2.5's 'with' statement: # rewritten using Python 2.5's 'with' statement:
# for ns, handler in self.iteritems(): # for ns, handler in six.iteritems(self):
# with handler as callable: # with handler as callable:
# for k, v in ns_confs.get(ns, {}).iteritems(): # for k, v in six.iteritems(ns_confs.get(ns, {})):
# callable(k, v) # callable(k, v)
for ns, handler in self.items(): for ns, handler in self.items():
exit = getattr(handler, "__exit__", None) exit = getattr(handler, '__exit__', None)
if exit: if exit:
callable = handler.__enter__() callable = handler.__enter__()
no_exc = True no_exc = True
@ -103,7 +75,7 @@ class NamespaceSet(dict):
try: try:
for k, v in ns_confs.get(ns, {}).items(): for k, v in ns_confs.get(ns, {}).items():
callable(k, v) callable(k, v)
except: except Exception:
# The exceptional case is handled here # The exceptional case is handled here
no_exc = False no_exc = False
if exit is None: if exit is None:
@ -120,7 +92,7 @@ class NamespaceSet(dict):
handler(k, v) handler(k, v)
def __repr__(self): def __repr__(self):
return "%s.%s(%s)" % (self.__module__, self.__class__.__name__, return '%s.%s(%s)' % (self.__module__, self.__class__.__name__,
dict.__repr__(self)) dict.__repr__(self))
def __copy__(self): def __copy__(self):
@ -154,16 +126,8 @@ class Config(dict):
dict.update(self, self.defaults) dict.update(self, self.defaults)
def update(self, config): def update(self, config):
"""Update self from a dict, file or filename.""" """Update self from a dict, file, or filename."""
if isinstance(config, basestring): self._apply(Parser.load(config))
# Filename
config = Parser().dict_from_file(config)
elif hasattr(config, 'read'):
# Open file object
config = Parser().dict_from_file(config)
else:
config = config.copy()
self._apply(config)
def _apply(self, config): def _apply(self, config):
"""Update self from a dict.""" """Update self from a dict."""
@ -182,7 +146,7 @@ class Config(dict):
self.namespaces({k: v}) self.namespaces({k: v})
class Parser(ConfigParser): class Parser(configparser.ConfigParser):
"""Sub-class of ConfigParser that keeps the case of options and that """Sub-class of ConfigParser that keeps the case of options and that
raises an exception if the file cannot be read. raises an exception if the file cannot be read.
@ -192,7 +156,7 @@ class Parser(ConfigParser):
return optionstr return optionstr
def read(self, filenames): def read(self, filenames):
if isinstance(filenames, basestring): if isinstance(filenames, text_or_bytes):
filenames = [filenames] filenames = [filenames]
for filename in filenames: for filename in filenames:
# try: # try:
@ -218,8 +182,8 @@ class Parser(ConfigParser):
value = unrepr(value) value = unrepr(value)
except Exception: except Exception:
x = sys.exc_info()[1] x = sys.exc_info()[1]
msg = ("Config error in section: %r, option: %r, " msg = ('Config error in section: %r, option: %r, '
"value: %r. Config values must be valid Python." % 'value: %r. Config values must be valid Python.' %
(section, option, value)) (section, option, value))
raise ValueError(msg, x.__class__.__name__, x.args) raise ValueError(msg, x.__class__.__name__, x.args)
result[section][option] = value result[section][option] = value
@ -232,6 +196,17 @@ class Parser(ConfigParser):
self.read(file) self.read(file)
return self.as_dict() return self.as_dict()
@classmethod
def load(self, input):
"""Resolve 'input' to dict from a dict, file, or filename."""
is_file = (
# Filename
isinstance(input, text_or_bytes)
# Open file object
or hasattr(input, 'read')
)
return Parser().dict_from_file(input) if is_file else input.copy()
# public domain "unrepr" implementation, found on the web and then improved. # public domain "unrepr" implementation, found on the web and then improved.
@ -241,7 +216,7 @@ class _Builder2:
def build(self, o): def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None) m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None: if m is None:
raise TypeError("unrepr does not recognize %s" % raise TypeError('unrepr does not recognize %s' %
repr(o.__class__.__name__)) repr(o.__class__.__name__))
return m(o) return m(o)
@ -254,7 +229,7 @@ class _Builder2:
# e.g. IronPython 1.0. # e.g. IronPython 1.0.
return eval(s) return eval(s)
p = compiler.parse("__tempvalue__ = " + s) p = compiler.parse('__tempvalue__ = ' + s)
return p.getChildren()[1].getChildren()[0].getChildren()[1] return p.getChildren()[1].getChildren()[0].getChildren()[1]
def build_Subscript(self, o): def build_Subscript(self, o):
@ -279,7 +254,7 @@ class _Builder2:
if class_name == 'Keyword': if class_name == 'Keyword':
kwargs.update(self.build(child)) kwargs.update(self.build(child))
# Everything else becomes args # Everything else becomes args
else : else:
args.append(self.build(child)) args.append(self.build(child))
return callee(*args, **kwargs) return callee(*args, **kwargs)
@ -327,7 +302,7 @@ class _Builder2:
except AttributeError: except AttributeError:
pass pass
raise TypeError("unrepr could not resolve the name %s" % repr(name)) raise TypeError('unrepr could not resolve the name %s' % repr(name))
def build_Add(self, o): def build_Add(self, o):
left, right = map(self.build, o.getChildren()) left, right = map(self.build, o.getChildren())
@ -356,7 +331,7 @@ class _Builder3:
def build(self, o): def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None) m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None: if m is None:
raise TypeError("unrepr does not recognize %s" % raise TypeError('unrepr does not recognize %s' %
repr(o.__class__.__name__)) repr(o.__class__.__name__))
return m(o) return m(o)
@ -369,7 +344,7 @@ class _Builder3:
# e.g. IronPython 1.0. # e.g. IronPython 1.0.
return eval(s) return eval(s)
p = ast.parse("__tempvalue__ = " + s) p = ast.parse('__tempvalue__ = ' + s)
return p.body[0].value return p.body[0].value
def build_Subscript(self, o): def build_Subscript(self, o):
@ -394,16 +369,16 @@ class _Builder3:
args.append(self.build(a)) args.append(self.build(a))
kwargs = {} kwargs = {}
for kw in o.keywords: for kw in o.keywords:
if kw.arg is None: # double asterix `**` if kw.arg is None: # double asterix `**`
rst = self.build(kw.value) rst = self.build(kw.value)
if not isinstance(rst, dict): if not isinstance(rst, dict):
raise TypeError("Invalid argument for call." raise TypeError('Invalid argument for call.'
"Must be a mapping object.") 'Must be a mapping object.')
# give preference to the keys set directly from arg=value # give preference to the keys set directly from arg=value
for k, v in rst.items(): for k, v in rst.items():
if k not in kwargs: if k not in kwargs:
kwargs[k] = v kwargs[k] = v
else: # defined on the call as: arg=value else: # defined on the call as: arg=value
kwargs[kw.arg] = self.build(kw.value) kwargs[kw.arg] = self.build(kw.value)
return callee(*args, **kwargs) return callee(*args, **kwargs)
@ -427,7 +402,7 @@ class _Builder3:
kwargs = {} kwargs = {}
else: else:
kwargs = self.build(o.kwargs) kwargs = self.build(o.kwargs)
if o.keywords is not None: # direct a=b keywords if o.keywords is not None: # direct a=b keywords
for kw in o.keywords: for kw in o.keywords:
# preference because is a direct keyword against **kwargs # preference because is a direct keyword against **kwargs
kwargs[kw.arg] = self.build(kw.value) kwargs[kw.arg] = self.build(kw.value)
@ -471,11 +446,13 @@ class _Builder3:
except AttributeError: except AttributeError:
pass pass
raise TypeError("unrepr could not resolve the name %s" % repr(name)) raise TypeError('unrepr could not resolve the name %s' % repr(name))
def build_NameConstant(self, o): def build_NameConstant(self, o):
return o.value return o.value
build_Constant = build_NameConstant # Python 3.8 change
def build_UnaryOp(self, o): def build_UnaryOp(self, o):
op, operand = map(self.build, [o.op, o.operand]) op, operand = map(self.build, [o.op, o.operand])
return op(operand) return op(operand)
@ -485,13 +462,13 @@ class _Builder3:
return op(left, right) return op(left, right)
def build_Add(self, o): def build_Add(self, o):
return _operator.add return operator.add
def build_Mult(self, o): def build_Mult(self, o):
return _operator.mul return operator.mul
def build_USub(self, o): def build_USub(self, o):
return _operator.neg return operator.neg
def build_Attribute(self, o): def build_Attribute(self, o):
parent = self.build(o.value) parent = self.build(o.value)
@ -523,7 +500,7 @@ def attributes(full_attribute_name):
"""Load a module and retrieve an attribute of that module.""" """Load a module and retrieve an attribute of that module."""
# Parse out the path, module, and attribute # Parse out the path, module, and attribute
last_dot = full_attribute_name.rfind(".") last_dot = full_attribute_name.rfind('.')
attr_name = full_attribute_name[last_dot + 1:] attr_name = full_attribute_name[last_dot + 1:]
mod_path = full_attribute_name[:last_dot] mod_path = full_attribute_name[:last_dot]

View file

@ -4,13 +4,13 @@ You need to edit your config file to use sessions. Here's an example::
[/] [/]
tools.sessions.on = True tools.sessions.on = True
tools.sessions.storage_type = "file" tools.sessions.storage_class = cherrypy.lib.sessions.FileSession
tools.sessions.storage_path = "/home/site/sessions" tools.sessions.storage_path = "/home/site/sessions"
tools.sessions.timeout = 60 tools.sessions.timeout = 60
This sets the session to be stored in files in the directory This sets the session to be stored in files in the directory
/home/site/sessions, and the session timeout to 60 minutes. If you omit /home/site/sessions, and the session timeout to 60 minutes. If you omit
``storage_type`` the sessions will be saved in RAM. ``storage_class``, the sessions will be saved in RAM.
``tools.sessions.on`` is the only required line for working sessions, ``tools.sessions.on`` is the only required line for working sessions,
the rest are optional. the rest are optional.
@ -57,6 +57,17 @@ However, CherryPy "recognizes" a session id by looking up the saved session
data for that id. Therefore, if you never save any session data, data for that id. Therefore, if you never save any session data,
**you will get a new session id for every request**. **you will get a new session id for every request**.
A side effect of CherryPy overwriting unrecognised session ids is that if you
have multiple, separate CherryPy applications running on a single domain (e.g.
on different ports), each app will overwrite the other's session id because by
default they use the same cookie name (``"session_id"``) but do not recognise
each others sessions. It is therefore a good idea to use a different name for
each, for example::
[/]
...
tools.sessions.name = "my_app_session_id"
================ ================
Sharing Sessions Sharing Sessions
================ ================
@ -94,15 +105,24 @@ import datetime
import os import os
import time import time
import threading import threading
import types import binascii
import six
from six.moves import cPickle as pickle
import contextlib2
import zc.lockfile
import cherrypy import cherrypy
from cherrypy._cpcompat import copyitems, pickle, random20, unicodestr
from cherrypy.lib import httputil from cherrypy.lib import httputil
from cherrypy.lib import lockfile
from cherrypy.lib import locking from cherrypy.lib import locking
from cherrypy.lib import is_iterator from cherrypy.lib import is_iterator
if six.PY2:
FileNotFoundError = OSError
missing = object() missing = object()
@ -115,17 +135,19 @@ class Session(object):
id_observers = None id_observers = None
"A list of callbacks to which to pass new id's." "A list of callbacks to which to pass new id's."
def _get_id(self): @property
def id(self):
"""Return the current session id."""
return self._id return self._id
def _set_id(self, value): @id.setter
def id(self, value):
self._id = value self._id = value
for o in self.id_observers: for o in self.id_observers:
o(value) o(value)
id = property(_get_id, _set_id, doc="The current session ID.")
timeout = 60 timeout = 60
"Number of minutes after which to delete session data." 'Number of minutes after which to delete session data.'
locked = False locked = False
""" """
@ -138,16 +160,16 @@ class Session(object):
automatically on the first attempt to access session data.""" automatically on the first attempt to access session data."""
clean_thread = None clean_thread = None
"Class-level Monitor which calls self.clean_up." 'Class-level Monitor which calls self.clean_up.'
clean_freq = 5 clean_freq = 5
"The poll rate for expired session cleanup in minutes." 'The poll rate for expired session cleanup in minutes.'
originalid = None originalid = None
"The session id passed by the client. May be missing or unsafe." 'The session id passed by the client. May be missing or unsafe.'
missing = False missing = False
"True if the session requested by the client did not exist." 'True if the session requested by the client did not exist.'
regenerated = False regenerated = False
""" """
@ -155,7 +177,7 @@ class Session(object):
internal calls to regenerate the session id.""" internal calls to regenerate the session id."""
debug = False debug = False
"If True, log debug information." 'If True, log debug information.'
# --------------------- Session management methods --------------------- # # --------------------- Session management methods --------------------- #
@ -182,7 +204,7 @@ class Session(object):
cherrypy.log('Expired or malicious session %r; ' cherrypy.log('Expired or malicious session %r; '
'making a new one' % id, 'TOOLS.SESSIONS') 'making a new one' % id, 'TOOLS.SESSIONS')
# Expired or malicious session. Make a new one. # Expired or malicious session. Make a new one.
# See https://bitbucket.org/cherrypy/cherrypy/issue/709. # See https://github.com/cherrypy/cherrypy/issues/709.
self.id = None self.id = None
self.missing = True self.missing = True
self._regenerate() self._regenerate()
@ -236,7 +258,7 @@ class Session(object):
def generate_id(self): def generate_id(self):
"""Return a new session id.""" """Return a new session id."""
return random20() return binascii.hexlify(os.urandom(20)).decode('ascii')
def save(self): def save(self):
"""Save session data.""" """Save session data."""
@ -335,13 +357,6 @@ class Session(object):
self.load() self.load()
return key in self._data return key in self._data
if hasattr({}, 'has_key'):
def has_key(self, key):
"""D.has_key(k) -> True if D has a key k, else False."""
if not self.loaded:
self.load()
return key in self._data
def get(self, key, default=None): def get(self, key, default=None):
"""D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
if not self.loaded: if not self.loaded:
@ -395,7 +410,7 @@ class RamSession(Session):
"""Clean up expired sessions.""" """Clean up expired sessions."""
now = self.now() now = self.now()
for _id, (data, expiration_time) in copyitems(self.cache): for _id, (data, expiration_time) in list(six.iteritems(self.cache)):
if expiration_time <= now: if expiration_time <= now:
try: try:
del self.cache[_id] del self.cache[_id]
@ -410,7 +425,11 @@ class RamSession(Session):
# added to remove obsolete lock objects # added to remove obsolete lock objects
for _id in list(self.locks): for _id in list(self.locks):
if _id not in self.cache and self.locks[_id].acquire(blocking=False): locked = (
_id not in self.cache
and self.locks[_id].acquire(blocking=False)
)
if locked:
lock = self.locks.pop(_id) lock = self.locks.pop(_id)
lock.release() lock.release()
@ -471,9 +490,11 @@ class FileSession(Session):
if isinstance(self.lock_timeout, (int, float)): if isinstance(self.lock_timeout, (int, float)):
self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout) self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout)
if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))): if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))):
raise ValueError("Lock timeout must be numeric seconds or " raise ValueError(
"a timedelta instance.") 'Lock timeout must be numeric seconds or a timedelta instance.'
)
@classmethod
def setup(cls, **kwargs): def setup(cls, **kwargs):
"""Set up the storage system for file-based sessions. """Set up the storage system for file-based sessions.
@ -485,12 +506,11 @@ class FileSession(Session):
for k, v in kwargs.items(): for k, v in kwargs.items():
setattr(cls, k, v) setattr(cls, k, v)
setup = classmethod(setup)
def _get_file_path(self): def _get_file_path(self):
f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
if not os.path.abspath(f).startswith(self.storage_path): if not os.path.abspath(f).startswith(self.storage_path):
raise cherrypy.HTTPError(400, "Invalid session id in cookie.") raise cherrypy.HTTPError(400, 'Invalid session id in cookie.')
return f return f
def _exists(self): def _exists(self):
@ -498,12 +518,12 @@ class FileSession(Session):
return os.path.exists(path) return os.path.exists(path)
def _load(self, path=None): def _load(self, path=None):
assert self.locked, ("The session load without being locked. " assert self.locked, ('The session load without being locked. '
"Check your tools' priority levels.") "Check your tools' priority levels.")
if path is None: if path is None:
path = self._get_file_path() path = self._get_file_path()
try: try:
f = open(path, "rb") f = open(path, 'rb')
try: try:
return pickle.load(f) return pickle.load(f)
finally: finally:
@ -511,21 +531,21 @@ class FileSession(Session):
except (IOError, EOFError): except (IOError, EOFError):
e = sys.exc_info()[1] e = sys.exc_info()[1]
if self.debug: if self.debug:
cherrypy.log("Error loading the session pickle: %s" % cherrypy.log('Error loading the session pickle: %s' %
e, 'TOOLS.SESSIONS') e, 'TOOLS.SESSIONS')
return None return None
def _save(self, expiration_time): def _save(self, expiration_time):
assert self.locked, ("The session was saved without being locked. " assert self.locked, ('The session was saved without being locked. '
"Check your tools' priority levels.") "Check your tools' priority levels.")
f = open(self._get_file_path(), "wb") f = open(self._get_file_path(), 'wb')
try: try:
pickle.dump((self._data, expiration_time), f, self.pickle_protocol) pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
finally: finally:
f.close() f.close()
def _delete(self): def _delete(self):
assert self.locked, ("The session deletion without being locked. " assert self.locked, ('The session deletion without being locked. '
"Check your tools' priority levels.") "Check your tools' priority levels.")
try: try:
os.unlink(self._get_file_path()) os.unlink(self._get_file_path())
@ -540,8 +560,8 @@ class FileSession(Session):
checker = locking.LockChecker(self.id, self.lock_timeout) checker = locking.LockChecker(self.id, self.lock_timeout)
while not checker.expired(): while not checker.expired():
try: try:
self.lock = lockfile.LockFile(path) self.lock = zc.lockfile.LockFile(path)
except lockfile.LockError: except zc.lockfile.LockError:
time.sleep(0.1) time.sleep(0.1)
else: else:
break break
@ -551,8 +571,9 @@ class FileSession(Session):
def release_lock(self, path=None): def release_lock(self, path=None):
"""Release the lock on the currently-loaded session data.""" """Release the lock on the currently-loaded session data."""
self.lock.release() self.lock.close()
self.lock.remove() with contextlib2.suppress(FileNotFoundError):
os.remove(self.lock._path)
self.locked = False self.locked = False
def clean_up(self): def clean_up(self):
@ -560,8 +581,11 @@ class FileSession(Session):
now = self.now() now = self.now()
# Iterate over all session files in self.storage_path # Iterate over all session files in self.storage_path
for fname in os.listdir(self.storage_path): for fname in os.listdir(self.storage_path):
if (fname.startswith(self.SESSION_PREFIX) have_session = (
and not fname.endswith(self.LOCK_SUFFIX)): fname.startswith(self.SESSION_PREFIX)
and not fname.endswith(self.LOCK_SUFFIX)
)
if have_session:
# We have a session file: lock and load it and check # We have a session file: lock and load it and check
# if it's expired. If it fails, nevermind. # if it's expired. If it fails, nevermind.
path = os.path.join(self.storage_path, fname) path = os.path.join(self.storage_path, fname)
@ -587,95 +611,8 @@ class FileSession(Session):
def __len__(self): def __len__(self):
"""Return the number of active sessions.""" """Return the number of active sessions."""
return len([fname for fname in os.listdir(self.storage_path) return len([fname for fname in os.listdir(self.storage_path)
if (fname.startswith(self.SESSION_PREFIX) if (fname.startswith(self.SESSION_PREFIX) and
and not fname.endswith(self.LOCK_SUFFIX))]) not fname.endswith(self.LOCK_SUFFIX))])
class PostgresqlSession(Session):
""" Implementation of the PostgreSQL backend for sessions. It assumes
a table like this::
create table session (
id varchar(40),
data text,
expiration_time timestamp
)
You must provide your own get_db function.
"""
pickle_protocol = pickle.HIGHEST_PROTOCOL
def __init__(self, id=None, **kwargs):
Session.__init__(self, id, **kwargs)
self.cursor = self.db.cursor()
def setup(cls, **kwargs):
"""Set up the storage system for Postgres-based sessions.
This should only be called once per process; this will be done
automatically when using sessions.init (as the built-in Tool does).
"""
for k, v in kwargs.items():
setattr(cls, k, v)
self.db = self.get_db()
setup = classmethod(setup)
def __del__(self):
if self.cursor:
self.cursor.close()
self.db.commit()
def _exists(self):
# Select session data from table
self.cursor.execute('select data, expiration_time from session '
'where id=%s', (self.id,))
rows = self.cursor.fetchall()
return bool(rows)
def _load(self):
# Select session data from table
self.cursor.execute('select data, expiration_time from session '
'where id=%s', (self.id,))
rows = self.cursor.fetchall()
if not rows:
return None
pickled_data, expiration_time = rows[0]
data = pickle.loads(pickled_data)
return data, expiration_time
def _save(self, expiration_time):
pickled_data = pickle.dumps(self._data, self.pickle_protocol)
self.cursor.execute('update session set data = %s, '
'expiration_time = %s where id = %s',
(pickled_data, expiration_time, self.id))
def _delete(self):
self.cursor.execute('delete from session where id=%s', (self.id,))
def acquire_lock(self):
"""Acquire an exclusive lock on the currently-loaded session data."""
# We use the "for update" clause to lock the row
self.locked = True
self.cursor.execute('select id from session where id=%s for update',
(self.id,))
if self.debug:
cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
def release_lock(self):
"""Release the lock on the currently-loaded session data."""
# We just close the cursor and that will remove the lock
# introduced by the "for update" clause
self.cursor.close()
self.locked = False
def clean_up(self):
"""Clean up expired sessions."""
self.cursor.execute('delete from session where expiration_time < %s',
(self.now(),))
class MemcachedSession(Session): class MemcachedSession(Session):
@ -684,11 +621,12 @@ class MemcachedSession(Session):
# Wrap all .get and .set operations in a single lock. # Wrap all .get and .set operations in a single lock.
mc_lock = threading.RLock() mc_lock = threading.RLock()
# This is a seperate set of locks per session id. # This is a separate set of locks per session id.
locks = {} locks = {}
servers = ['127.0.0.1:11211'] servers = ['127.0.0.1:11211']
@classmethod
def setup(cls, **kwargs): def setup(cls, **kwargs):
"""Set up the storage system for memcached-based sessions. """Set up the storage system for memcached-based sessions.
@ -700,21 +638,6 @@ class MemcachedSession(Session):
import memcache import memcache
cls.cache = memcache.Client(cls.servers) cls.cache = memcache.Client(cls.servers)
setup = classmethod(setup)
def _get_id(self):
return self._id
def _set_id(self, value):
# This encode() call is where we differ from the superclass.
# Memcache keys MUST be byte strings, not unicode.
if isinstance(value, unicodestr):
value = value.encode('utf-8')
self._id = value
for o in self.id_observers:
o(value)
id = property(_get_id, _set_id, doc="The current session ID.")
def _exists(self): def _exists(self):
self.mc_lock.acquire() self.mc_lock.acquire()
@ -737,7 +660,7 @@ class MemcachedSession(Session):
try: try:
if not self.cache.set(self.id, (self._data, expiration_time), td): if not self.cache.set(self.id, (self._data, expiration_time), td):
raise AssertionError( raise AssertionError(
"Session data for id %r not set." % self.id) 'Session data for id %r not set.' % self.id)
finally: finally:
self.mc_lock.release() self.mc_lock.release()
@ -766,13 +689,13 @@ class MemcachedSession(Session):
def save(): def save():
"""Save any changed session data.""" """Save any changed session data."""
if not hasattr(cherrypy.serving, "session"): if not hasattr(cherrypy.serving, 'session'):
return return
request = cherrypy.serving.request request = cherrypy.serving.request
response = cherrypy.serving.response response = cherrypy.serving.response
# Guard against running twice # Guard against running twice
if hasattr(request, "_sessionsaved"): if hasattr(request, '_sessionsaved'):
return return
request._sessionsaved = True request._sessionsaved = True
@ -786,28 +709,39 @@ def save():
if is_iterator(response.body): if is_iterator(response.body):
response.collapse_body() response.collapse_body()
cherrypy.session.save() cherrypy.session.save()
save.failsafe = True save.failsafe = True
def close(): def close():
"""Close the session object for this request.""" """Close the session object for this request."""
sess = getattr(cherrypy.serving, "session", None) sess = getattr(cherrypy.serving, 'session', None)
if getattr(sess, "locked", False): if getattr(sess, 'locked', False):
# If the session is still locked we release the lock # If the session is still locked we release the lock
sess.release_lock() sess.release_lock()
if sess.debug: if sess.debug:
cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS') cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS')
close.failsafe = True close.failsafe = True
close.priority = 90 close.priority = 90
def init(storage_type='ram', path=None, path_header=None, name='session_id', def init(storage_type=None, path=None, path_header=None, name='session_id',
timeout=60, domain=None, secure=False, clean_freq=5, timeout=60, domain=None, secure=False, clean_freq=5,
persistent=True, httponly=False, debug=False, **kwargs): persistent=True, httponly=False, debug=False,
# Py27 compat
# *, storage_class=RamSession,
**kwargs):
"""Initialize session object (using cookies). """Initialize session object (using cookies).
storage_class
The Session subclass to use. Defaults to RamSession.
storage_type storage_type
One of 'ram', 'file', 'postgresql', 'memcached'. This will be (deprecated)
One of 'ram', 'file', memcached'. This will be
used to look up the corresponding class in cherrypy.lib.sessions used to look up the corresponding class in cherrypy.lib.sessions
globals. For example, 'file' will use the FileSession class. globals. For example, 'file' will use the FileSession class.
@ -851,10 +785,13 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
you're using for more information. you're using for more information.
""" """
# Py27 compat
storage_class = kwargs.pop('storage_class', RamSession)
request = cherrypy.serving.request request = cherrypy.serving.request
# Guard against running twice # Guard against running twice
if hasattr(request, "_session_init_flag"): if hasattr(request, '_session_init_flag'):
return return
request._session_init_flag = True request._session_init_flag = True
@ -866,11 +803,18 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
cherrypy.log('ID obtained from request.cookie: %r' % id, cherrypy.log('ID obtained from request.cookie: %r' % id,
'TOOLS.SESSIONS') 'TOOLS.SESSIONS')
# Find the storage class and call setup (first time only). first_time = not hasattr(cherrypy, 'session')
storage_class = storage_type.title() + 'Session'
storage_class = globals()[storage_class] if storage_type:
if not hasattr(cherrypy, "session"): if first_time:
if hasattr(storage_class, "setup"): msg = 'storage_type is deprecated. Supply storage_class instead'
cherrypy.log(msg)
storage_class = storage_type.title() + 'Session'
storage_class = globals()[storage_class]
# call setup first time only
if first_time:
if hasattr(storage_class, 'setup'):
storage_class.setup(**kwargs) storage_class.setup(**kwargs)
# Create and attach a new Session instance to cherrypy.serving. # Create and attach a new Session instance to cherrypy.serving.
@ -887,7 +831,7 @@ def init(storage_type='ram', path=None, path_header=None, name='session_id',
sess.id_observers.append(update_cookie) sess.id_observers.append(update_cookie)
# Create cherrypy.session which will proxy to cherrypy.serving.session # Create cherrypy.session which will proxy to cherrypy.serving.session
if not hasattr(cherrypy, "session"): if not hasattr(cherrypy, 'session'):
cherrypy.session = cherrypy._ThreadLocalProxy('session') cherrypy.session = cherrypy._ThreadLocalProxy('session')
if persistent: if persistent:
@ -941,24 +885,30 @@ def set_response_cookie(path=None, path_header=None, name='session_id',
'/' '/'
) )
# We'd like to use the "max-age" param as indicated in
# http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
# save it to disk and the session is lost if people close
# the browser. So we have to use the old "expires" ... sigh ...
## cookie[name]['max-age'] = timeout * 60
if timeout: if timeout:
e = time.time() + (timeout * 60) cookie[name]['max-age'] = timeout * 60
cookie[name]['expires'] = httputil.HTTPDate(e) _add_MSIE_max_age_workaround(cookie[name], timeout)
if domain is not None: if domain is not None:
cookie[name]['domain'] = domain cookie[name]['domain'] = domain
if secure: if secure:
cookie[name]['secure'] = 1 cookie[name]['secure'] = 1
if httponly: if httponly:
if not cookie[name].isReservedKey('httponly'): if not cookie[name].isReservedKey('httponly'):
raise ValueError("The httponly cookie token is not supported.") raise ValueError('The httponly cookie token is not supported.')
cookie[name]['httponly'] = 1 cookie[name]['httponly'] = 1
def _add_MSIE_max_age_workaround(cookie, timeout):
"""
We'd like to use the "max-age" param as indicated in
http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
save it to disk and the session is lost if people close
the browser. So we have to use the old "expires" ... sigh ...
"""
expires = time.time() + timeout * 60
cookie['expires'] = httputil.HTTPDate(expires)
def expire(): def expire():
"""Expire the current session cookie.""" """Expire the current session cookie."""
name = cherrypy.serving.request.config.get( name = cherrypy.serving.request.config.get(
@ -966,3 +916,4 @@ def expire():
one_year = 60 * 60 * 24 * 365 one_year = 60 * 60 * 24 * 365
e = time.time() - one_year e = time.time() - one_year
cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e) cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e)
cherrypy.serving.response.cookie[name].pop('max-age', None)

View file

@ -1,23 +1,32 @@
"""Module with helpers for serving static files."""
import os import os
import platform
import re import re
import stat import stat
import mimetypes import mimetypes
try: from email.generator import _make_boundary as make_boundary
from io import UnsupportedOperation from io import UnsupportedOperation
except ImportError:
UnsupportedOperation = object() from six.moves import urllib
import cherrypy import cherrypy
from cherrypy._cpcompat import ntob, unquote from cherrypy._cpcompat import ntob
from cherrypy.lib import cptools, httputil, file_generator_limited from cherrypy.lib import cptools, httputil, file_generator_limited
mimetypes.init() def _setup_mimetypes():
mimetypes.types_map['.dwg'] = 'image/x-dwg' """Pre-initialize global mimetype map."""
mimetypes.types_map['.ico'] = 'image/x-icon' if not mimetypes.inited:
mimetypes.types_map['.bz2'] = 'application/x-bzip2' mimetypes.init()
mimetypes.types_map['.gz'] = 'application/x-gzip' mimetypes.types_map['.dwg'] = 'image/x-dwg'
mimetypes.types_map['.ico'] = 'image/x-icon'
mimetypes.types_map['.bz2'] = 'application/x-bzip2'
mimetypes.types_map['.gz'] = 'application/x-gzip'
_setup_mimetypes()
def serve_file(path, content_type=None, disposition=None, name=None, def serve_file(path, content_type=None, disposition=None, name=None,
@ -33,7 +42,6 @@ def serve_file(path, content_type=None, disposition=None, name=None,
to the basename of path. If disposition is None, no Content-Disposition to the basename of path. If disposition is None, no Content-Disposition
header will be written. header will be written.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response
# If path is relative, users should fix it by making path absolute. # If path is relative, users should fix it by making path absolute.
@ -71,7 +79,7 @@ def serve_file(path, content_type=None, disposition=None, name=None,
if content_type is None: if content_type is None:
# Set content-type based on filename extension # Set content-type based on filename extension
ext = "" ext = ''
i = path.rfind('.') i = path.rfind('.')
if i != -1: if i != -1:
ext = path[i:].lower() ext = path[i:].lower()
@ -86,7 +94,7 @@ def serve_file(path, content_type=None, disposition=None, name=None,
if name is None: if name is None:
name = os.path.basename(path) name = os.path.basename(path)
cd = '%s; filename="%s"' % (disposition, name) cd = '%s; filename="%s"' % (disposition, name)
response.headers["Content-Disposition"] = cd response.headers['Content-Disposition'] = cd
if debug: if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
@ -115,7 +123,6 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
serve_fileobj(), expecting that the data would be served starting from that serve_fileobj(), expecting that the data would be served starting from that
position. position.
""" """
response = cherrypy.serving.response response = cherrypy.serving.response
try: try:
@ -144,7 +151,7 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
cd = disposition cd = disposition
else: else:
cd = '%s; filename="%s"' % (disposition, name) cd = '%s; filename="%s"' % (disposition, name)
response.headers["Content-Disposition"] = cd response.headers['Content-Disposition'] = cd
if debug: if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
@ -158,12 +165,12 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
# HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
request = cherrypy.serving.request request = cherrypy.serving.request
if request.protocol >= (1, 1): if request.protocol >= (1, 1):
response.headers["Accept-Ranges"] = "bytes" response.headers['Accept-Ranges'] = 'bytes'
r = httputil.get_ranges(request.headers.get('Range'), content_length) r = httputil.get_ranges(request.headers.get('Range'), content_length)
if r == []: if r == []:
response.headers['Content-Range'] = "bytes */%s" % content_length response.headers['Content-Range'] = 'bytes */%s' % content_length
message = ("Invalid Range (first-byte-pos greater than " message = ('Invalid Range (first-byte-pos greater than '
"Content-Length)") 'Content-Length)')
if debug: if debug:
cherrypy.log(message, 'TOOLS.STATIC') cherrypy.log(message, 'TOOLS.STATIC')
raise cherrypy.HTTPError(416, message) raise cherrypy.HTTPError(416, message)
@ -179,31 +186,25 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
cherrypy.log( cherrypy.log(
'Single part; start: %r, stop: %r' % (start, stop), 'Single part; start: %r, stop: %r' % (start, stop),
'TOOLS.STATIC') 'TOOLS.STATIC')
response.status = "206 Partial Content" response.status = '206 Partial Content'
response.headers['Content-Range'] = ( response.headers['Content-Range'] = (
"bytes %s-%s/%s" % (start, stop - 1, content_length)) 'bytes %s-%s/%s' % (start, stop - 1, content_length))
response.headers['Content-Length'] = r_len response.headers['Content-Length'] = r_len
fileobj.seek(start) fileobj.seek(start)
response.body = file_generator_limited(fileobj, r_len) response.body = file_generator_limited(fileobj, r_len)
else: else:
# Return a multipart/byteranges response. # Return a multipart/byteranges response.
response.status = "206 Partial Content" response.status = '206 Partial Content'
try:
# Python 3
from email.generator import _make_boundary as make_boundary
except ImportError:
# Python 2
from mimetools import choose_boundary as make_boundary
boundary = make_boundary() boundary = make_boundary()
ct = "multipart/byteranges; boundary=%s" % boundary ct = 'multipart/byteranges; boundary=%s' % boundary
response.headers['Content-Type'] = ct response.headers['Content-Type'] = ct
if "Content-Length" in response.headers: if 'Content-Length' in response.headers:
# Delete Content-Length header so finalize() recalcs it. # Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"] del response.headers['Content-Length']
def file_ranges(): def file_ranges():
# Apache compatibility: # Apache compatibility:
yield ntob("\r\n") yield b'\r\n'
for start, stop in r: for start, stop in r:
if debug: if debug:
@ -211,23 +212,23 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
'Multipart; start: %r, stop: %r' % ( 'Multipart; start: %r, stop: %r' % (
start, stop), start, stop),
'TOOLS.STATIC') 'TOOLS.STATIC')
yield ntob("--" + boundary, 'ascii') yield ntob('--' + boundary, 'ascii')
yield ntob("\r\nContent-type: %s" % content_type, yield ntob('\r\nContent-type: %s' % content_type,
'ascii') 'ascii')
yield ntob( yield ntob(
"\r\nContent-range: bytes %s-%s/%s\r\n\r\n" % ( '\r\nContent-range: bytes %s-%s/%s\r\n\r\n' % (
start, stop - 1, content_length), start, stop - 1, content_length),
'ascii') 'ascii')
fileobj.seek(start) fileobj.seek(start)
gen = file_generator_limited(fileobj, stop - start) gen = file_generator_limited(fileobj, stop - start)
for chunk in gen: for chunk in gen:
yield chunk yield chunk
yield ntob("\r\n") yield b'\r\n'
# Final boundary # Final boundary
yield ntob("--" + boundary + "--", 'ascii') yield ntob('--' + boundary + '--', 'ascii')
# Apache compatibility: # Apache compatibility:
yield ntob("\r\n") yield b'\r\n'
response.body = file_ranges() response.body = file_ranges()
return response.body return response.body
else: else:
@ -244,7 +245,7 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
def serve_download(path, name=None): def serve_download(path, name=None):
"""Serve 'path' as an application/x-download attachment.""" """Serve 'path' as an application/x-download attachment."""
# This is such a common idiom I felt it deserved its own wrapper. # This is such a common idiom I felt it deserved its own wrapper.
return serve_file(path, "application/x-download", "attachment", name) return serve_file(path, 'application/x-download', 'attachment', name)
def _attempt(filename, content_types, debug=False): def _attempt(filename, content_types, debug=False):
@ -268,7 +269,7 @@ def _attempt(filename, content_types, debug=False):
return False return False
def staticdir(section, dir, root="", match="", content_types=None, index="", def staticdir(section, dir, root='', match='', content_types=None, index='',
debug=False): debug=False):
"""Serve a static resource from the given (root +) dir. """Serve a static resource from the given (root +) dir.
@ -306,7 +307,7 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
# If dir is relative, make absolute using "root". # If dir is relative, make absolute using "root".
if not os.path.isabs(dir): if not os.path.isabs(dir):
if not root: if not root:
msg = "Static dir requires an absolute dir (or root)." msg = 'Static dir requires an absolute dir (or root).'
if debug: if debug:
cherrypy.log(msg, 'TOOLS.STATICDIR') cherrypy.log(msg, 'TOOLS.STATICDIR')
raise ValueError(msg) raise ValueError(msg)
@ -315,10 +316,18 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
# Determine where we are in the object tree relative to 'section' # Determine where we are in the object tree relative to 'section'
# (where the static tool was defined). # (where the static tool was defined).
if section == 'global': if section == 'global':
section = "/" section = '/'
section = section.rstrip(r"\/") section = section.rstrip(r'\/')
branch = request.path_info[len(section) + 1:] branch = request.path_info[len(section) + 1:]
branch = unquote(branch.lstrip(r"\/")) branch = urllib.parse.unquote(branch.lstrip(r'\/'))
# Requesting a file in sub-dir of the staticdir results
# in mixing of delimiter styles, e.g. C:\static\js/script.js.
# Windows accepts this form except not when the path is
# supplied in extended-path notation, e.g. \\?\C:\static\js/script.js.
# http://bit.ly/1vdioCX
if platform.system() == 'Windows':
branch = branch.replace('/', '\\')
# If branch is "", filename will end in a slash # If branch is "", filename will end in a slash
filename = os.path.join(dir, branch) filename = os.path.join(dir, branch)
@ -338,11 +347,11 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
if index: if index:
handled = _attempt(os.path.join(filename, index), content_types) handled = _attempt(os.path.join(filename, index), content_types)
if handled: if handled:
request.is_index = filename[-1] in (r"\/") request.is_index = filename[-1] in (r'\/')
return handled return handled
def staticfile(filename, root=None, match="", content_types=None, debug=False): def staticfile(filename, root=None, match='', content_types=None, debug=False):
"""Serve a static resource from the given (root +) filename. """Serve a static resource from the given (root +) filename.
match match

View file

@ -1,21 +1,19 @@
"""XML-RPC tool helpers."""
import sys import sys
from six.moves.xmlrpc_client import (
loads as xmlrpc_loads, dumps as xmlrpc_dumps,
Fault as XMLRPCFault
)
import cherrypy import cherrypy
from cherrypy._cpcompat import ntob from cherrypy._cpcompat import ntob
def get_xmlrpclib():
try:
import xmlrpc.client as x
except ImportError:
import xmlrpclib as x
return x
def process_body(): def process_body():
"""Return (params, method) from request body.""" """Return (params, method) from request body."""
try: try:
return get_xmlrpclib().loads(cherrypy.request.body.read()) return xmlrpc_loads(cherrypy.request.body.read())
except Exception: except Exception:
return ('ERROR PARAMS', ), 'ERRORMETHOD' return ('ERROR PARAMS', ), 'ERRORMETHOD'
@ -31,9 +29,10 @@ def patched_path(path):
def _set_response(body): def _set_response(body):
"""Set up HTTP status, headers and body within CherryPy."""
# The XML-RPC spec (http://www.xmlrpc.com/spec) says: # The XML-RPC spec (http://www.xmlrpc.com/spec) says:
# "Unless there's a lower-level error, always return 200 OK." # "Unless there's a lower-level error, always return 200 OK."
# Since Python's xmlrpclib interprets a non-200 response # Since Python's xmlrpc_client interprets a non-200 response
# as a "Protocol Error", we'll just return 200 every time. # as a "Protocol Error", we'll just return 200 every time.
response = cherrypy.response response = cherrypy.response
response.status = '200 OK' response.status = '200 OK'
@ -43,15 +42,20 @@ def _set_response(body):
def respond(body, encoding='utf-8', allow_none=0): def respond(body, encoding='utf-8', allow_none=0):
xmlrpclib = get_xmlrpclib() """Construct HTTP response body."""
if not isinstance(body, xmlrpclib.Fault): if not isinstance(body, XMLRPCFault):
body = (body,) body = (body,)
_set_response(xmlrpclib.dumps(body, methodresponse=1,
encoding=encoding, _set_response(
allow_none=allow_none)) xmlrpc_dumps(
body, methodresponse=1,
encoding=encoding,
allow_none=allow_none
)
)
def on_error(*args, **kwargs): def on_error(*args, **kwargs):
"""Construct HTTP response body for an error response."""
body = str(sys.exc_info()[1]) body = str(sys.exc_info()[1])
xmlrpclib = get_xmlrpclib() _set_response(xmlrpc_dumps(XMLRPCFault(1, body)))
_set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body)))

View file

@ -10,5 +10,8 @@ use with the bus. Some use tool-specific channels; see the documentation
for each class. for each class.
""" """
from cherrypy.process.wspbus import bus from .wspbus import bus
from cherrypy.process import plugins, servers from . import plugins, servers
__all__ = ('bus', 'plugins', 'servers')

View file

@ -7,8 +7,10 @@ import sys
import time import time
import threading import threading
from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident from six.moves import _thread
from cherrypy._cpcompat import ntob, Timer, SetDaemonProperty
from cherrypy._cpcompat import text_or_bytes
from cherrypy._cpcompat import ntob, Timer
# _module__file__base is used by Autoreload to make # _module__file__base is used by Autoreload to make
# absolute any filenames retrieved from sys.modules which are not # absolute any filenames retrieved from sys.modules which are not
@ -104,15 +106,14 @@ class SignalHandler(object):
if sys.platform[:4] == 'java': if sys.platform[:4] == 'java':
del self.handlers['SIGUSR1'] del self.handlers['SIGUSR1']
self.handlers['SIGUSR2'] = self.bus.graceful self.handlers['SIGUSR2'] = self.bus.graceful
self.bus.log("SIGUSR1 cannot be set on the JVM platform. " self.bus.log('SIGUSR1 cannot be set on the JVM platform. '
"Using SIGUSR2 instead.") 'Using SIGUSR2 instead.')
self.handlers['SIGINT'] = self._jython_SIGINT_handler self.handlers['SIGINT'] = self._jython_SIGINT_handler
self._previous_handlers = {} self._previous_handlers = {}
# used to determine is the process is a daemon in `self._is_daemonized` # used to determine is the process is a daemon in `self._is_daemonized`
self._original_pid = os.getpid() self._original_pid = os.getpid()
def _jython_SIGINT_handler(self, signum=None, frame=None): def _jython_SIGINT_handler(self, signum=None, frame=None):
# See http://bugs.jython.org/issue1313 # See http://bugs.jython.org/issue1313
self.bus.log('Keyboard Interrupt: shutting down bus') self.bus.log('Keyboard Interrupt: shutting down bus')
@ -131,12 +132,10 @@ class SignalHandler(object):
is executing inside other process like in a CI tool is executing inside other process like in a CI tool
(Buildbot, Jenkins). (Buildbot, Jenkins).
""" """
if (self._original_pid != os.getpid() and return (
not os.isatty(sys.stdin.fileno())): self._original_pid != os.getpid() and
return True not os.isatty(sys.stdin.fileno())
else: )
return False
def subscribe(self): def subscribe(self):
"""Subscribe self.handlers to signals.""" """Subscribe self.handlers to signals."""
@ -152,19 +151,19 @@ class SignalHandler(object):
signame = self.signals[signum] signame = self.signals[signum]
if handler is None: if handler is None:
self.bus.log("Restoring %s handler to SIG_DFL." % signame) self.bus.log('Restoring %s handler to SIG_DFL.' % signame)
handler = _signal.SIG_DFL handler = _signal.SIG_DFL
else: else:
self.bus.log("Restoring %s handler %r." % (signame, handler)) self.bus.log('Restoring %s handler %r.' % (signame, handler))
try: try:
our_handler = _signal.signal(signum, handler) our_handler = _signal.signal(signum, handler)
if our_handler is None: if our_handler is None:
self.bus.log("Restored old %s handler %r, but our " self.bus.log('Restored old %s handler %r, but our '
"handler was not registered." % 'handler was not registered.' %
(signame, handler), level=30) (signame, handler), level=30)
except ValueError: except ValueError:
self.bus.log("Unable to restore %s handler %r." % self.bus.log('Unable to restore %s handler %r.' %
(signame, handler), level=40, traceback=True) (signame, handler), level=40, traceback=True)
def set_handler(self, signal, listener=None): def set_handler(self, signal, listener=None):
@ -176,39 +175,39 @@ class SignalHandler(object):
If the given signal name or number is not available on the current If the given signal name or number is not available on the current
platform, ValueError is raised. platform, ValueError is raised.
""" """
if isinstance(signal, basestring): if isinstance(signal, text_or_bytes):
signum = getattr(_signal, signal, None) signum = getattr(_signal, signal, None)
if signum is None: if signum is None:
raise ValueError("No such signal: %r" % signal) raise ValueError('No such signal: %r' % signal)
signame = signal signame = signal
else: else:
try: try:
signame = self.signals[signal] signame = self.signals[signal]
except KeyError: except KeyError:
raise ValueError("No such signal: %r" % signal) raise ValueError('No such signal: %r' % signal)
signum = signal signum = signal
prev = _signal.signal(signum, self._handle_signal) prev = _signal.signal(signum, self._handle_signal)
self._previous_handlers[signum] = prev self._previous_handlers[signum] = prev
if listener is not None: if listener is not None:
self.bus.log("Listening for %s." % signame) self.bus.log('Listening for %s.' % signame)
self.bus.subscribe(signame, listener) self.bus.subscribe(signame, listener)
def _handle_signal(self, signum=None, frame=None): def _handle_signal(self, signum=None, frame=None):
"""Python signal handler (self.set_handler subscribes it for you).""" """Python signal handler (self.set_handler subscribes it for you)."""
signame = self.signals[signum] signame = self.signals[signum]
self.bus.log("Caught signal %s." % signame) self.bus.log('Caught signal %s.' % signame)
self.bus.publish(signame) self.bus.publish(signame)
def handle_SIGHUP(self): def handle_SIGHUP(self):
"""Restart if daemonized, else exit.""" """Restart if daemonized, else exit."""
if self._is_daemonized(): if self._is_daemonized():
self.bus.log("SIGHUP caught while daemonized. Restarting.") self.bus.log('SIGHUP caught while daemonized. Restarting.')
self.bus.restart() self.bus.restart()
else: else:
# not daemonized (may be foreground or background) # not daemonized (may be foreground or background)
self.bus.log("SIGHUP caught but not daemonized. Exiting.") self.bus.log('SIGHUP caught but not daemonized. Exiting.')
self.bus.exit() self.bus.exit()
@ -223,7 +222,8 @@ class DropPrivileges(SimplePlugin):
"""Drop privileges. uid/gid arguments not available on Windows. """Drop privileges. uid/gid arguments not available on Windows.
Special thanks to `Gavin Baker <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_ Special thanks to `Gavin Baker
<http://antonym.org/2005/12/dropping-privileges-in-python.html>`_
""" """
def __init__(self, bus, umask=None, uid=None, gid=None): def __init__(self, bus, umask=None, uid=None, gid=None):
@ -233,57 +233,57 @@ class DropPrivileges(SimplePlugin):
self.gid = gid self.gid = gid
self.umask = umask self.umask = umask
def _get_uid(self): @property
def uid(self):
"""The uid under which to run. Availability: Unix."""
return self._uid return self._uid
def _set_uid(self, val): @uid.setter
def uid(self, val):
if val is not None: if val is not None:
if pwd is None: if pwd is None:
self.bus.log("pwd module not available; ignoring uid.", self.bus.log('pwd module not available; ignoring uid.',
level=30) level=30)
val = None val = None
elif isinstance(val, basestring): elif isinstance(val, text_or_bytes):
val = pwd.getpwnam(val)[2] val = pwd.getpwnam(val)[2]
self._uid = val self._uid = val
uid = property(_get_uid, _set_uid,
doc="The uid under which to run. Availability: Unix.")
def _get_gid(self): @property
def gid(self):
"""The gid under which to run. Availability: Unix."""
return self._gid return self._gid
def _set_gid(self, val): @gid.setter
def gid(self, val):
if val is not None: if val is not None:
if grp is None: if grp is None:
self.bus.log("grp module not available; ignoring gid.", self.bus.log('grp module not available; ignoring gid.',
level=30) level=30)
val = None val = None
elif isinstance(val, basestring): elif isinstance(val, text_or_bytes):
val = grp.getgrnam(val)[2] val = grp.getgrnam(val)[2]
self._gid = val self._gid = val
gid = property(_get_gid, _set_gid,
doc="The gid under which to run. Availability: Unix.")
def _get_umask(self): @property
def umask(self):
"""The default permission mode for newly created files and directories.
Usually expressed in octal format, for example, ``0644``.
Availability: Unix, Windows.
"""
return self._umask return self._umask
def _set_umask(self, val): @umask.setter
def umask(self, val):
if val is not None: if val is not None:
try: try:
os.umask os.umask
except AttributeError: except AttributeError:
self.bus.log("umask function not available; ignoring umask.", self.bus.log('umask function not available; ignoring umask.',
level=30) level=30)
val = None val = None
self._umask = val self._umask = val
umask = property(
_get_umask,
_set_umask,
doc="""The default permission mode for newly created files and
directories.
Usually expressed in octal format, for example, ``0644``.
Availability: Unix, Windows.
""")
def start(self): def start(self):
# uid/gid # uid/gid
@ -347,7 +347,7 @@ class Daemonizer(SimplePlugin):
process still return proper exit codes. Therefore, if you use this process still return proper exit codes. Therefore, if you use this
plugin to daemonize, don't use the return code as an accurate indicator plugin to daemonize, don't use the return code as an accurate indicator
of whether the process fully started. In fact, that return code only of whether the process fully started. In fact, that return code only
indicates if the process succesfully finished the first fork. indicates if the process successfully finished the first fork.
""" """
def __init__(self, bus, stdin='/dev/null', stdout='/dev/null', def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
@ -372,6 +372,15 @@ class Daemonizer(SimplePlugin):
'Daemonizing now may cause strange failures.' % 'Daemonizing now may cause strange failures.' %
threading.enumerate(), level=30) threading.enumerate(), level=30)
self.daemonize(self.stdin, self.stdout, self.stderr, self.bus.log)
self.finalized = True
start.priority = 65
@staticmethod
def daemonize(
stdin='/dev/null', stdout='/dev/null', stderr='/dev/null',
logger=lambda msg: None):
# See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
# (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7) # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
# and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
@ -380,41 +389,29 @@ class Daemonizer(SimplePlugin):
sys.stdout.flush() sys.stdout.flush()
sys.stderr.flush() sys.stderr.flush()
# Do first fork. error_tmpl = (
try: '{sys.argv[0]}: fork #{n} failed: ({exc.errno}) {exc.strerror}\n'
pid = os.fork() )
if pid == 0:
# This is the child process. Continue.
pass
else:
# This is the first parent. Exit, now that we've forked.
self.bus.log('Forking once.')
os._exit(0)
except OSError:
# Python raises OSError rather than returning negative numbers.
exc = sys.exc_info()[1]
sys.exit("%s: fork #1 failed: (%d) %s\n"
% (sys.argv[0], exc.errno, exc.strerror))
os.setsid() for fork in range(2):
msg = ['Forking once.', 'Forking twice.'][fork]
try:
pid = os.fork()
if pid > 0:
# This is the parent; exit.
logger(msg)
os._exit(0)
except OSError as exc:
# Python raises OSError rather than returning negative numbers.
sys.exit(error_tmpl.format(sys=sys, exc=exc, n=fork + 1))
if fork == 0:
os.setsid()
# Do second fork
try:
pid = os.fork()
if pid > 0:
self.bus.log('Forking twice.')
os._exit(0) # Exit second parent
except OSError:
exc = sys.exc_info()[1]
sys.exit("%s: fork #2 failed: (%d) %s\n"
% (sys.argv[0], exc.errno, exc.strerror))
os.chdir("/")
os.umask(0) os.umask(0)
si = open(self.stdin, "r") si = open(stdin, 'r')
so = open(self.stdout, "a+") so = open(stdout, 'a+')
se = open(self.stderr, "a+") se = open(stderr, 'a+')
# os.dup2(fd, fd2) will close fd2 if necessary, # os.dup2(fd, fd2) will close fd2 if necessary,
# so we don't explicitly close stdin/out/err. # so we don't explicitly close stdin/out/err.
@ -423,9 +420,7 @@ class Daemonizer(SimplePlugin):
os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno()) os.dup2(se.fileno(), sys.stderr.fileno())
self.bus.log('Daemonized to PID: %s' % os.getpid()) logger('Daemonized to PID: %s' % os.getpid())
self.finalized = True
start.priority = 65
class PIDFile(SimplePlugin): class PIDFile(SimplePlugin):
@ -442,7 +437,7 @@ class PIDFile(SimplePlugin):
if self.finalized: if self.finalized:
self.bus.log('PID %r already written to %r.' % (pid, self.pidfile)) self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
else: else:
open(self.pidfile, "wb").write(ntob("%s\n" % pid, 'utf8')) open(self.pidfile, 'wb').write(ntob('%s\n' % pid, 'utf8'))
self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
self.finalized = True self.finalized = True
start.priority = 70 start.priority = 70
@ -453,7 +448,7 @@ class PIDFile(SimplePlugin):
self.bus.log('PID file removed: %r.' % self.pidfile) self.bus.log('PID file removed: %r.' % self.pidfile)
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
raise raise
except: except Exception:
pass pass
@ -481,13 +476,13 @@ class PerpetualTimer(Timer):
except Exception: except Exception:
if self.bus: if self.bus:
self.bus.log( self.bus.log(
"Error in perpetual timer thread function %r." % 'Error in perpetual timer thread function %r.' %
self.function, level=40, traceback=True) self.function, level=40, traceback=True)
# Quit on first error to avoid massive logs. # Quit on first error to avoid massive logs.
raise raise
class BackgroundTask(SetDaemonProperty, threading.Thread): class BackgroundTask(threading.Thread):
"""A subclass of threading.Thread whose run() method repeats. """A subclass of threading.Thread whose run() method repeats.
@ -499,7 +494,7 @@ class BackgroundTask(SetDaemonProperty, threading.Thread):
""" """
def __init__(self, interval, function, args=[], kwargs={}, bus=None): def __init__(self, interval, function, args=[], kwargs={}, bus=None):
threading.Thread.__init__(self) super(BackgroundTask, self).__init__()
self.interval = interval self.interval = interval
self.function = function self.function = function
self.args = args self.args = args
@ -523,7 +518,7 @@ class BackgroundTask(SetDaemonProperty, threading.Thread):
self.function(*self.args, **self.kwargs) self.function(*self.args, **self.kwargs)
except Exception: except Exception:
if self.bus: if self.bus:
self.bus.log("Error in background task thread function %r." self.bus.log('Error in background task thread function %r.'
% self.function, level=40, traceback=True) % self.function, level=40, traceback=True)
# Quit on first error to avoid massive logs. # Quit on first error to avoid massive logs.
raise raise
@ -560,24 +555,24 @@ class Monitor(SimplePlugin):
bus=self.bus) bus=self.bus)
self.thread.setName(threadname) self.thread.setName(threadname)
self.thread.start() self.thread.start()
self.bus.log("Started monitor thread %r." % threadname) self.bus.log('Started monitor thread %r.' % threadname)
else: else:
self.bus.log("Monitor thread %r already started." % threadname) self.bus.log('Monitor thread %r already started.' % threadname)
start.priority = 70 start.priority = 70
def stop(self): def stop(self):
"""Stop our callback's background task thread.""" """Stop our callback's background task thread."""
if self.thread is None: if self.thread is None:
self.bus.log("No thread running for %s." % self.bus.log('No thread running for %s.' %
self.name or self.__class__.__name__) self.name or self.__class__.__name__)
else: else:
if self.thread is not threading.currentThread(): if self.thread is not threading.currentThread():
name = self.thread.getName() name = self.thread.getName()
self.thread.cancel() self.thread.cancel()
if not get_daemon(self.thread): if not self.thread.daemon:
self.bus.log("Joining %r" % name) self.bus.log('Joining %r' % name)
self.thread.join() self.thread.join()
self.bus.log("Stopped thread %r." % name) self.bus.log('Stopped thread %r.' % name)
self.thread = None self.thread = None
def graceful(self): def graceful(self):
@ -632,23 +627,40 @@ class Autoreloader(Monitor):
def sysfiles(self): def sysfiles(self):
"""Return a Set of sys.modules filenames to monitor.""" """Return a Set of sys.modules filenames to monitor."""
files = set() search_mod_names = filter(re.compile(self.match).match, sys.modules)
for k, m in list(sys.modules.items()): mods = map(sys.modules.get, search_mod_names)
if re.match(self.match, k): return set(filter(None, map(self._file_for_module, mods)))
if (
hasattr(m, '__loader__') and @classmethod
hasattr(m.__loader__, 'archive') def _file_for_module(cls, module):
): """Return the relevant file for the module."""
f = m.__loader__.archive return (
else: cls._archive_for_zip_module(module)
f = getattr(m, '__file__', None) or cls._file_for_file_module(module)
if f is not None and not os.path.isabs(f): )
# ensure absolute paths so a os.chdir() in the app
# doesn't break me @staticmethod
f = os.path.normpath( def _archive_for_zip_module(module):
os.path.join(_module__file__base, f)) """Return the archive filename for the module if relevant."""
files.add(f) try:
return files return module.__loader__.archive
except AttributeError:
pass
@classmethod
def _file_for_file_module(cls, module):
"""Return the file for the module."""
try:
return module.__file__ and cls._make_absolute(module.__file__)
except AttributeError:
pass
@staticmethod
def _make_absolute(filename):
"""Ensure filename is absolute to avoid effect of os.chdir."""
return filename if os.path.isabs(filename) else (
os.path.normpath(os.path.join(_module__file__base, filename))
)
def run(self): def run(self):
"""Reload the process if registered files have been modified.""" """Reload the process if registered files have been modified."""
@ -674,10 +686,10 @@ class Autoreloader(Monitor):
else: else:
if mtime is None or mtime > oldtime: if mtime is None or mtime > oldtime:
# The file has been deleted or modified. # The file has been deleted or modified.
self.bus.log("Restarting because %s changed." % self.bus.log('Restarting because %s changed.' %
filename) filename)
self.thread.cancel() self.thread.cancel()
self.bus.log("Stopped thread %r." % self.bus.log('Stopped thread %r.' %
self.thread.getName()) self.thread.getName())
self.bus.restart() self.bus.restart()
return return
@ -717,7 +729,7 @@ class ThreadManager(SimplePlugin):
If the current thread has already been seen, any 'start_thread' If the current thread has already been seen, any 'start_thread'
listeners will not be run again. listeners will not be run again.
""" """
thread_ident = get_thread_ident() thread_ident = _thread.get_ident()
if thread_ident not in self.threads: if thread_ident not in self.threads:
# We can't just use get_ident as the thread ID # We can't just use get_ident as the thread ID
# because some platforms reuse thread ID's. # because some platforms reuse thread ID's.
@ -727,7 +739,7 @@ class ThreadManager(SimplePlugin):
def release_thread(self): def release_thread(self):
"""Release the current thread and run 'stop_thread' listeners.""" """Release the current thread and run 'stop_thread' listeners."""
thread_ident = get_thread_ident() thread_ident = _thread.get_ident()
i = self.threads.pop(thread_ident, None) i = self.threads.pop(thread_ident, None)
if i is not None: if i is not None:
self.bus.publish('stop_thread', i) self.bus.publish('stop_thread', i)

View file

@ -1,4 +1,4 @@
""" r"""
Starting in CherryPy 3.1, cherrypy.server is implemented as an Starting in CherryPy 3.1, cherrypy.server is implemented as an
:ref:`Engine Plugin<plugins>`. It's an instance of :ref:`Engine Plugin<plugins>`. It's an instance of
:class:`cherrypy._cpserver.Server`, which is a subclass of :class:`cherrypy._cpserver.Server`, which is a subclass of
@ -12,10 +12,14 @@ If you need to start more than one HTTP server (to serve on multiple ports, or
protocols, etc.), you can manually register each one and then start them all protocols, etc.), you can manually register each one and then start them all
with engine.start:: with engine.start::
s1 = ServerAdapter(cherrypy.engine, MyWSGIServer(host='0.0.0.0', port=80)) s1 = ServerAdapter(
s2 = ServerAdapter(cherrypy.engine, cherrypy.engine,
another.HTTPServer(host='127.0.0.1', MyWSGIServer(host='0.0.0.0', port=80)
SSL=True)) )
s2 = ServerAdapter(
cherrypy.engine,
another.HTTPServer(host='127.0.0.1', SSL=True)
)
s1.subscribe() s1.subscribe()
s2.subscribe() s2.subscribe()
cherrypy.engine.start() cherrypy.engine.start()
@ -58,10 +62,10 @@ hello.py::
import cherrypy import cherrypy
class HelloWorld: class HelloWorld:
\"""Sample request handler class.\""" '''Sample request handler class.'''
@cherrypy.expose
def index(self): def index(self):
return "Hello world!" return "Hello world!"
index.exposed = True
cherrypy.tree.mount(HelloWorld()) cherrypy.tree.mount(HelloWorld())
# CherryPy autoreload must be disabled for the flup server to work # CherryPy autoreload must be disabled for the flup server to work
@ -113,9 +117,18 @@ Please see `Lighttpd FastCGI Docs
an explanation of the possible configuration options. an explanation of the possible configuration options.
""" """
import os
import sys import sys
import time import time
import warnings import warnings
import contextlib
import portend
class Timeouts:
occupied = 5
free = 1
class ServerAdapter(object): class ServerAdapter(object):
@ -150,49 +163,56 @@ class ServerAdapter(object):
def start(self): def start(self):
"""Start the HTTP server.""" """Start the HTTP server."""
if self.bind_addr is None:
on_what = "unknown interface (dynamic?)"
elif isinstance(self.bind_addr, tuple):
on_what = self._get_base()
else:
on_what = "socket file: %s" % self.bind_addr
if self.running: if self.running:
self.bus.log("Already serving on %s" % on_what) self.bus.log('Already serving on %s' % self.description)
return return
self.interrupt = None self.interrupt = None
if not self.httpserver: if not self.httpserver:
raise ValueError("No HTTP server has been created.") raise ValueError('No HTTP server has been created.')
# Start the httpserver in a new thread. if not os.environ.get('LISTEN_PID', None):
if isinstance(self.bind_addr, tuple): # Start the httpserver in a new thread.
wait_for_free_port(*self.bind_addr) if isinstance(self.bind_addr, tuple):
portend.free(*self.bind_addr, timeout=Timeouts.free)
import threading import threading
t = threading.Thread(target=self._start_http_thread) t = threading.Thread(target=self._start_http_thread)
t.setName("HTTPServer " + t.getName()) t.setName('HTTPServer ' + t.getName())
t.start() t.start()
self.wait() self.wait()
self.running = True self.running = True
self.bus.log("Serving on %s" % on_what) self.bus.log('Serving on %s' % self.description)
start.priority = 75 start.priority = 75
@property
def description(self):
"""
A description about where this server is bound.
"""
if self.bind_addr is None:
on_what = 'unknown interface (dynamic?)'
elif isinstance(self.bind_addr, tuple):
on_what = self._get_base()
else:
on_what = 'socket file: %s' % self.bind_addr
return on_what
def _get_base(self): def _get_base(self):
if not self.httpserver: if not self.httpserver:
return '' return ''
host, port = self.bind_addr host, port = self.bound_addr
if getattr(self.httpserver, 'ssl_adapter', None): if getattr(self.httpserver, 'ssl_adapter', None):
scheme = "https" scheme = 'https'
if port != 443: if port != 443:
host += ":%s" % port host += ':%s' % port
else: else:
scheme = "http" scheme = 'http'
if port != 80: if port != 80:
host += ":%s" % port host += ':%s' % port
return "%s://%s" % (scheme, host) return '%s://%s' % (scheme, host)
def _start_http_thread(self): def _start_http_thread(self):
"""HTTP servers MUST be running in new threads, so that the """HTTP servers MUST be running in new threads, so that the
@ -204,32 +224,52 @@ class ServerAdapter(object):
try: try:
self.httpserver.start() self.httpserver.start()
except KeyboardInterrupt: except KeyboardInterrupt:
self.bus.log("<Ctrl-C> hit: shutting down HTTP server") self.bus.log('<Ctrl-C> hit: shutting down HTTP server')
self.interrupt = sys.exc_info()[1] self.interrupt = sys.exc_info()[1]
self.bus.exit() self.bus.exit()
except SystemExit: except SystemExit:
self.bus.log("SystemExit raised: shutting down HTTP server") self.bus.log('SystemExit raised: shutting down HTTP server')
self.interrupt = sys.exc_info()[1] self.interrupt = sys.exc_info()[1]
self.bus.exit() self.bus.exit()
raise raise
except: except Exception:
self.interrupt = sys.exc_info()[1] self.interrupt = sys.exc_info()[1]
self.bus.log("Error in HTTP server: shutting down", self.bus.log('Error in HTTP server: shutting down',
traceback=True, level=40) traceback=True, level=40)
self.bus.exit() self.bus.exit()
raise raise
def wait(self): def wait(self):
"""Wait until the HTTP server is ready to receive requests.""" """Wait until the HTTP server is ready to receive requests."""
while not getattr(self.httpserver, "ready", False): while not getattr(self.httpserver, 'ready', False):
if self.interrupt: if self.interrupt:
raise self.interrupt raise self.interrupt
time.sleep(.1) time.sleep(.1)
# Wait for port to be occupied # bypass check when LISTEN_PID is set
if isinstance(self.bind_addr, tuple): if os.environ.get('LISTEN_PID', None):
host, port = self.bind_addr return
wait_for_occupied_port(host, port)
# bypass check when running via socket-activation
# (for socket-activation the port will be managed by systemd)
if not isinstance(self.bind_addr, tuple):
return
# wait for port to be occupied
with _safe_wait(*self.bound_addr):
portend.occupied(*self.bound_addr, timeout=Timeouts.occupied)
@property
def bound_addr(self):
"""
The bind address, or if it's an ephemeral port and the
socket has been bound, return the actual port bound.
"""
host, port = self.bind_addr
if port == 0 and self.httpserver.socket:
# Bound to ephemeral port. Get the actual port allocated.
port = self.httpserver.socket.getsockname()[1]
return host, port
def stop(self): def stop(self):
"""Stop the HTTP server.""" """Stop the HTTP server."""
@ -238,11 +278,11 @@ class ServerAdapter(object):
self.httpserver.stop() self.httpserver.stop()
# Wait for the socket to be truly freed. # Wait for the socket to be truly freed.
if isinstance(self.bind_addr, tuple): if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr) portend.free(*self.bound_addr, timeout=Timeouts.free)
self.running = False self.running = False
self.bus.log("HTTP Server %s shut down" % self.httpserver) self.bus.log('HTTP Server %s shut down' % self.httpserver)
else: else:
self.bus.log("HTTP Server %s already shut down" % self.httpserver) self.bus.log('HTTP Server %s already shut down' % self.httpserver)
stop.priority = 25 stop.priority = 25
def restart(self): def restart(self):
@ -359,107 +399,18 @@ class FlupSCGIServer(object):
self.scgiserver._threadPool.maxSpare = 0 self.scgiserver._threadPool.maxSpare = 0
def client_host(server_host): @contextlib.contextmanager
"""Return the host on which a client can connect to the given listener.""" def _safe_wait(host, port):
if server_host == '0.0.0.0': """
# 0.0.0.0 is INADDR_ANY, which should answer on localhost. On systems where a loopback interface is not available and the
return '127.0.0.1' server is bound to all interfaces, it's difficult to determine
if server_host in ('::', '::0', '::0.0.0.0'): whether the server is in fact occupying the port. In this case,
# :: is IN6ADDR_ANY, which should answer on localhost. just issue a warning and move on. See issue #1100.
# ::0 and ::0.0.0.0 are non-canonical but common """
# ways to write IN6ADDR_ANY.
return '::1'
return server_host
def check_port(host, port, timeout=1.0):
"""Raise an error if the given port is not free on the given host."""
if not host:
raise ValueError("Host values of '' or None are not allowed.")
host = client_host(host)
port = int(port)
import socket
# AF_INET or AF_INET6 socket
# Get the correct address family for our host (allows IPv6 addresses)
try: try:
info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, yield
socket.SOCK_STREAM) except portend.Timeout:
except socket.gaierror: if host == portend.client_host(host):
if ':' in host: raise
info = [( msg = 'Unable to verify that the server is bound on %r' % port
socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0) warnings.warn(msg)
)]
else:
info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))]
for res in info:
af, socktype, proto, canonname, sa = res
s = None
try:
s = socket.socket(af, socktype, proto)
# See http://groups.google.com/group/cherrypy-users/
# browse_frm/thread/bbfe5eb39c904fe0
s.settimeout(timeout)
s.connect((host, port))
s.close()
except socket.error:
if s:
s.close()
else:
raise IOError("Port %s is in use on %s; perhaps the previous "
"httpserver did not shut down properly." %
(repr(port), repr(host)))
# Feel free to increase these defaults on slow systems:
free_port_timeout = 0.1
occupied_port_timeout = 1.0
def wait_for_free_port(host, port, timeout=None):
"""Wait for the specified port to become free (drop requests)."""
if not host:
raise ValueError("Host values of '' or None are not allowed.")
if timeout is None:
timeout = free_port_timeout
for trial in range(50):
try:
# we are expecting a free port, so reduce the timeout
check_port(host, port, timeout=timeout)
except IOError:
# Give the old server thread time to free the port.
time.sleep(timeout)
else:
return
raise IOError("Port %r not free on %r" % (port, host))
def wait_for_occupied_port(host, port, timeout=None):
"""Wait for the specified port to become active (receive requests)."""
if not host:
raise ValueError("Host values of '' or None are not allowed.")
if timeout is None:
timeout = occupied_port_timeout
for trial in range(50):
try:
check_port(host, port, timeout=timeout)
except IOError:
# port is occupied
return
else:
time.sleep(timeout)
if host == client_host(host):
raise IOError("Port %r not bound on %r" % (port, host))
# On systems where a loopback interface is not available and the
# server is bound to all interfaces, it's difficult to determine
# whether the server is in fact occupying the port. In this case,
# just issue a warning and move on. See issue #1100.
msg = "Unable to verify that the server is bound on %r" % port
warnings.warn(msg)

View file

@ -85,19 +85,20 @@ class Win32Bus(wspbus.Bus):
return self.events[state] return self.events[state]
except KeyError: except KeyError:
event = win32event.CreateEvent(None, 0, 0, event = win32event.CreateEvent(None, 0, 0,
"WSPBus %s Event (pid=%r)" % 'WSPBus %s Event (pid=%r)' %
(state.name, os.getpid())) (state.name, os.getpid()))
self.events[state] = event self.events[state] = event
return event return event
def _get_state(self): @property
def state(self):
return self._state return self._state
def _set_state(self, value): @state.setter
def state(self, value):
self._state = value self._state = value
event = self._get_state_event(value) event = self._get_state_event(value)
win32event.PulseEvent(event) win32event.PulseEvent(event)
state = property(_get_state, _set_state)
def wait(self, state, interval=0.1, channel=None): def wait(self, state, interval=0.1, channel=None):
"""Wait for the given state(s), KeyboardInterrupt or SystemExit. """Wait for the given state(s), KeyboardInterrupt or SystemExit.
@ -135,7 +136,8 @@ class _ControlCodes(dict):
for key, val in self.items(): for key, val in self.items():
if val is obj: if val is obj:
return key return key
raise ValueError("The given object could not be found: %r" % obj) raise ValueError('The given object could not be found: %r' % obj)
control_codes = _ControlCodes({'graceful': 138}) control_codes = _ControlCodes({'graceful': 138})
@ -153,14 +155,14 @@ class PyWebService(win32serviceutil.ServiceFramework):
"""Python Web Service.""" """Python Web Service."""
_svc_name_ = "Python Web Service" _svc_name_ = 'Python Web Service'
_svc_display_name_ = "Python Web Service" _svc_display_name_ = 'Python Web Service'
_svc_deps_ = None # sequence of service names on which this depends _svc_deps_ = None # sequence of service names on which this depends
_exe_name_ = "pywebsvc" _exe_name_ = 'pywebsvc'
_exe_args_ = None # Default to no arguments _exe_args_ = None # Default to no arguments
# Only exists on Windows 2000 or later, ignored on windows NT # Only exists on Windows 2000 or later, ignored on windows NT
_svc_description_ = "Python Web Service" _svc_description_ = 'Python Web Service'
def SvcDoRun(self): def SvcDoRun(self):
from cherrypy import process from cherrypy import process
@ -173,6 +175,7 @@ class PyWebService(win32serviceutil.ServiceFramework):
process.bus.exit() process.bus.exit()
def SvcOther(self, control): def SvcOther(self, control):
from cherrypy import process
process.bus.publish(control_codes.key_for(control)) process.bus.publish(control_codes.key_for(control))

View file

@ -1,4 +1,4 @@
"""An implementation of the Web Site Process Bus. r"""An implementation of the Web Site Process Bus.
This module is completely standalone, depending only on the stdlib. This module is completely standalone, depending only on the stdlib.
@ -61,12 +61,28 @@ the new state.::
""" """
import atexit import atexit
try:
import ctypes
except ImportError:
"""Google AppEngine is shipped without ctypes
:seealso: http://stackoverflow.com/a/6523777/70170
"""
ctypes = None
import operator
import os import os
import sys import sys
import threading import threading
import time import time
import traceback as _traceback import traceback as _traceback
import warnings import warnings
import subprocess
import functools
import six
# Here I save the value of os.getcwd(), which, if I am imported early enough, # Here I save the value of os.getcwd(), which, if I am imported early enough,
# will be the directory from which the startup script was run. This is needed # will be the directory from which the startup script was run. This is needed
@ -78,15 +94,13 @@ _startup_cwd = os.getcwd()
class ChannelFailures(Exception): class ChannelFailures(Exception):
"""Exception raised during errors on Bus.publish()."""
"""Exception raised when errors occur in a listener during Bus.publish().
"""
delimiter = '\n' delimiter = '\n'
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
# Don't use 'super' here; Exceptions are old-style in Py2.4 """Initialize ChannelFailures errors wrapper."""
# See https://bitbucket.org/cherrypy/cherrypy/issue/959 super(ChannelFailures, self).__init__(*args, **kwargs)
Exception.__init__(self, *args, **kwargs)
self._exceptions = list() self._exceptions = list()
def handle_exception(self): def handle_exception(self):
@ -98,12 +112,14 @@ class ChannelFailures(Exception):
return self._exceptions[:] return self._exceptions[:]
def __str__(self): def __str__(self):
"""Render the list of errors, which happened in channel."""
exception_strings = map(repr, self.get_instances()) exception_strings = map(repr, self.get_instances())
return self.delimiter.join(exception_strings) return self.delimiter.join(exception_strings)
__repr__ = __str__ __repr__ = __str__
def __bool__(self): def __bool__(self):
"""Determine whether any error happened in channel."""
return bool(self._exceptions) return bool(self._exceptions)
__nonzero__ = __bool__ __nonzero__ = __bool__
@ -116,12 +132,14 @@ class _StateEnum(object):
name = None name = None
def __repr__(self): def __repr__(self):
return "states.%s" % self.name return 'states.%s' % self.name
def __setattr__(self, key, value): def __setattr__(self, key, value):
if isinstance(value, self.State): if isinstance(value, self.State):
value.name = key value.name = key
object.__setattr__(self, key, value) object.__setattr__(self, key, value)
states = _StateEnum() states = _StateEnum()
states.STOPPED = states.State() states.STOPPED = states.State()
states.STARTING = states.State() states.STARTING = states.State()
@ -142,7 +160,6 @@ else:
class Bus(object): class Bus(object):
"""Process state-machine and messenger for HTTP site deployment. """Process state-machine and messenger for HTTP site deployment.
All listeners for a given channel are guaranteed to be called even All listeners for a given channel are guaranteed to be called even
@ -158,18 +175,31 @@ class Bus(object):
max_cloexec_files = max_files max_cloexec_files = max_files
def __init__(self): def __init__(self):
"""Initialize pub/sub bus."""
self.execv = False self.execv = False
self.state = states.STOPPED self.state = states.STOPPED
channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main'
self.listeners = dict( self.listeners = dict(
[(channel, set()) for channel (channel, set())
in ('start', 'stop', 'exit', 'graceful', 'log', 'main')]) for channel in channels
)
self._priorities = {} self._priorities = {}
def subscribe(self, channel, callback, priority=None): def subscribe(self, channel, callback=None, priority=None):
"""Add the given callback at the given channel (if not present).""" """Add the given callback at the given channel (if not present).
if channel not in self.listeners:
self.listeners[channel] = set() If callback is None, return a partial suitable for decorating
self.listeners[channel].add(callback) the callback.
"""
if callback is None:
return functools.partial(
self.subscribe,
channel,
priority=priority,
)
ch_listeners = self.listeners.setdefault(channel, set())
ch_listeners.add(callback)
if priority is None: if priority is None:
priority = getattr(callback, 'priority', 50) priority = getattr(callback, 'priority', 50)
@ -190,14 +220,11 @@ class Bus(object):
exc = ChannelFailures() exc = ChannelFailures()
output = [] output = []
items = [(self._priorities[(channel, listener)], listener) raw_items = (
for listener in self.listeners[channel]] (self._priorities[(channel, listener)], listener)
try: for listener in self.listeners[channel]
items.sort(key=lambda item: item[0]) )
except TypeError: items = sorted(raw_items, key=operator.itemgetter(0))
# Python 2.3 had no 'key' arg, but that doesn't matter
# since it could sort dissimilar types just fine.
items.sort()
for priority, listener in items: for priority, listener in items:
try: try:
output.append(listener(*args, **kwargs)) output.append(listener(*args, **kwargs))
@ -209,26 +236,26 @@ class Bus(object):
if exc and e.code == 0: if exc and e.code == 0:
e.code = 1 e.code = 1
raise raise
except: except Exception:
exc.handle_exception() exc.handle_exception()
if channel == 'log': if channel == 'log':
# Assume any further messages to 'log' will fail. # Assume any further messages to 'log' will fail.
pass pass
else: else:
self.log("Error in %r listener %r" % (channel, listener), self.log('Error in %r listener %r' % (channel, listener),
level=40, traceback=True) level=40, traceback=True)
if exc: if exc:
raise exc raise exc
return output return output
def _clean_exit(self): def _clean_exit(self):
"""An atexit handler which asserts the Bus is not running.""" """Assert that the Bus is not running in atexit handler callback."""
if self.state != states.EXITING: if self.state != states.EXITING:
warnings.warn( warnings.warn(
"The main thread is exiting, but the Bus is in the %r state; " 'The main thread is exiting, but the Bus is in the %r state; '
"shutting it down automatically now. You must either call " 'shutting it down automatically now. You must either call '
"bus.block() after start(), or call bus.exit() before the " 'bus.block() after start(), or call bus.exit() before the '
"main thread exits." % self.state, RuntimeWarning) 'main thread exits.' % self.state, RuntimeWarning)
self.exit() self.exit()
def start(self): def start(self):
@ -243,13 +270,13 @@ class Bus(object):
self.log('Bus STARTED') self.log('Bus STARTED')
except (KeyboardInterrupt, SystemExit): except (KeyboardInterrupt, SystemExit):
raise raise
except: except Exception:
self.log("Shutting down due to error in start listener:", self.log('Shutting down due to error in start listener:',
level=40, traceback=True) level=40, traceback=True)
e_info = sys.exc_info()[1] e_info = sys.exc_info()[1]
try: try:
self.exit() self.exit()
except: except Exception:
# Any stop/exit errors will be logged inside publish(). # Any stop/exit errors will be logged inside publish().
pass pass
# Re-raise the original error # Re-raise the original error
@ -258,6 +285,7 @@ class Bus(object):
def exit(self): def exit(self):
"""Stop all services and prepare to exit the process.""" """Stop all services and prepare to exit the process."""
exitstate = self.state exitstate = self.state
EX_SOFTWARE = 70
try: try:
self.stop() self.stop()
@ -267,19 +295,19 @@ class Bus(object):
# This isn't strictly necessary, but it's better than seeing # This isn't strictly necessary, but it's better than seeing
# "Waiting for child threads to terminate..." and then nothing. # "Waiting for child threads to terminate..." and then nothing.
self.log('Bus EXITED') self.log('Bus EXITED')
except: except Exception:
# This method is often called asynchronously (whether thread, # This method is often called asynchronously (whether thread,
# signal handler, console handler, or atexit handler), so we # signal handler, console handler, or atexit handler), so we
# can't just let exceptions propagate out unhandled. # can't just let exceptions propagate out unhandled.
# Assume it's been logged and just die. # Assume it's been logged and just die.
os._exit(70) # EX_SOFTWARE os._exit(EX_SOFTWARE)
if exitstate == states.STARTING: if exitstate == states.STARTING:
# exit() was called before start() finished, possibly due to # exit() was called before start() finished, possibly due to
# Ctrl-C because a start listener got stuck. In this case, # Ctrl-C because a start listener got stuck. In this case,
# we could get stuck in a loop where Ctrl-C never exits the # we could get stuck in a loop where Ctrl-C never exits the
# process, so we just call os.exit here. # process, so we just call os.exit here.
os._exit(70) # EX_SOFTWARE os._exit(EX_SOFTWARE)
def restart(self): def restart(self):
"""Restart the process (may close connections). """Restart the process (may close connections).
@ -317,11 +345,11 @@ class Bus(object):
raise raise
# Waiting for ALL child threads to finish is necessary on OS X. # Waiting for ALL child threads to finish is necessary on OS X.
# See https://bitbucket.org/cherrypy/cherrypy/issue/581. # See https://github.com/cherrypy/cherrypy/issues/581.
# It's also good to let them all shut down before allowing # It's also good to let them all shut down before allowing
# the main thread to call atexit handlers. # the main thread to call atexit handlers.
# See https://bitbucket.org/cherrypy/cherrypy/issue/751. # See https://github.com/cherrypy/cherrypy/issues/751.
self.log("Waiting for child threads to terminate...") self.log('Waiting for child threads to terminate...')
for t in threading.enumerate(): for t in threading.enumerate():
# Validate the we're not trying to join the MainThread # Validate the we're not trying to join the MainThread
# that will cause a deadlock and the case exist when # that will cause a deadlock and the case exist when
@ -329,18 +357,13 @@ class Bus(object):
# that another thread executes cherrypy.engine.exit() # that another thread executes cherrypy.engine.exit()
if ( if (
t != threading.currentThread() and t != threading.currentThread() and
t.isAlive() and not isinstance(t, threading._MainThread) and
not isinstance(t, threading._MainThread) # Note that any dummy (external) threads are
# always daemonic.
not t.daemon
): ):
# Note that any dummy (external) threads are always daemonic. self.log('Waiting for thread %s.' % t.getName())
if hasattr(threading.Thread, "daemon"): t.join()
# Python 2.6+
d = t.daemon
else:
d = t.isDaemon()
if not d:
self.log("Waiting for thread %s." % t.getName())
t.join()
if self.execv: if self.execv:
self._do_execv() self._do_execv()
@ -352,23 +375,9 @@ class Bus(object):
else: else:
states = [state] states = [state]
def _wait(): while self.state not in states:
while self.state not in states: time.sleep(interval)
time.sleep(interval) self.publish(channel)
self.publish(channel)
# From http://psyco.sourceforge.net/psycoguide/bugs.html:
# "The compiled machine code does not include the regular polling
# done by Python, meaning that a KeyboardInterrupt will not be
# detected before execution comes back to the regular Python
# interpreter. Your program cannot be interrupted if caught
# into an infinite Psyco-compiled loop."
try:
sys.modules['psyco'].cannotcompile(_wait)
except (KeyError, AttributeError):
pass
_wait()
def _do_execv(self): def _do_execv(self):
"""Re-execute the current process. """Re-execute the current process.
@ -376,14 +385,20 @@ class Bus(object):
This must be called from the main thread, because certain platforms This must be called from the main thread, because certain platforms
(OS X) don't allow execv to be called in a child thread very well. (OS X) don't allow execv to be called in a child thread very well.
""" """
args = sys.argv[:] try:
args = self._get_true_argv()
except NotImplementedError:
"""It's probably win32 or GAE"""
args = [sys.executable] + self._get_interpreter_argv() + sys.argv
self.log('Re-spawning %s' % ' '.join(args)) self.log('Re-spawning %s' % ' '.join(args))
self._extend_pythonpath(os.environ)
if sys.platform[:4] == 'java': if sys.platform[:4] == 'java':
from _systemrestart import SystemRestart from _systemrestart import SystemRestart
raise SystemRestart raise SystemRestart
else: else:
args.insert(0, sys.executable)
if sys.platform == 'win32': if sys.platform == 'win32':
args = ['"%s"' % arg for arg in args] args = ['"%s"' % arg for arg in args]
@ -392,6 +407,134 @@ class Bus(object):
self._set_cloexec() self._set_cloexec()
os.execv(sys.executable, args) os.execv(sys.executable, args)
@staticmethod
def _get_interpreter_argv():
"""Retrieve current Python interpreter's arguments.
Returns empty tuple in case of frozen mode, uses built-in arguments
reproduction function otherwise.
Frozen mode is possible for the app has been packaged into a binary
executable using py2exe. In this case the interpreter's arguments are
already built-in into that executable.
:seealso: https://github.com/cherrypy/cherrypy/issues/1526
Ref: https://pythonhosted.org/PyInstaller/runtime-information.html
"""
return ([]
if getattr(sys, 'frozen', False)
else subprocess._args_from_interpreter_flags())
@staticmethod
def _get_true_argv():
"""Retrieve all real arguments of the python interpreter.
...even those not listed in ``sys.argv``
:seealso: http://stackoverflow.com/a/28338254/595220
:seealso: http://stackoverflow.com/a/6683222/595220
:seealso: http://stackoverflow.com/a/28414807/595220
"""
try:
char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p
argv = ctypes.POINTER(char_p)()
argc = ctypes.c_int()
ctypes.pythonapi.Py_GetArgcArgv(
ctypes.byref(argc),
ctypes.byref(argv),
)
_argv = argv[:argc.value]
# The code below is trying to correctly handle special cases.
# `-c`'s argument interpreted by Python itself becomes `-c` as
# well. Same applies to `-m`. This snippet is trying to survive
# at least the case with `-m`
# Ref: https://github.com/cherrypy/cherrypy/issues/1545
# Ref: python/cpython@418baf9
argv_len, is_command, is_module = len(_argv), False, False
try:
m_ind = _argv.index('-m')
if m_ind < argv_len - 1 and _argv[m_ind + 1] in ('-c', '-m'):
"""
In some older Python versions `-m`'s argument may be
substituted with `-c`, not `-m`
"""
is_module = True
except (IndexError, ValueError):
m_ind = None
try:
c_ind = _argv.index('-c')
if c_ind < argv_len - 1 and _argv[c_ind + 1] == '-c':
is_command = True
except (IndexError, ValueError):
c_ind = None
if is_module:
"""It's containing `-m -m` sequence of arguments"""
if is_command and c_ind < m_ind:
"""There's `-c -c` before `-m`"""
raise RuntimeError(
"Cannot reconstruct command from '-c'. Ref: "
'https://github.com/cherrypy/cherrypy/issues/1545')
# Survive module argument here
original_module = sys.argv[0]
if not os.access(original_module, os.R_OK):
"""There's no such module exist"""
raise AttributeError(
"{} doesn't seem to be a module "
'accessible by current user'.format(original_module))
del _argv[m_ind:m_ind + 2] # remove `-m -m`
# ... and substitute it with the original module path:
_argv.insert(m_ind, original_module)
elif is_command:
"""It's containing just `-c -c` sequence of arguments"""
raise RuntimeError(
"Cannot reconstruct command from '-c'. "
'Ref: https://github.com/cherrypy/cherrypy/issues/1545')
except AttributeError:
"""It looks Py_GetArgcArgv is completely absent in some environments
It is known, that there's no Py_GetArgcArgv in MS Windows and
``ctypes`` module is completely absent in Google AppEngine
:seealso: https://github.com/cherrypy/cherrypy/issues/1506
:seealso: https://github.com/cherrypy/cherrypy/issues/1512
:ref: http://bit.ly/2gK6bXK
"""
raise NotImplementedError
else:
return _argv
@staticmethod
def _extend_pythonpath(env):
"""Prepend current working dir to PATH environment variable if needed.
If sys.path[0] is an empty string, the interpreter was likely
invoked with -m and the effective path is about to change on
re-exec. Add the current directory to $PYTHONPATH to ensure
that the new process sees the same path.
This issue cannot be addressed in the general case because
Python cannot reliably reconstruct the
original command line (http://bugs.python.org/issue14208).
(This idea filched from tornado.autoreload)
"""
path_prefix = '.' + os.pathsep
existing_path = env.get('PYTHONPATH', '')
needs_patch = (
sys.path[0] == '' and
not existing_path.startswith(path_prefix)
)
if needs_patch:
env['PYTHONPATH'] = path_prefix + existing_path
def _set_cloexec(self): def _set_cloexec(self):
"""Set the CLOEXEC flag on all open files (except stdin/out/err). """Set the CLOEXEC flag on all open files (except stdin/out/err).
@ -437,10 +580,11 @@ class Bus(object):
return t return t
def log(self, msg="", level=20, traceback=False): def log(self, msg='', level=20, traceback=False):
"""Log the given message. Append the last traceback if requested.""" """Log the given message. Append the last traceback if requested."""
if traceback: if traceback:
msg += "\n" + "".join(_traceback.format_exception(*sys.exc_info())) msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info()))
self.publish('log', msg, level) self.publish('log', msg, level)
bus = Bus() bus = Bus()

View file

@ -8,7 +8,7 @@ then tweak as desired.
Even before any tweaking, this should serve a few demonstration pages. Even before any tweaking, this should serve a few demonstration pages.
Change to this directory and run: Change to this directory and run:
../cherryd -c site.conf cherryd -c site.conf
""" """
@ -19,36 +19,38 @@ import os
local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__)) local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__))
@cherrypy.config(**{'tools.log_tracebacks.on': True})
class Root: class Root:
"""Declaration of the CherryPy app URI structure."""
_cp_config = {'tools.log_tracebacks.on': True, @cherrypy.expose
}
def index(self): def index(self):
"""Render HTML-template at the root path of the web-app."""
return """<html> return """<html>
<body>Try some <a href='%s?a=7'>other</a> path, <body>Try some <a href='%s?a=7'>other</a> path,
or a <a href='%s?n=14'>default</a> path.<br /> or a <a href='%s?n=14'>default</a> path.<br />
Or, just look at the pretty picture:<br /> Or, just look at the pretty picture:<br />
<img src='%s' /> <img src='%s' />
</body></html>""" % (url("other"), url("else"), </body></html>""" % (url('other'), url('else'),
url("files/made_with_cherrypy_small.png")) url('files/made_with_cherrypy_small.png'))
index.exposed = True
@cherrypy.expose
def default(self, *args, **kwargs): def default(self, *args, **kwargs):
return "args: %s kwargs: %s" % (args, kwargs) """Render catch-all args and kwargs."""
default.exposed = True return 'args: %s kwargs: %s' % (args, kwargs)
@cherrypy.expose
def other(self, a=2, b='bananas', c=None): def other(self, a=2, b='bananas', c=None):
"""Render number of fruits based on third argument."""
cherrypy.response.headers['Content-Type'] = 'text/plain' cherrypy.response.headers['Content-Type'] = 'text/plain'
if c is None: if c is None:
return "Have %d %s." % (int(a), b) return 'Have %d %s.' % (int(a), b)
else: else:
return "Have %d %s, %s." % (int(a), b, c) return 'Have %d %s, %s.' % (int(a), b, c)
other.exposed = True
files = cherrypy.tools.staticdir.handler( files = tools.staticdir.handler(
section="/files", section='/files',
dir=os.path.join(local_dir, "static"), dir=os.path.join(local_dir, 'static'),
# Ignore .php files, etc. # Ignore .php files, etc.
match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$', match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$',
) )
@ -57,5 +59,5 @@ Or, just look at the pretty picture:<br />
root = Root() root = Root()
# Uncomment the following to use your own favicon instead of CP's default. # Uncomment the following to use your own favicon instead of CP's default.
#favicon_path = os.path.join(local_dir, "favicon.ico") # favicon_path = os.path.join(local_dir, "favicon.ico")
#root.favicon_ico = tools.staticfile.handler(filename=favicon_path) # root.favicon_ico = tools.staticfile.handler(filename=favicon_path)

View file

@ -19,4 +19,4 @@ RewriteRule ^(.*)$ /fastcgi.pyc [L]
# If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot. # If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot.
# The filename does not have to exist in the local filesystem. URIs that Apache resolves to this # The filename does not have to exist in the local filesystem. URIs that Apache resolves to this
# filename will be handled by this external FastCGI application. # filename will be handled by this external FastCGI application.
FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088 FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088

View file

@ -1,3 +1,3 @@
[/] [/]
log.error_file: "error.log" log.error_file: "error.log"
log.access_file: "access.log" log.access_file: "access.log"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Before After
Before After

View file

@ -0,0 +1,24 @@
"""
Regression test suite for CherryPy.
"""
import os
import sys
def newexit():
os._exit(1)
def setup():
# We want to monkey patch sys.exit so that we can get some
# information about where exit is being called.
newexit._old = sys.exit
sys.exit = newexit
def teardown():
try:
sys.exit = sys.exit._old
except AttributeError:
sys.exit = sys._exit

View file

@ -0,0 +1,39 @@
"""Test module for the @-decorator syntax, which is version-specific"""
import cherrypy
from cherrypy import expose, tools
class ExposeExamples(object):
@expose
def no_call(self):
return 'Mr E. R. Bradshaw'
@expose()
def call_empty(self):
return 'Mrs. B.J. Smegma'
@expose('call_alias')
def nesbitt(self):
return 'Mr Nesbitt'
@expose(['alias1', 'alias2'])
def andrews(self):
return 'Mr Ken Andrews'
@expose(alias='alias3')
def watson(self):
return 'Mr. and Mrs. Watson'
class ToolExamples(object):
@expose
# This is here to demonstrate that using the config decorator
# does not overwrite other config attributes added by the Tool
# decorator (in this case response_headers).
@cherrypy.config(**{'response.stream': True})
@tools.response_headers(headers=[('Content-Type', 'application/data')])
def blah(self):
yield b'blah'

View file

@ -0,0 +1,69 @@
import os
import sys
import time
import cherrypy
starttime = time.time()
class Root:
@cherrypy.expose
def index(self):
return 'Hello World'
@cherrypy.expose
def mtimes(self):
return repr(cherrypy.engine.publish('Autoreloader', 'mtimes'))
@cherrypy.expose
def pid(self):
return str(os.getpid())
@cherrypy.expose
def start(self):
return repr(starttime)
@cherrypy.expose
def exit(self):
# This handler might be called before the engine is STARTED if an
# HTTP worker thread handles it before the HTTP server returns
# control to engine.start. We avoid that race condition here
# by waiting for the Bus to be STARTED.
cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
cherrypy.engine.exit()
@cherrypy.engine.subscribe('start', priority=100)
def unsub_sig():
cherrypy.log('unsubsig: %s' % cherrypy.config.get('unsubsig', False))
if cherrypy.config.get('unsubsig', False):
cherrypy.log('Unsubscribing the default cherrypy signal handler')
cherrypy.engine.signal_handler.unsubscribe()
try:
from signal import signal, SIGTERM
except ImportError:
pass
else:
def old_term_handler(signum=None, frame=None):
cherrypy.log('I am an old SIGTERM handler.')
sys.exit(0)
cherrypy.log('Subscribing the new one.')
signal(SIGTERM, old_term_handler)
@cherrypy.engine.subscribe('start', priority=6)
def starterror():
if cherrypy.config.get('starterror', False):
1 / 0
@cherrypy.engine.subscribe('start', priority=6)
def log_test_case_name():
if cherrypy.config.get('test_case_name', False):
cherrypy.log('STARTED FROM: %s' %
cherrypy.config.get('test_case_name'))
cherrypy.tree.mount(Root(), '/', {'/': {}})

View file

@ -0,0 +1,425 @@
"""CherryPy Benchmark Tool
Usage:
benchmark.py [options]
--null: use a null Request object (to bench the HTTP server only)
--notests: start the server but do not run the tests; this allows
you to check the tested pages with a browser
--help: show this help message
--cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy)
--modpython: run tests via apache on 54583 (with modpython_gateway)
--ab=path: Use the ab script/executable at 'path' (see below)
--apache=path: Use the apache script/exe at 'path' (see below)
To run the benchmarks, the Apache Benchmark tool "ab" must either be on
your system path, or specified via the --ab=path option.
To run the modpython tests, the "apache" executable or script must be
on your system path, or provided via the --apache=path option. On some
platforms, "apache" may be called "apachectl" or "apache2ctl"--create
a symlink to them if needed.
"""
import getopt
import os
import re
import sys
import time
import cherrypy
from cherrypy import _cperror, _cpmodpy
from cherrypy.lib import httputil
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
AB_PATH = ''
APACHE_PATH = 'apache'
SCRIPT_NAME = '/cpbench/users/rdelon/apps/blog'
__all__ = ['ABSession', 'Root', 'print_report',
'run_standard_benchmarks', 'safe_threads',
'size_report', 'thread_report',
]
size_cache = {}
class Root:
@cherrypy.expose
def index(self):
return """<html>
<head>
<title>CherryPy Benchmark</title>
</head>
<body>
<ul>
<li><a href="hello">Hello, world! (14 byte dynamic)</a></li>
<li><a href="static/index.html">Static file (14 bytes static)</a></li>
<li><form action="sizer">Response of length:
<input type='text' name='size' value='10' /></form>
</li>
</ul>
</body>
</html>"""
@cherrypy.expose
def hello(self):
return 'Hello, world\r\n'
@cherrypy.expose
def sizer(self, size):
resp = size_cache.get(size, None)
if resp is None:
size_cache[size] = resp = 'X' * int(size)
return resp
def init():
cherrypy.config.update({
'log.error.file': '',
'environment': 'production',
'server.socket_host': '127.0.0.1',
'server.socket_port': 54583,
'server.max_request_header_size': 0,
'server.max_request_body_size': 0,
})
# Cheat mode on ;)
del cherrypy.config['tools.log_tracebacks.on']
del cherrypy.config['tools.log_headers.on']
del cherrypy.config['tools.trailing_slash.on']
appconf = {
'/static': {
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static',
'tools.staticdir.root': curdir,
},
}
globals().update(
app=cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf),
)
class NullRequest:
"""A null HTTP request class, returning 200 and an empty body."""
def __init__(self, local, remote, scheme='http'):
pass
def close(self):
pass
def run(self, method, path, query_string, protocol, headers, rfile):
cherrypy.response.status = '200 OK'
cherrypy.response.header_list = [('Content-Type', 'text/html'),
('Server', 'Null CherryPy'),
('Date', httputil.HTTPDate()),
('Content-Length', '0'),
]
cherrypy.response.body = ['']
return cherrypy.response
class NullResponse:
pass
class ABSession:
"""A session of 'ab', the Apache HTTP server benchmarking tool.
Example output from ab:
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/
Benchmarking 127.0.0.1 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Server Software: CherryPy/3.1beta
Server Hostname: 127.0.0.1
Server Port: 54583
Document Path: /static/index.html
Document Length: 14 bytes
Concurrency Level: 10
Time taken for tests: 9.643867 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 189000 bytes
HTML transferred: 14000 bytes
Requests per second: 103.69 [#/sec] (mean)
Time per request: 96.439 [ms] (mean)
Time per request: 9.644 [ms] (mean, across all concurrent requests)
Transfer rate: 19.08 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 2.9 0 10
Processing: 20 94 7.3 90 130
Waiting: 0 43 28.1 40 100
Total: 20 95 7.3 100 130
Percentage of the requests served within a certain time (ms)
50% 100
66% 100
75% 100
80% 100
90% 100
95% 100
98% 100
99% 110
100% 130 (longest request)
Finished 1000 requests
"""
parse_patterns = [
('complete_requests', 'Completed',
br'^Complete requests:\s*(\d+)'),
('failed_requests', 'Failed',
br'^Failed requests:\s*(\d+)'),
('requests_per_second', 'req/sec',
br'^Requests per second:\s*([0-9.]+)'),
('time_per_request_concurrent', 'msec/req',
br'^Time per request:\s*([0-9.]+).*concurrent requests\)$'),
('transfer_rate', 'KB/sec',
br'^Transfer rate:\s*([0-9.]+)')
]
def __init__(self, path=SCRIPT_NAME + '/hello', requests=1000,
concurrency=10):
self.path = path
self.requests = requests
self.concurrency = concurrency
def args(self):
port = cherrypy.server.socket_port
assert self.concurrency > 0
assert self.requests > 0
# Don't use "localhost".
# Cf
# http://mail.python.org/pipermail/python-win32/2008-March/007050.html
return ('-k -n %s -c %s http://127.0.0.1:%s%s' %
(self.requests, self.concurrency, port, self.path))
def run(self):
# Parse output of ab, setting attributes on self
try:
self.output = _cpmodpy.read_process(AB_PATH or 'ab', self.args())
except Exception:
print(_cperror.format_exc())
raise
for attr, name, pattern in self.parse_patterns:
val = re.search(pattern, self.output, re.MULTILINE)
if val:
val = val.group(1)
setattr(self, attr, val)
else:
setattr(self, attr, None)
safe_threads = (25, 50, 100, 200, 400)
if sys.platform in ('win32',):
# For some reason, ab crashes with > 50 threads on my Win2k laptop.
safe_threads = (10, 20, 30, 40, 50)
def thread_report(path=SCRIPT_NAME + '/hello', concurrency=safe_threads):
sess = ABSession(path)
attrs, names, patterns = list(zip(*sess.parse_patterns))
avg = dict.fromkeys(attrs, 0.0)
yield ('threads',) + names
for c in concurrency:
sess.concurrency = c
sess.run()
row = [c]
for attr in attrs:
val = getattr(sess, attr)
if val is None:
print(sess.output)
row = None
break
val = float(val)
avg[attr] += float(val)
row.append(val)
if row:
yield row
# Add a row of averages.
yield ['Average'] + [str(avg[attr] / len(concurrency)) for attr in attrs]
def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000),
concurrency=50):
sess = ABSession(concurrency=concurrency)
attrs, names, patterns = list(zip(*sess.parse_patterns))
yield ('bytes',) + names
for sz in sizes:
sess.path = '%s/sizer?size=%s' % (SCRIPT_NAME, sz)
sess.run()
yield [sz] + [getattr(sess, attr) for attr in attrs]
def print_report(rows):
for row in rows:
print('')
for val in row:
sys.stdout.write(str(val).rjust(10) + ' | ')
print('')
def run_standard_benchmarks():
print('')
print('Client Thread Report (1000 requests, 14 byte response body, '
'%s server threads):' % cherrypy.server.thread_pool)
print_report(thread_report())
print('')
print('Client Thread Report (1000 requests, 14 bytes via staticdir, '
'%s server threads):' % cherrypy.server.thread_pool)
print_report(thread_report('%s/static/index.html' % SCRIPT_NAME))
print('')
print('Size Report (1000 requests, 50 client threads, '
'%s server threads):' % cherrypy.server.thread_pool)
print_report(size_report())
# modpython and other WSGI #
def startup_modpython(req=None):
"""Start the CherryPy app server in 'serverless' mode (for modpython/WSGI).
"""
if cherrypy.engine.state == cherrypy._cpengine.STOPPED:
if req:
if 'nullreq' in req.get_options():
cherrypy.engine.request_class = NullRequest
cherrypy.engine.response_class = NullResponse
ab_opt = req.get_options().get('ab', '')
if ab_opt:
global AB_PATH
AB_PATH = ab_opt
cherrypy.engine.start()
if cherrypy.engine.state == cherrypy._cpengine.STARTING:
cherrypy.engine.wait()
return 0 # apache.OK
def run_modpython(use_wsgi=False):
print('Starting mod_python...')
pyopts = []
# Pass the null and ab=path options through Apache
if '--null' in opts:
pyopts.append(('nullreq', ''))
if '--ab' in opts:
pyopts.append(('ab', opts['--ab']))
s = _cpmodpy.ModPythonServer
if use_wsgi:
pyopts.append(('wsgi.application', 'cherrypy::tree'))
pyopts.append(
('wsgi.startup', 'cherrypy.test.benchmark::startup_modpython'))
handler = 'modpython_gateway::handler'
s = s(port=54583, opts=pyopts,
apache_path=APACHE_PATH, handler=handler)
else:
pyopts.append(
('cherrypy.setup', 'cherrypy.test.benchmark::startup_modpython'))
s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH)
try:
s.start()
run()
finally:
s.stop()
if __name__ == '__main__':
init()
longopts = ['cpmodpy', 'modpython', 'null', 'notests',
'help', 'ab=', 'apache=']
try:
switches, args = getopt.getopt(sys.argv[1:], '', longopts)
opts = dict(switches)
except getopt.GetoptError:
print(__doc__)
sys.exit(2)
if '--help' in opts:
print(__doc__)
sys.exit(0)
if '--ab' in opts:
AB_PATH = opts['--ab']
if '--notests' in opts:
# Return without stopping the server, so that the pages
# can be tested from a standard web browser.
def run():
port = cherrypy.server.socket_port
print('You may now open http://127.0.0.1:%s%s/' %
(port, SCRIPT_NAME))
if '--null' in opts:
print('Using null Request object')
else:
def run():
end = time.time() - start
print('Started in %s seconds' % end)
if '--null' in opts:
print('\nUsing null Request object')
try:
try:
run_standard_benchmarks()
except Exception:
print(_cperror.format_exc())
raise
finally:
cherrypy.engine.exit()
print('Starting CherryPy app server...')
class NullWriter(object):
"""Suppresses the printing of socket errors."""
def write(self, data):
pass
sys.stderr = NullWriter()
start = time.time()
if '--cpmodpy' in opts:
run_modpython()
elif '--modpython' in opts:
run_modpython(use_wsgi=True)
else:
if '--null' in opts:
cherrypy.server.request_class = NullRequest
cherrypy.server.response_class = NullResponse
cherrypy.engine.start_with_callback(run)
cherrypy.engine.block()

View file

@ -0,0 +1,49 @@
"""Demonstration app for cherrypy.checker.
This application is intentionally broken and badly designed.
To demonstrate the output of the CherryPy Checker, simply execute
this module.
"""
import os
import cherrypy
thisdir = os.path.dirname(os.path.abspath(__file__))
class Root:
pass
if __name__ == '__main__':
conf = {'/base': {'tools.staticdir.root': thisdir,
# Obsolete key.
'throw_errors': True,
},
# This entry should be OK.
'/base/static': {'tools.staticdir.on': True,
'tools.staticdir.dir': 'static'},
# Warn on missing folder.
'/base/js': {'tools.staticdir.on': True,
'tools.staticdir.dir': 'js'},
# Warn on dir with an abs path even though we provide root.
'/base/static2': {'tools.staticdir.on': True,
'tools.staticdir.dir': '/static'},
# Warn on dir with a relative path with no root.
'/static3': {'tools.staticdir.on': True,
'tools.staticdir.dir': 'static'},
# Warn on unknown namespace
'/unknown': {'toobles.gzip.on': True},
# Warn special on cherrypy.<known ns>.*
'/cpknown': {'cherrypy.tools.encode.on': True},
# Warn on mismatched types
'/conftype': {'request.show_tracebacks': 14},
# Warn on unknown tool.
'/web': {'tools.unknown.on': True},
# Warn on server.* in app config.
'/app1': {'server.socket_host': '0.0.0.0'},
# Warn on 'localhost'
'global': {'server.socket_host': 'localhost'},
# Warn on '[name]'
'[/extra_brackets]': {},
}
cherrypy.quickstart(Root(), config=conf)

View file

@ -0,0 +1,18 @@
# Apache2 server conf file for testing CherryPy with mod_fastcgi.
# fumanchu: I had to hard-code paths due to crazy Debian layouts :(
ServerRoot /usr/lib/apache2
User #1000
ErrorLog /usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/mod_fastcgi.error.log
DocumentRoot "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test"
ServerName 127.0.0.1
Listen 8080
LoadModule fastcgi_module modules/mod_fastcgi.so
LoadModule rewrite_module modules/mod_rewrite.so
Options +ExecCGI
SetHandler fastcgi-script
RewriteEngine On
RewriteRule ^(.*)$ /fastcgi.pyc [L]
FastCgiExternalServer "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000

View file

@ -0,0 +1,14 @@
# Apache2 server conf file for testing CherryPy with mod_fcgid.
DocumentRoot "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test"
ServerName 127.0.0.1
Listen 8080
LoadModule fastcgi_module modules/mod_fastcgi.dll
LoadModule rewrite_module modules/mod_rewrite.so
Options ExecCGI
SetHandler fastcgi-script
RewriteEngine On
RewriteRule ^(.*)$ /fastcgi.pyc [L]
FastCgiExternalServer "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000

542
lib/cherrypy/test/helper.py Normal file
View file

@ -0,0 +1,542 @@
"""A library of helper functions for the CherryPy test suite."""
import datetime
import io
import logging
import os
import re
import subprocess
import sys
import time
import unittest
import warnings
import portend
import pytest
import six
from cheroot.test import webtest
import cherrypy
from cherrypy._cpcompat import text_or_bytes, HTTPSConnection, ntob
from cherrypy.lib import httputil
from cherrypy.lib import gctools
log = logging.getLogger(__name__)
thisdir = os.path.abspath(os.path.dirname(__file__))
serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
class Supervisor(object):
"""Base class for modeling and controlling servers during testing."""
def __init__(self, **kwargs):
for k, v in kwargs.items():
if k == 'port':
setattr(self, k, int(v))
setattr(self, k, v)
def log_to_stderr(msg, level):
return sys.stderr.write(msg + os.linesep)
class LocalSupervisor(Supervisor):
"""Base class for modeling/controlling servers which run in the same
process.
When the server side runs in a different process, start/stop can dump all
state between each test module easily. When the server side runs in the
same process as the client, however, we have to do a bit more work to
ensure config and mounted apps are reset between tests.
"""
using_apache = False
using_wsgi = False
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
cherrypy.server.httpserver = self.httpserver_class
# This is perhaps the wrong place for this call but this is the only
# place that i've found so far that I KNOW is early enough to set this.
cherrypy.config.update({'log.screen': False})
engine = cherrypy.engine
if hasattr(engine, 'signal_handler'):
engine.signal_handler.subscribe()
if hasattr(engine, 'console_control_handler'):
engine.console_control_handler.subscribe()
def start(self, modulename=None):
"""Load and start the HTTP server."""
if modulename:
# Unhook httpserver so cherrypy.server.start() creates a new
# one (with config from setup_server, if declared).
cherrypy.server.httpserver = None
cherrypy.engine.start()
self.sync_apps()
def sync_apps(self):
"""Tell the server about any apps which the setup functions mounted."""
pass
def stop(self):
td = getattr(self, 'teardown', None)
if td:
td()
cherrypy.engine.exit()
servers_copy = list(six.iteritems(getattr(cherrypy, 'servers', {})))
for name, server in servers_copy:
server.unsubscribe()
del cherrypy.servers[name]
class NativeServerSupervisor(LocalSupervisor):
"""Server supervisor for the builtin HTTP server."""
httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer'
using_apache = False
using_wsgi = False
def __str__(self):
return 'Builtin HTTP Server on %s:%s' % (self.host, self.port)
class LocalWSGISupervisor(LocalSupervisor):
"""Server supervisor for the builtin WSGI server."""
httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer'
using_apache = False
using_wsgi = True
def __str__(self):
return 'Builtin WSGI Server on %s:%s' % (self.host, self.port)
def sync_apps(self):
"""Hook a new WSGI app into the origin server."""
cherrypy.server.httpserver.wsgi_app = self.get_app()
def get_app(self, app=None):
"""Obtain a new (decorated) WSGI app to hook into the origin server."""
if app is None:
app = cherrypy.tree
if self.validate:
try:
from wsgiref import validate
except ImportError:
warnings.warn(
'Error importing wsgiref. The validator will not run.')
else:
# wraps the app in the validator
app = validate.validator(app)
return app
def get_cpmodpy_supervisor(**options):
from cherrypy.test import modpy
sup = modpy.ModPythonSupervisor(**options)
sup.template = modpy.conf_cpmodpy
return sup
def get_modpygw_supervisor(**options):
from cherrypy.test import modpy
sup = modpy.ModPythonSupervisor(**options)
sup.template = modpy.conf_modpython_gateway
sup.using_wsgi = True
return sup
def get_modwsgi_supervisor(**options):
from cherrypy.test import modwsgi
return modwsgi.ModWSGISupervisor(**options)
def get_modfcgid_supervisor(**options):
from cherrypy.test import modfcgid
return modfcgid.ModFCGISupervisor(**options)
def get_modfastcgi_supervisor(**options):
from cherrypy.test import modfastcgi
return modfastcgi.ModFCGISupervisor(**options)
def get_wsgi_u_supervisor(**options):
cherrypy.server.wsgi_version = ('u', 0)
return LocalWSGISupervisor(**options)
class CPWebCase(webtest.WebCase):
script_name = ''
scheme = 'http'
available_servers = {'wsgi': LocalWSGISupervisor,
'wsgi_u': get_wsgi_u_supervisor,
'native': NativeServerSupervisor,
'cpmodpy': get_cpmodpy_supervisor,
'modpygw': get_modpygw_supervisor,
'modwsgi': get_modwsgi_supervisor,
'modfcgid': get_modfcgid_supervisor,
'modfastcgi': get_modfastcgi_supervisor,
}
default_server = 'wsgi'
@classmethod
def _setup_server(cls, supervisor, conf):
v = sys.version.split()[0]
log.info('Python version used to run this test script: %s' % v)
log.info('CherryPy version: %s' % cherrypy.__version__)
if supervisor.scheme == 'https':
ssl = ' (ssl)'
else:
ssl = ''
log.info('HTTP server version: %s%s' % (supervisor.protocol, ssl))
log.info('PID: %s' % os.getpid())
cherrypy.server.using_apache = supervisor.using_apache
cherrypy.server.using_wsgi = supervisor.using_wsgi
if sys.platform[:4] == 'java':
cherrypy.config.update({'server.nodelay': False})
if isinstance(conf, text_or_bytes):
parser = cherrypy.lib.reprconf.Parser()
conf = parser.dict_from_file(conf).get('global', {})
else:
conf = conf or {}
baseconf = conf.copy()
baseconf.update({'server.socket_host': supervisor.host,
'server.socket_port': supervisor.port,
'server.protocol_version': supervisor.protocol,
'environment': 'test_suite',
})
if supervisor.scheme == 'https':
# baseconf['server.ssl_module'] = 'builtin'
baseconf['server.ssl_certificate'] = serverpem
baseconf['server.ssl_private_key'] = serverpem
# helper must be imported lazily so the coverage tool
# can run against module-level statements within cherrypy.
# Also, we have to do "from cherrypy.test import helper",
# exactly like each test module does, because a relative import
# would stick a second instance of webtest in sys.modules,
# and we wouldn't be able to globally override the port anymore.
if supervisor.scheme == 'https':
webtest.WebCase.HTTP_CONN = HTTPSConnection
return baseconf
@classmethod
def setup_class(cls):
''
# Creates a server
conf = {
'scheme': 'http',
'protocol': 'HTTP/1.1',
'port': 54583,
'host': '127.0.0.1',
'validate': False,
'server': 'wsgi',
}
supervisor_factory = cls.available_servers.get(
conf.get('server', 'wsgi'))
if supervisor_factory is None:
raise RuntimeError('Unknown server in config: %s' % conf['server'])
supervisor = supervisor_factory(**conf)
# Copied from "run_test_suite"
cherrypy.config.reset()
baseconf = cls._setup_server(supervisor, conf)
cherrypy.config.update(baseconf)
setup_client()
if hasattr(cls, 'setup_server'):
# Clear the cherrypy tree and clear the wsgi server so that
# it can be updated with the new root
cherrypy.tree = cherrypy._cptree.Tree()
cherrypy.server.httpserver = None
cls.setup_server()
# Add a resource for verifying there are no refleaks
# to *every* test class.
cherrypy.tree.mount(gctools.GCRoot(), '/gc')
cls.do_gc_test = True
supervisor.start(cls.__module__)
cls.supervisor = supervisor
@classmethod
def teardown_class(cls):
''
if hasattr(cls, 'setup_server'):
cls.supervisor.stop()
do_gc_test = False
def test_gc(self):
if not self.do_gc_test:
return
self.getPage('/gc/stats')
try:
self.assertBody('Statistics:')
except Exception:
'Failures occur intermittently. See #1420'
def prefix(self):
return self.script_name.rstrip('/')
def base(self):
if ((self.scheme == 'http' and self.PORT == 80) or
(self.scheme == 'https' and self.PORT == 443)):
port = ''
else:
port = ':%s' % self.PORT
return '%s://%s%s%s' % (self.scheme, self.HOST, port,
self.script_name.rstrip('/'))
def exit(self):
sys.exit()
def getPage(self, url, headers=None, method='GET', body=None,
protocol=None, raise_subcls=None):
"""Open the url. Return status, headers, body.
`raise_subcls` must be a tuple with the exceptions classes
or a single exception class that are not going to be considered
a socket.error regardless that they were are subclass of a
socket.error and therefore not considered for a connection retry.
"""
if self.script_name:
url = httputil.urljoin(self.script_name, url)
return webtest.WebCase.getPage(self, url, headers, method, body,
protocol, raise_subcls)
def skip(self, msg='skipped '):
pytest.skip(msg)
def assertErrorPage(self, status, message=None, pattern=''):
"""Compare the response body with a built in error page.
The function will optionally look for the regexp pattern,
within the exception embedded in the error page."""
# This will never contain a traceback
page = cherrypy._cperror.get_error_page(status, message=message)
# First, test the response body without checking the traceback.
# Stick a match-all group (.*) in to grab the traceback.
def esc(text):
return re.escape(ntob(text))
epage = re.escape(page)
epage = epage.replace(
esc('<pre id="traceback"></pre>'),
esc('<pre id="traceback">') + b'(.*)' + esc('</pre>'))
m = re.match(epage, self.body, re.DOTALL)
if not m:
self._handlewebError(
'Error page does not match; expected:\n' + page)
return
# Now test the pattern against the traceback
if pattern is None:
# Special-case None to mean that there should be *no* traceback.
if m and m.group(1):
self._handlewebError('Error page contains traceback')
else:
if (m is None) or (
not re.search(ntob(re.escape(pattern), self.encoding),
m.group(1))):
msg = 'Error page does not contain %s in traceback'
self._handlewebError(msg % repr(pattern))
date_tolerance = 2
def assertEqualDates(self, dt1, dt2, seconds=None):
"""Assert abs(dt1 - dt2) is within Y seconds."""
if seconds is None:
seconds = self.date_tolerance
if dt1 > dt2:
diff = dt1 - dt2
else:
diff = dt2 - dt1
if not diff < datetime.timedelta(seconds=seconds):
raise AssertionError('%r and %r are not within %r seconds.' %
(dt1, dt2, seconds))
def _test_method_sorter(_, x, y):
"""Monkeypatch the test sorter to always run test_gc last in each suite."""
if x == 'test_gc':
return 1
if y == 'test_gc':
return -1
if x > y:
return 1
if x < y:
return -1
return 0
unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter
def setup_client():
"""Set up the WebCase classes to match the server's socket settings."""
webtest.WebCase.PORT = cherrypy.server.socket_port
webtest.WebCase.HOST = cherrypy.server.socket_host
if cherrypy.server.ssl_certificate:
CPWebCase.scheme = 'https'
# --------------------------- Spawning helpers --------------------------- #
class CPProcess(object):
pid_file = os.path.join(thisdir, 'test.pid')
config_file = os.path.join(thisdir, 'test.conf')
config_template = """[global]
server.socket_host: '%(host)s'
server.socket_port: %(port)s
checker.on: False
log.screen: False
log.error_file: r'%(error_log)s'
log.access_file: r'%(access_log)s'
%(ssl)s
%(extra)s
"""
error_log = os.path.join(thisdir, 'test.error.log')
access_log = os.path.join(thisdir, 'test.access.log')
def __init__(self, wait=False, daemonize=False, ssl=False,
socket_host=None, socket_port=None):
self.wait = wait
self.daemonize = daemonize
self.ssl = ssl
self.host = socket_host or cherrypy.server.socket_host
self.port = socket_port or cherrypy.server.socket_port
def write_conf(self, extra=''):
if self.ssl:
serverpem = os.path.join(thisdir, 'test.pem')
ssl = """
server.ssl_certificate: r'%s'
server.ssl_private_key: r'%s'
""" % (serverpem, serverpem)
else:
ssl = ''
conf = self.config_template % {
'host': self.host,
'port': self.port,
'error_log': self.error_log,
'access_log': self.access_log,
'ssl': ssl,
'extra': extra,
}
with io.open(self.config_file, 'w', encoding='utf-8') as f:
f.write(six.text_type(conf))
def start(self, imports=None):
"""Start cherryd in a subprocess."""
portend.free(self.host, self.port, timeout=1)
args = [
'-m',
'cherrypy',
'-c', self.config_file,
'-p', self.pid_file,
]
r"""
Command for running cherryd server with autoreload enabled
Using
```
['-c',
"__requires__ = 'CherryPy'; \
import pkg_resources, re, sys; \
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \
sys.exit(\
pkg_resources.load_entry_point(\
'CherryPy', 'console_scripts', 'cherryd')())"]
```
doesn't work as it's impossible to reconstruct the `-c`'s contents.
Ref: https://github.com/cherrypy/cherrypy/issues/1545
"""
if not isinstance(imports, (list, tuple)):
imports = [imports]
for i in imports:
if i:
args.append('-i')
args.append(i)
if self.daemonize:
args.append('-d')
env = os.environ.copy()
# Make sure we import the cherrypy package in which this module is
# defined.
grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..'))
if env.get('PYTHONPATH', ''):
env['PYTHONPATH'] = os.pathsep.join(
(grandparentdir, env['PYTHONPATH']))
else:
env['PYTHONPATH'] = grandparentdir
self._proc = subprocess.Popen([sys.executable] + args, env=env)
if self.wait:
self.exit_code = self._proc.wait()
else:
portend.occupied(self.host, self.port, timeout=5)
# Give the engine a wee bit more time to finish STARTING
if self.daemonize:
time.sleep(2)
else:
time.sleep(1)
def get_pid(self):
if self.daemonize:
return int(open(self.pid_file, 'rb').read())
return self._proc.pid
def join(self):
"""Wait for the process to exit."""
if self.daemonize:
return self._join_daemon()
self._proc.wait()
def _join_daemon(self):
try:
try:
# Mac, UNIX
os.wait()
except AttributeError:
# Windows
try:
pid = self.get_pid()
except IOError:
# Assume the subprocess deleted the pidfile on shutdown.
pass
else:
os.waitpid(pid, 0)
except OSError:
x = sys.exc_info()[1]
if x.args != (10, 'No child processes'):
raise

View file

@ -0,0 +1,228 @@
"""logtest, a unittest.TestCase helper for testing log output."""
import sys
import time
from uuid import UUID
import six
from cherrypy._cpcompat import text_or_bytes, ntob
try:
# On Windows, msvcrt.getch reads a single char without output.
import msvcrt
def getchar():
return msvcrt.getch()
except ImportError:
# Unix getchr
import tty
import termios
def getchar():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
class LogCase(object):
"""unittest.TestCase mixin for testing log messages.
logfile: a filename for the desired log. Yes, I know modes are evil,
but it makes the test functions so much cleaner to set this once.
lastmarker: the last marker in the log. This can be used to search for
messages since the last marker.
markerPrefix: a string with which to prefix log markers. This should be
unique enough from normal log output to use for marker identification.
"""
logfile = None
lastmarker = None
markerPrefix = b'test suite marker: '
def _handleLogError(self, msg, data, marker, pattern):
print('')
print(' ERROR: %s' % msg)
if not self.interactive:
raise self.failureException(msg)
p = (' Show: '
'[L]og [M]arker [P]attern; '
'[I]gnore, [R]aise, or sys.e[X]it >> ')
sys.stdout.write(p + ' ')
# ARGH
sys.stdout.flush()
while True:
i = getchar().upper()
if i not in 'MPLIRX':
continue
print(i.upper()) # Also prints new line
if i == 'L':
for x, line in enumerate(data):
if (x + 1) % self.console_height == 0:
# The \r and comma should make the next line overwrite
sys.stdout.write('<-- More -->\r ')
m = getchar().lower()
# Erase our "More" prompt
sys.stdout.write(' \r ')
if m == 'q':
break
print(line.rstrip())
elif i == 'M':
print(repr(marker or self.lastmarker))
elif i == 'P':
print(repr(pattern))
elif i == 'I':
# return without raising the normal exception
return
elif i == 'R':
raise self.failureException(msg)
elif i == 'X':
self.exit()
sys.stdout.write(p + ' ')
def exit(self):
sys.exit()
def emptyLog(self):
"""Overwrite self.logfile with 0 bytes."""
open(self.logfile, 'wb').write('')
def markLog(self, key=None):
"""Insert a marker line into the log and set self.lastmarker."""
if key is None:
key = str(time.time())
self.lastmarker = key
open(self.logfile, 'ab+').write(
ntob('%s%s\n' % (self.markerPrefix, key), 'utf-8'))
def _read_marked_region(self, marker=None):
"""Return lines from self.logfile in the marked region.
If marker is None, self.lastmarker is used. If the log hasn't
been marked (using self.markLog), the entire log will be returned.
"""
# Give the logger time to finish writing?
# time.sleep(0.5)
logfile = self.logfile
marker = marker or self.lastmarker
if marker is None:
return open(logfile, 'rb').readlines()
if isinstance(marker, six.text_type):
marker = marker.encode('utf-8')
data = []
in_region = False
for line in open(logfile, 'rb'):
if in_region:
if line.startswith(self.markerPrefix) and marker not in line:
break
else:
data.append(line)
elif marker in line:
in_region = True
return data
def assertInLog(self, line, marker=None):
"""Fail if the given (partial) line is not in the log.
The log will be searched from the given marker to the next marker.
If marker is None, self.lastmarker is used. If the log hasn't
been marked (using self.markLog), the entire log will be searched.
"""
data = self._read_marked_region(marker)
for logline in data:
if line in logline:
return
msg = '%r not found in log' % line
self._handleLogError(msg, data, marker, line)
def assertNotInLog(self, line, marker=None):
"""Fail if the given (partial) line is in the log.
The log will be searched from the given marker to the next marker.
If marker is None, self.lastmarker is used. If the log hasn't
been marked (using self.markLog), the entire log will be searched.
"""
data = self._read_marked_region(marker)
for logline in data:
if line in logline:
msg = '%r found in log' % line
self._handleLogError(msg, data, marker, line)
def assertValidUUIDv4(self, marker=None):
"""Fail if the given UUIDv4 is not valid.
The log will be searched from the given marker to the next marker.
If marker is None, self.lastmarker is used. If the log hasn't
been marked (using self.markLog), the entire log will be searched.
"""
data = self._read_marked_region(marker)
data = [
chunk.decode('utf-8').rstrip('\n').rstrip('\r')
for chunk in data
]
for log_chunk in data:
try:
uuid_log = data[-1]
uuid_obj = UUID(uuid_log, version=4)
except (TypeError, ValueError):
pass # it might be in other chunk
else:
if str(uuid_obj) == uuid_log:
return
msg = '%r is not a valid UUIDv4' % uuid_log
self._handleLogError(msg, data, marker, log_chunk)
msg = 'UUIDv4 not found in log'
self._handleLogError(msg, data, marker, log_chunk)
def assertLog(self, sliceargs, lines, marker=None):
"""Fail if log.readlines()[sliceargs] is not contained in 'lines'.
The log will be searched from the given marker to the next marker.
If marker is None, self.lastmarker is used. If the log hasn't
been marked (using self.markLog), the entire log will be searched.
"""
data = self._read_marked_region(marker)
if isinstance(sliceargs, int):
# Single arg. Use __getitem__ and allow lines to be str or list.
if isinstance(lines, (tuple, list)):
lines = lines[0]
if isinstance(lines, six.text_type):
lines = lines.encode('utf-8')
if lines not in data[sliceargs]:
msg = '%r not found on log line %r' % (lines, sliceargs)
self._handleLogError(
msg,
[data[sliceargs], '--EXTRA CONTEXT--'] + data[
sliceargs + 1:sliceargs + 6],
marker,
lines)
else:
# Multiple args. Use __getslice__ and require lines to be list.
if isinstance(lines, tuple):
lines = list(lines)
elif isinstance(lines, text_or_bytes):
raise TypeError("The 'lines' arg must be a list when "
"'sliceargs' is a tuple.")
start, stop = sliceargs
for line, logline in zip(lines, data[start:stop]):
if isinstance(line, six.text_type):
line = line.encode('utf-8')
if line not in logline:
msg = '%r not found in log' % line
self._handleLogError(msg, data[start:stop], marker, line)

View file

@ -0,0 +1,136 @@
"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing.
To autostart fastcgi, the "apache" executable or script must be
on your system path, or you must override the global APACHE_PATH.
On some platforms, "apache" may be called "apachectl", "apache2ctl",
or "httpd"--create a symlink to them if needed.
You'll also need the WSGIServer from flup.servers.
See http://projects.amor.org/misc/wiki/ModPythonGateway
KNOWN BUGS
==========
1. Apache processes Range headers automatically; CherryPy's truncated
output is then truncated again by Apache. See test_core.testRanges.
This was worked around in http://www.cherrypy.org/changeset/1319.
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
See test_core.testHTTPMethods.
3. Max request header and body settings do not work with Apache.
4. Apache replaces status "reason phrases" automatically. For example,
CherryPy may set "304 Not modified" but Apache will write out
"304 Not Modified" (capital "M").
5. Apache does not allow custom error codes as per the spec.
6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
Request-URI too early.
7. mod_python will not read request bodies which use the "chunked"
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
mod_python's requestobject.c).
8. Apache will output a "Content-Length: 0" response header even if there's
no response entity body. This isn't really a bug; it just differs from
the CherryPy default.
"""
import os
import re
import cherrypy
from cherrypy.process import servers
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
def read_process(cmd, args=''):
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
try:
firstline = pipeout.readline()
if (re.search(r'(not recognized|No such file|not found)', firstline,
re.IGNORECASE)):
raise IOError('%s must be on your system path.' % cmd)
output = firstline + pipeout.read()
finally:
pipeout.close()
return output
APACHE_PATH = 'apache2ctl'
CONF_PATH = 'fastcgi.conf'
conf_fastcgi = """
# Apache2 server conf file for testing CherryPy with mod_fastcgi.
# fumanchu: I had to hard-code paths due to crazy Debian layouts :(
ServerRoot /usr/lib/apache2
User #1000
ErrorLog %(root)s/mod_fastcgi.error.log
DocumentRoot "%(root)s"
ServerName 127.0.0.1
Listen %(port)s
LoadModule fastcgi_module modules/mod_fastcgi.so
LoadModule rewrite_module modules/mod_rewrite.so
Options +ExecCGI
SetHandler fastcgi-script
RewriteEngine On
RewriteRule ^(.*)$ /fastcgi.pyc [L]
FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
"""
def erase_script_name(environ, start_response):
environ['SCRIPT_NAME'] = ''
return cherrypy.tree(environ, start_response)
class ModFCGISupervisor(helper.LocalWSGISupervisor):
httpserver_class = 'cherrypy.process.servers.FlupFCGIServer'
using_apache = True
using_wsgi = True
template = conf_fastcgi
def __str__(self):
return 'FCGI Server on %s:%s' % (self.host, self.port)
def start(self, modulename):
cherrypy.server.httpserver = servers.FlupFCGIServer(
application=erase_script_name, bindAddress=('127.0.0.1', 4000))
cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
cherrypy.server.socket_port = 4000
# For FCGI, we both start apache...
self.start_apache()
# ...and our local server
cherrypy.engine.start()
self.sync_apps()
def start_apache(self):
fcgiconf = CONF_PATH
if not os.path.isabs(fcgiconf):
fcgiconf = os.path.join(curdir, fcgiconf)
# Write the Apache conf file.
f = open(fcgiconf, 'wb')
try:
server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
output = self.template % {'port': self.port, 'root': curdir,
'server': server}
output = output.replace('\r\n', '\n')
f.write(output)
finally:
f.close()
result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
if result:
print(result)
def stop(self):
"""Gracefully shutdown a server that is serving forever."""
read_process(APACHE_PATH, '-k stop')
helper.LocalWSGISupervisor.stop(self)
def sync_apps(self):
cherrypy.server.httpserver.fcgiserver.application = self.get_app(
erase_script_name)

View file

@ -0,0 +1,124 @@
"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing.
To autostart fcgid, the "apache" executable or script must be
on your system path, or you must override the global APACHE_PATH.
On some platforms, "apache" may be called "apachectl", "apache2ctl",
or "httpd"--create a symlink to them if needed.
You'll also need the WSGIServer from flup.servers.
See http://projects.amor.org/misc/wiki/ModPythonGateway
KNOWN BUGS
==========
1. Apache processes Range headers automatically; CherryPy's truncated
output is then truncated again by Apache. See test_core.testRanges.
This was worked around in http://www.cherrypy.org/changeset/1319.
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
See test_core.testHTTPMethods.
3. Max request header and body settings do not work with Apache.
4. Apache replaces status "reason phrases" automatically. For example,
CherryPy may set "304 Not modified" but Apache will write out
"304 Not Modified" (capital "M").
5. Apache does not allow custom error codes as per the spec.
6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
Request-URI too early.
7. mod_python will not read request bodies which use the "chunked"
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
mod_python's requestobject.c).
8. Apache will output a "Content-Length: 0" response header even if there's
no response entity body. This isn't really a bug; it just differs from
the CherryPy default.
"""
import os
import re
import cherrypy
from cherrypy._cpcompat import ntob
from cherrypy.process import servers
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
def read_process(cmd, args=''):
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
try:
firstline = pipeout.readline()
if (re.search(r'(not recognized|No such file|not found)', firstline,
re.IGNORECASE)):
raise IOError('%s must be on your system path.' % cmd)
output = firstline + pipeout.read()
finally:
pipeout.close()
return output
APACHE_PATH = 'httpd'
CONF_PATH = 'fcgi.conf'
conf_fcgid = """
# Apache2 server conf file for testing CherryPy with mod_fcgid.
DocumentRoot "%(root)s"
ServerName 127.0.0.1
Listen %(port)s
LoadModule fastcgi_module modules/mod_fastcgi.dll
LoadModule rewrite_module modules/mod_rewrite.so
Options ExecCGI
SetHandler fastcgi-script
RewriteEngine On
RewriteRule ^(.*)$ /fastcgi.pyc [L]
FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
"""
class ModFCGISupervisor(helper.LocalSupervisor):
using_apache = True
using_wsgi = True
template = conf_fcgid
def __str__(self):
return 'FCGI Server on %s:%s' % (self.host, self.port)
def start(self, modulename):
cherrypy.server.httpserver = servers.FlupFCGIServer(
application=cherrypy.tree, bindAddress=('127.0.0.1', 4000))
cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
# For FCGI, we both start apache...
self.start_apache()
# ...and our local server
helper.LocalServer.start(self, modulename)
def start_apache(self):
fcgiconf = CONF_PATH
if not os.path.isabs(fcgiconf):
fcgiconf = os.path.join(curdir, fcgiconf)
# Write the Apache conf file.
f = open(fcgiconf, 'wb')
try:
server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
output = self.template % {'port': self.port, 'root': curdir,
'server': server}
output = ntob(output.replace('\r\n', '\n'))
f.write(output)
finally:
f.close()
result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
if result:
print(result)
def stop(self):
"""Gracefully shutdown a server that is serving forever."""
read_process(APACHE_PATH, '-k stop')
helper.LocalServer.stop(self)
def sync_apps(self):
cherrypy.server.httpserver.fcgiserver.application = self.get_app()

164
lib/cherrypy/test/modpy.py Normal file
View file

@ -0,0 +1,164 @@
"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing.
To autostart modpython, the "apache" executable or script must be
on your system path, or you must override the global APACHE_PATH.
On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
create a symlink to them if needed.
If you wish to test the WSGI interface instead of our _cpmodpy interface,
you also need the 'modpython_gateway' module at:
http://projects.amor.org/misc/wiki/ModPythonGateway
KNOWN BUGS
==========
1. Apache processes Range headers automatically; CherryPy's truncated
output is then truncated again by Apache. See test_core.testRanges.
This was worked around in http://www.cherrypy.org/changeset/1319.
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
See test_core.testHTTPMethods.
3. Max request header and body settings do not work with Apache.
4. Apache replaces status "reason phrases" automatically. For example,
CherryPy may set "304 Not modified" but Apache will write out
"304 Not Modified" (capital "M").
5. Apache does not allow custom error codes as per the spec.
6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
Request-URI too early.
7. mod_python will not read request bodies which use the "chunked"
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
mod_python's requestobject.c).
8. Apache will output a "Content-Length: 0" response header even if there's
no response entity body. This isn't really a bug; it just differs from
the CherryPy default.
"""
import os
import re
import cherrypy
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
def read_process(cmd, args=''):
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
try:
firstline = pipeout.readline()
if (re.search(r'(not recognized|No such file|not found)', firstline,
re.IGNORECASE)):
raise IOError('%s must be on your system path.' % cmd)
output = firstline + pipeout.read()
finally:
pipeout.close()
return output
APACHE_PATH = 'httpd'
CONF_PATH = 'test_mp.conf'
conf_modpython_gateway = """
# Apache2 server conf file for testing CherryPy with modpython_gateway.
ServerName 127.0.0.1
DocumentRoot "/"
Listen %(port)s
LoadModule python_module modules/mod_python.so
SetHandler python-program
PythonFixupHandler cherrypy.test.modpy::wsgisetup
PythonOption testmod %(modulename)s
PythonHandler modpython_gateway::handler
PythonOption wsgi.application cherrypy::tree
PythonOption socket_host %(host)s
PythonDebug On
"""
conf_cpmodpy = """
# Apache2 server conf file for testing CherryPy with _cpmodpy.
ServerName 127.0.0.1
DocumentRoot "/"
Listen %(port)s
LoadModule python_module modules/mod_python.so
SetHandler python-program
PythonFixupHandler cherrypy.test.modpy::cpmodpysetup
PythonHandler cherrypy._cpmodpy::handler
PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server
PythonOption socket_host %(host)s
PythonDebug On
"""
class ModPythonSupervisor(helper.Supervisor):
using_apache = True
using_wsgi = False
template = None
def __str__(self):
return 'ModPython Server on %s:%s' % (self.host, self.port)
def start(self, modulename):
mpconf = CONF_PATH
if not os.path.isabs(mpconf):
mpconf = os.path.join(curdir, mpconf)
f = open(mpconf, 'wb')
try:
f.write(self.template %
{'port': self.port, 'modulename': modulename,
'host': self.host})
finally:
f.close()
result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
if result:
print(result)
def stop(self):
"""Gracefully shutdown a server that is serving forever."""
read_process(APACHE_PATH, '-k stop')
loaded = False
def wsgisetup(req):
global loaded
if not loaded:
loaded = True
options = req.get_options()
cherrypy.config.update({
'log.error_file': os.path.join(curdir, 'test.log'),
'environment': 'test_suite',
'server.socket_host': options['socket_host'],
})
modname = options['testmod']
mod = __import__(modname, globals(), locals(), [''])
mod.setup_server()
cherrypy.server.unsubscribe()
cherrypy.engine.start()
from mod_python import apache
return apache.OK
def cpmodpysetup(req):
global loaded
if not loaded:
loaded = True
options = req.get_options()
cherrypy.config.update({
'log.error_file': os.path.join(curdir, 'test.log'),
'environment': 'test_suite',
'server.socket_host': options['socket_host'],
})
from mod_python import apache
return apache.OK

View file

@ -0,0 +1,154 @@
"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server.
To autostart modwsgi, the "apache" executable or script must be
on your system path, or you must override the global APACHE_PATH.
On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
create a symlink to them if needed.
KNOWN BUGS
==========
##1. Apache processes Range headers automatically; CherryPy's truncated
## output is then truncated again by Apache. See test_core.testRanges.
## This was worked around in http://www.cherrypy.org/changeset/1319.
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
See test_core.testHTTPMethods.
3. Max request header and body settings do not work with Apache.
##4. Apache replaces status "reason phrases" automatically. For example,
## CherryPy may set "304 Not modified" but Apache will write out
## "304 Not Modified" (capital "M").
##5. Apache does not allow custom error codes as per the spec.
##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
## Request-URI too early.
7. mod_wsgi will not read request bodies which use the "chunked"
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
mod_python's requestobject.c).
8. When responding with 204 No Content, mod_wsgi adds a Content-Length
header for you.
9. When an error is raised, mod_wsgi has no facility for printing a
traceback as the response content (it's sent to the Apache log instead).
10. Startup and shutdown of Apache when running mod_wsgi seems slow.
"""
import os
import re
import sys
import time
import portend
from cheroot.test import webtest
import cherrypy
from cherrypy.test import helper
curdir = os.path.abspath(os.path.dirname(__file__))
def read_process(cmd, args=''):
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
try:
firstline = pipeout.readline()
if (re.search(r'(not recognized|No such file|not found)', firstline,
re.IGNORECASE)):
raise IOError('%s must be on your system path.' % cmd)
output = firstline + pipeout.read()
finally:
pipeout.close()
return output
if sys.platform == 'win32':
APACHE_PATH = 'httpd'
else:
APACHE_PATH = 'apache'
CONF_PATH = 'test_mw.conf'
conf_modwsgi = r"""
# Apache2 server conf file for testing CherryPy with modpython_gateway.
ServerName 127.0.0.1
DocumentRoot "/"
Listen %(port)s
AllowEncodedSlashes On
LoadModule rewrite_module modules/mod_rewrite.so
RewriteEngine on
RewriteMap escaping int:escape
LoadModule log_config_module modules/mod_log_config.so
LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined
CustomLog "%(curdir)s/apache.access.log" combined
ErrorLog "%(curdir)s/apache.error.log"
LogLevel debug
LoadModule wsgi_module modules/mod_wsgi.so
LoadModule env_module modules/mod_env.so
WSGIScriptAlias / "%(curdir)s/modwsgi.py"
SetEnv testmod %(testmod)s
""" # noqa E501
class ModWSGISupervisor(helper.Supervisor):
"""Server Controller for ModWSGI and CherryPy."""
using_apache = True
using_wsgi = True
template = conf_modwsgi
def __str__(self):
return 'ModWSGI Server on %s:%s' % (self.host, self.port)
def start(self, modulename):
mpconf = CONF_PATH
if not os.path.isabs(mpconf):
mpconf = os.path.join(curdir, mpconf)
f = open(mpconf, 'wb')
try:
output = (self.template %
{'port': self.port, 'testmod': modulename,
'curdir': curdir})
f.write(output)
finally:
f.close()
result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
if result:
print(result)
# Make a request so mod_wsgi starts up our app.
# If we don't, concurrent initial requests will 404.
portend.occupied('127.0.0.1', self.port, timeout=5)
webtest.openURL('/ihopetheresnodefault', port=self.port)
time.sleep(1)
def stop(self):
"""Gracefully shutdown a server that is serving forever."""
read_process(APACHE_PATH, '-k stop')
loaded = False
def application(environ, start_response):
global loaded
if not loaded:
loaded = True
modname = 'cherrypy.test.' + environ['testmod']
mod = __import__(modname, globals(), locals(), [''])
mod.setup_server()
cherrypy.config.update({
'log.error_file': os.path.join(curdir, 'test.error.log'),
'log.access_file': os.path.join(curdir, 'test.access.log'),
'environment': 'test_suite',
'engine.SIGHUP': None,
'engine.SIGTERM': None,
})
return cherrypy.tree(environ, start_response)

View file

@ -0,0 +1,161 @@
#!/usr/bin/python
"""A session demonstration app."""
import calendar
from datetime import datetime
import sys
import six
import cherrypy
from cherrypy.lib import sessions
page = """
<html>
<head>
<style type='text/css'>
table { border-collapse: collapse; border: 1px solid #663333; }
th { text-align: right; background-color: #663333; color: white; padding: 0.5em; }
td { white-space: pre-wrap; font-family: monospace; padding: 0.5em;
border: 1px solid #663333; }
.warn { font-family: serif; color: #990000; }
</style>
<script type="text/javascript">
<!--
function twodigit(d) { return d < 10 ? "0" + d : d; }
function formattime(t) {
var month = t.getUTCMonth() + 1;
var day = t.getUTCDate();
var year = t.getUTCFullYear();
var hours = t.getUTCHours();
var minutes = t.getUTCMinutes();
return (year + "/" + twodigit(month) + "/" + twodigit(day) + " " +
hours + ":" + twodigit(minutes) + " UTC");
}
function interval(s) {
// Return the given interval (in seconds) as an English phrase
var seconds = s %% 60;
s = Math.floor(s / 60);
var minutes = s %% 60;
s = Math.floor(s / 60);
var hours = s %% 24;
var v = twodigit(hours) + ":" + twodigit(minutes) + ":" + twodigit(seconds);
var days = Math.floor(s / 24);
if (days != 0) v = days + ' days, ' + v;
return v;
}
var fudge_seconds = 5;
function init() {
// Set the content of the 'btime' cell.
var currentTime = new Date();
var bunixtime = Math.floor(currentTime.getTime() / 1000);
var v = formattime(currentTime);
v += " (Unix time: " + bunixtime + ")";
var diff = Math.abs(%(serverunixtime)s - bunixtime);
if (diff > fudge_seconds) v += "<p class='warn'>Browser and Server times disagree.</p>";
document.getElementById('btime').innerHTML = v;
// Warn if response cookie expires is not close to one hour in the future.
// Yes, we want this to happen when wit hit the 'Expire' link, too.
var expires = Date.parse("%(expires)s") / 1000;
var onehour = (60 * 60);
if (Math.abs(expires - (bunixtime + onehour)) > fudge_seconds) {
diff = Math.floor(expires - bunixtime);
if (expires > (bunixtime + onehour)) {
var msg = "Response cookie 'expires' date is " + interval(diff) + " in the future.";
} else {
var msg = "Response cookie 'expires' date is " + interval(0 - diff) + " in the past.";
}
document.getElementById('respcookiewarn').innerHTML = msg;
}
}
//-->
</script>
</head>
<body onload='init()'>
<h2>Session Demo</h2>
<p>Reload this page. The session ID should not change from one reload to the next</p>
<p><a href='../'>Index</a> | <a href='expire'>Expire</a> | <a href='regen'>Regenerate</a></p>
<table>
<tr><th>Session ID:</th><td>%(sessionid)s<p class='warn'>%(changemsg)s</p></td></tr>
<tr><th>Request Cookie</th><td>%(reqcookie)s</td></tr>
<tr><th>Response Cookie</th><td>%(respcookie)s<p id='respcookiewarn' class='warn'></p></td></tr>
<tr><th>Session Data</th><td>%(sessiondata)s</td></tr>
<tr><th>Server Time</th><td id='stime'>%(servertime)s (Unix time: %(serverunixtime)s)</td></tr>
<tr><th>Browser Time</th><td id='btime'>&nbsp;</td></tr>
<tr><th>Cherrypy Version:</th><td>%(cpversion)s</td></tr>
<tr><th>Python Version:</th><td>%(pyversion)s</td></tr>
</table>
</body></html>
""" # noqa E501
class Root(object):
def page(self):
changemsg = []
if cherrypy.session.id != cherrypy.session.originalid:
if cherrypy.session.originalid is None:
changemsg.append(
'Created new session because no session id was given.')
if cherrypy.session.missing:
changemsg.append(
'Created new session due to missing '
'(expired or malicious) session.')
if cherrypy.session.regenerated:
changemsg.append('Application generated a new session.')
try:
expires = cherrypy.response.cookie['session_id']['expires']
except KeyError:
expires = ''
return page % {
'sessionid': cherrypy.session.id,
'changemsg': '<br>'.join(changemsg),
'respcookie': cherrypy.response.cookie.output(),
'reqcookie': cherrypy.request.cookie.output(),
'sessiondata': list(six.iteritems(cherrypy.session)),
'servertime': (
datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC'
),
'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()),
'cpversion': cherrypy.__version__,
'pyversion': sys.version,
'expires': expires,
}
@cherrypy.expose
def index(self):
# Must modify data or the session will not be saved.
cherrypy.session['color'] = 'green'
return self.page()
@cherrypy.expose
def expire(self):
sessions.expire()
return self.page()
@cherrypy.expose
def regen(self):
cherrypy.session.regenerate()
# Must modify data or the session will not be saved.
cherrypy.session['color'] = 'yellow'
return self.page()
if __name__ == '__main__':
cherrypy.config.update({
# 'environment': 'production',
'log.screen': True,
'tools.sessions.on': True,
})
cherrypy.quickstart(Root())

View file

@ -0,0 +1,5 @@
<html>
<body>
<h1>I couldn't find that thing you were looking for!</h1>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -0,0 +1 @@
Hello, world

View file

@ -0,0 +1 @@
Dummy stylesheet

View file

@ -0,0 +1,38 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ
R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn
da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB
AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj
9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT
enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18
8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8
tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i
0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR
MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB
yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb
8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5
yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ=
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv
MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW
MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy
cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG
A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn
bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx
FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl
cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A
ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M
C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg
KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ
2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ
/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0
MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G
CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME
BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S
8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2
D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T
NluCaWQys3MS
-----END CERTIFICATE-----

View file

@ -0,0 +1,135 @@
# This file is part of CherryPy <http://www.cherrypy.org/>
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
from hashlib import md5
import cherrypy
from cherrypy._cpcompat import ntob
from cherrypy.lib import auth_basic
from cherrypy.test import helper
class BasicAuthTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'This is public.'
class BasicProtected:
@cherrypy.expose
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
class BasicProtected2:
@cherrypy.expose
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
class BasicProtected2_u:
@cherrypy.expose
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
userpassdict = {'xuser': 'xpassword'}
userhashdict = {'xuser': md5(b'xpassword').hexdigest()}
userhashdict_u = {'xюзер': md5(ntob('їжа', 'utf-8')).hexdigest()}
def checkpasshash(realm, user, password):
p = userhashdict.get(user)
return p and p == md5(ntob(password)).hexdigest() or False
def checkpasshash_u(realm, user, password):
p = userhashdict_u.get(user)
return p and p == md5(ntob(password, 'utf-8')).hexdigest() or False
basic_checkpassword_dict = auth_basic.checkpassword_dict(userpassdict)
conf = {
'/basic': {
'tools.auth_basic.on': True,
'tools.auth_basic.realm': 'wonderland',
'tools.auth_basic.checkpassword': basic_checkpassword_dict
},
'/basic2': {
'tools.auth_basic.on': True,
'tools.auth_basic.realm': 'wonderland',
'tools.auth_basic.checkpassword': checkpasshash,
'tools.auth_basic.accept_charset': 'ISO-8859-1',
},
'/basic2_u': {
'tools.auth_basic.on': True,
'tools.auth_basic.realm': 'wonderland',
'tools.auth_basic.checkpassword': checkpasshash_u,
'tools.auth_basic.accept_charset': 'UTF-8',
},
}
root = Root()
root.basic = BasicProtected()
root.basic2 = BasicProtected2()
root.basic2_u = BasicProtected2_u()
cherrypy.tree.mount(root, config=conf)
def testPublic(self):
self.getPage('/')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
self.assertBody('This is public.')
def testBasic(self):
self.getPage('/basic/')
self.assertStatus(401)
self.assertHeader(
'WWW-Authenticate',
'Basic realm="wonderland", charset="UTF-8"'
)
self.getPage('/basic/',
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
self.assertStatus(401)
self.getPage('/basic/',
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
self.assertStatus('200 OK')
self.assertBody("Hello xuser, you've been authorized.")
def testBasic2(self):
self.getPage('/basic2/')
self.assertStatus(401)
self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"')
self.getPage('/basic2/',
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
self.assertStatus(401)
self.getPage('/basic2/',
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
self.assertStatus('200 OK')
self.assertBody("Hello xuser, you've been authorized.")
def testBasic2_u(self):
self.getPage('/basic2_u/')
self.assertStatus(401)
self.assertHeader(
'WWW-Authenticate',
'Basic realm="wonderland", charset="UTF-8"'
)
self.getPage('/basic2_u/',
[('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')])
self.assertStatus(401)
self.getPage('/basic2_u/',
[('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')])
self.assertStatus('200 OK')
self.assertBody("Hello xюзер, you've been authorized.")

View file

@ -0,0 +1,134 @@
# This file is part of CherryPy <http://www.cherrypy.org/>
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
import six
import cherrypy
from cherrypy.lib import auth_digest
from cherrypy._cpcompat import ntob
from cherrypy.test import helper
def _fetch_users():
return {'test': 'test', '☃йюзер': 'їпароль'}
get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(_fetch_users())
class DigestAuthTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'This is public.'
class DigestProtected:
@cherrypy.expose
def index(self, *args, **kwargs):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
conf = {'/digest': {'tools.auth_digest.on': True,
'tools.auth_digest.realm': 'localhost',
'tools.auth_digest.get_ha1': get_ha1,
'tools.auth_digest.key': 'a565c27146791cfb',
'tools.auth_digest.debug': True,
'tools.auth_digest.accept_charset': 'UTF-8'}}
root = Root()
root.digest = DigestProtected()
cherrypy.tree.mount(root, config=conf)
def testPublic(self):
self.getPage('/')
assert self.status == '200 OK'
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
assert self.body == b'This is public.'
def _test_parametric_digest(self, username, realm):
test_uri = '/digest/?@/=%2F%40&%f0%9f%99%88=path'
self.getPage(test_uri)
assert self.status_code == 401
msg = 'Digest authentification scheme was not found'
www_auth_digest = tuple(filter(
lambda kv: kv[0].lower() == 'www-authenticate'
and kv[1].startswith('Digest '),
self.headers,
))
assert len(www_auth_digest) == 1, msg
items = www_auth_digest[0][-1][7:].split(', ')
tokens = {}
for item in items:
key, value = item.split('=')
tokens[key.lower()] = value
assert tokens['realm'] == '"localhost"'
assert tokens['algorithm'] == '"MD5"'
assert tokens['qop'] == '"auth"'
assert tokens['charset'] == '"UTF-8"'
nonce = tokens['nonce'].strip('"')
# Test user agent response with a wrong value for 'realm'
base_auth = ('Digest username="%s", '
'realm="%s", '
'nonce="%s", '
'uri="%s", '
'algorithm=MD5, '
'response="%s", '
'qop=auth, '
'nc=%s, '
'cnonce="1522e61005789929"')
encoded_user = username
if six.PY3:
encoded_user = encoded_user.encode('utf-8')
encoded_user = encoded_user.decode('latin1')
auth_header = base_auth % (
encoded_user, realm, nonce, test_uri,
'11111111111111111111111111111111', '00000001',
)
auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
# calculate the response digest
ha1 = get_ha1(auth.realm, auth.username)
response = auth.request_digest(ha1)
auth_header = base_auth % (
encoded_user, realm, nonce, test_uri,
response, '00000001',
)
self.getPage(test_uri, [('Authorization', auth_header)])
def test_wrong_realm(self):
# send response with correct response digest, but wrong realm
self._test_parametric_digest(username='test', realm='wrong realm')
assert self.status_code == 401
def test_ascii_user(self):
self._test_parametric_digest(username='test', realm='localhost')
assert self.status == '200 OK'
assert self.body == b"Hello test, you've been authorized."
def test_unicode_user(self):
self._test_parametric_digest(username='☃йюзер', realm='localhost')
assert self.status == '200 OK'
assert self.body == ntob(
"Hello ☃йюзер, you've been authorized.", 'utf-8',
)
def test_wrong_scheme(self):
basic_auth = {
'Authorization': 'Basic foo:bar',
}
self.getPage('/digest/', headers=list(basic_auth.items()))
assert self.status_code == 401

View file

@ -0,0 +1,274 @@
import threading
import time
import unittest
from cherrypy.process import wspbus
msg = 'Listener %d on channel %s: %s.'
class PublishSubscribeTests(unittest.TestCase):
def get_listener(self, channel, index):
def listener(arg=None):
self.responses.append(msg % (index, channel, arg))
return listener
def test_builtin_channels(self):
b = wspbus.Bus()
self.responses, expected = [], []
for channel in b.listeners:
for index, priority in enumerate([100, 50, 0, 51]):
b.subscribe(channel,
self.get_listener(channel, index), priority)
for channel in b.listeners:
b.publish(channel)
expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)])
b.publish(channel, arg=79347)
expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)])
self.assertEqual(self.responses, expected)
def test_custom_channels(self):
b = wspbus.Bus()
self.responses, expected = [], []
custom_listeners = ('hugh', 'louis', 'dewey')
for channel in custom_listeners:
for index, priority in enumerate([None, 10, 60, 40]):
b.subscribe(channel,
self.get_listener(channel, index), priority)
for channel in custom_listeners:
b.publish(channel, 'ah so')
expected.extend([msg % (i, channel, 'ah so')
for i in (1, 3, 0, 2)])
b.publish(channel)
expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)])
self.assertEqual(self.responses, expected)
def test_listener_errors(self):
b = wspbus.Bus()
self.responses, expected = [], []
channels = [c for c in b.listeners if c != 'log']
for channel in channels:
b.subscribe(channel, self.get_listener(channel, 1))
# This will break since the lambda takes no args.
b.subscribe(channel, lambda: None, priority=20)
for channel in channels:
self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123)
expected.append(msg % (1, channel, 123))
self.assertEqual(self.responses, expected)
class BusMethodTests(unittest.TestCase):
def log(self, bus):
self._log_entries = []
def logit(msg, level):
self._log_entries.append(msg)
bus.subscribe('log', logit)
def assertLog(self, entries):
self.assertEqual(self._log_entries, entries)
def get_listener(self, channel, index):
def listener(arg=None):
self.responses.append(msg % (index, channel, arg))
return listener
def test_start(self):
b = wspbus.Bus()
self.log(b)
self.responses = []
num = 3
for index in range(num):
b.subscribe('start', self.get_listener('start', index))
b.start()
try:
# The start method MUST call all 'start' listeners.
self.assertEqual(
set(self.responses),
set([msg % (i, 'start', None) for i in range(num)]))
# The start method MUST move the state to STARTED
# (or EXITING, if errors occur)
self.assertEqual(b.state, b.states.STARTED)
# The start method MUST log its states.
self.assertLog(['Bus STARTING', 'Bus STARTED'])
finally:
# Exit so the atexit handler doesn't complain.
b.exit()
def test_stop(self):
b = wspbus.Bus()
self.log(b)
self.responses = []
num = 3
for index in range(num):
b.subscribe('stop', self.get_listener('stop', index))
b.stop()
# The stop method MUST call all 'stop' listeners.
self.assertEqual(set(self.responses),
set([msg % (i, 'stop', None) for i in range(num)]))
# The stop method MUST move the state to STOPPED
self.assertEqual(b.state, b.states.STOPPED)
# The stop method MUST log its states.
self.assertLog(['Bus STOPPING', 'Bus STOPPED'])
def test_graceful(self):
b = wspbus.Bus()
self.log(b)
self.responses = []
num = 3
for index in range(num):
b.subscribe('graceful', self.get_listener('graceful', index))
b.graceful()
# The graceful method MUST call all 'graceful' listeners.
self.assertEqual(
set(self.responses),
set([msg % (i, 'graceful', None) for i in range(num)]))
# The graceful method MUST log its states.
self.assertLog(['Bus graceful'])
def test_exit(self):
b = wspbus.Bus()
self.log(b)
self.responses = []
num = 3
for index in range(num):
b.subscribe('stop', self.get_listener('stop', index))
b.subscribe('exit', self.get_listener('exit', index))
b.exit()
# The exit method MUST call all 'stop' listeners,
# and then all 'exit' listeners.
self.assertEqual(set(self.responses),
set([msg % (i, 'stop', None) for i in range(num)] +
[msg % (i, 'exit', None) for i in range(num)]))
# The exit method MUST move the state to EXITING
self.assertEqual(b.state, b.states.EXITING)
# The exit method MUST log its states.
self.assertLog(
['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED'])
def test_wait(self):
b = wspbus.Bus()
def f(method):
time.sleep(0.2)
getattr(b, method)()
for method, states in [('start', [b.states.STARTED]),
('stop', [b.states.STOPPED]),
('start',
[b.states.STARTING, b.states.STARTED]),
('exit', [b.states.EXITING]),
]:
threading.Thread(target=f, args=(method,)).start()
b.wait(states)
# The wait method MUST wait for the given state(s).
if b.state not in states:
self.fail('State %r not in %r' % (b.state, states))
def test_block(self):
b = wspbus.Bus()
self.log(b)
def f():
time.sleep(0.2)
b.exit()
def g():
time.sleep(0.4)
threading.Thread(target=f).start()
threading.Thread(target=g).start()
threads = [t for t in threading.enumerate() if not t.daemon]
self.assertEqual(len(threads), 3)
b.block()
# The block method MUST wait for the EXITING state.
self.assertEqual(b.state, b.states.EXITING)
# The block method MUST wait for ALL non-main, non-daemon threads to
# finish.
threads = [t for t in threading.enumerate() if not t.daemon]
self.assertEqual(len(threads), 1)
# The last message will mention an indeterminable thread name; ignore
# it
self.assertEqual(self._log_entries[:-1],
['Bus STOPPING', 'Bus STOPPED',
'Bus EXITING', 'Bus EXITED',
'Waiting for child threads to terminate...'])
def test_start_with_callback(self):
b = wspbus.Bus()
self.log(b)
try:
events = []
def f(*args, **kwargs):
events.append(('f', args, kwargs))
def g():
events.append('g')
b.subscribe('start', g)
b.start_with_callback(f, (1, 3, 5), {'foo': 'bar'})
# Give wait() time to run f()
time.sleep(0.2)
# The callback method MUST wait for the STARTED state.
self.assertEqual(b.state, b.states.STARTED)
# The callback method MUST run after all start methods.
self.assertEqual(events, ['g', ('f', (1, 3, 5), {'foo': 'bar'})])
finally:
b.exit()
def test_log(self):
b = wspbus.Bus()
self.log(b)
self.assertLog([])
# Try a normal message.
expected = []
for msg in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']:
b.log(msg)
expected.append(msg)
self.assertLog(expected)
# Try an error message
try:
foo
except NameError:
b.log('You are lost and gone forever', traceback=True)
lastmsg = self._log_entries[-1]
if 'Traceback' not in lastmsg or 'NameError' not in lastmsg:
self.fail('Last log message %r did not contain '
'the expected traceback.' % lastmsg)
else:
self.fail('NameError was not raised as expected.')
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,392 @@
import datetime
from itertools import count
import os
import threading
import time
from six.moves import range
from six.moves import urllib
import pytest
import cherrypy
from cherrypy.lib import httputil
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
gif_bytes = (
b'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;'
)
class CacheTest(helper.CPWebCase):
@staticmethod
def setup_server():
@cherrypy.config(**{'tools.caching.on': True})
class Root:
def __init__(self):
self.counter = 0
self.control_counter = 0
self.longlock = threading.Lock()
@cherrypy.expose
def index(self):
self.counter += 1
msg = 'visit #%s' % self.counter
return msg
@cherrypy.expose
def control(self):
self.control_counter += 1
return 'visit #%s' % self.control_counter
@cherrypy.expose
def a_gif(self):
cherrypy.response.headers[
'Last-Modified'] = httputil.HTTPDate()
return gif_bytes
@cherrypy.expose
def long_process(self, seconds='1'):
try:
self.longlock.acquire()
time.sleep(float(seconds))
finally:
self.longlock.release()
return 'success!'
@cherrypy.expose
def clear_cache(self, path):
cherrypy._cache.store[cherrypy.request.base + path].clear()
@cherrypy.config(**{
'tools.caching.on': True,
'tools.response_headers.on': True,
'tools.response_headers.headers': [
('Vary', 'Our-Varying-Header')
],
})
class VaryHeaderCachingServer(object):
def __init__(self):
self.counter = count(1)
@cherrypy.expose
def index(self):
return 'visit #%s' % next(self.counter)
@cherrypy.config(**{
'tools.expires.on': True,
'tools.expires.secs': 60,
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static',
'tools.staticdir.root': curdir,
})
class UnCached(object):
@cherrypy.expose
@cherrypy.config(**{'tools.expires.secs': 0})
def force(self):
cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
self._cp_config['tools.expires.force'] = True
self._cp_config['tools.expires.secs'] = 0
return 'being forceful'
@cherrypy.expose
def dynamic(self):
cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
cherrypy.response.headers['Cache-Control'] = 'private'
return 'D-d-d-dynamic!'
@cherrypy.expose
def cacheable(self):
cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
return "Hi, I'm cacheable."
@cherrypy.expose
@cherrypy.config(**{'tools.expires.secs': 86400})
def specific(self):
cherrypy.response.headers[
'Etag'] = 'need_this_to_make_me_cacheable'
return 'I am being specific'
class Foo(object):
pass
@cherrypy.expose
@cherrypy.config(**{'tools.expires.secs': Foo()})
def wrongtype(self):
cherrypy.response.headers[
'Etag'] = 'need_this_to_make_me_cacheable'
return 'Woops'
@cherrypy.config(**{
'tools.gzip.mime_types': ['text/*', 'image/*'],
'tools.caching.on': True,
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static',
'tools.staticdir.root': curdir
})
class GzipStaticCache(object):
pass
cherrypy.tree.mount(Root())
cherrypy.tree.mount(UnCached(), '/expires')
cherrypy.tree.mount(VaryHeaderCachingServer(), '/varying_headers')
cherrypy.tree.mount(GzipStaticCache(), '/gzip_static_cache')
cherrypy.config.update({'tools.gzip.on': True})
def testCaching(self):
elapsed = 0.0
for trial in range(10):
self.getPage('/')
# The response should be the same every time,
# except for the Age response header.
self.assertBody('visit #1')
if trial != 0:
age = int(self.assertHeader('Age'))
self.assert_(age >= elapsed)
elapsed = age
# POST, PUT, DELETE should not be cached.
self.getPage('/', method='POST')
self.assertBody('visit #2')
# Because gzip is turned on, the Vary header should always Vary for
# content-encoding
self.assertHeader('Vary', 'Accept-Encoding')
# The previous request should have invalidated the cache,
# so this request will recalc the response.
self.getPage('/', method='GET')
self.assertBody('visit #3')
# ...but this request should get the cached copy.
self.getPage('/', method='GET')
self.assertBody('visit #3')
self.getPage('/', method='DELETE')
self.assertBody('visit #4')
# The previous request should have invalidated the cache,
# so this request will recalc the response.
self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')])
self.assertHeader('Content-Encoding', 'gzip')
self.assertHeader('Vary')
self.assertEqual(
cherrypy.lib.encoding.decompress(self.body), b'visit #5')
# Now check that a second request gets the gzip header and gzipped body
# This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped
# response body was being gzipped a second time.
self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')])
self.assertHeader('Content-Encoding', 'gzip')
self.assertEqual(
cherrypy.lib.encoding.decompress(self.body), b'visit #5')
# Now check that a third request that doesn't accept gzip
# skips the cache (because the 'Vary' header denies it).
self.getPage('/', method='GET')
self.assertNoHeader('Content-Encoding')
self.assertBody('visit #6')
def testVaryHeader(self):
self.getPage('/varying_headers/')
self.assertStatus('200 OK')
self.assertHeaderItemValue('Vary', 'Our-Varying-Header')
self.assertBody('visit #1')
# Now check that different 'Vary'-fields don't evict each other.
# This test creates 2 requests with different 'Our-Varying-Header'
# and then tests if the first one still exists.
self.getPage('/varying_headers/',
headers=[('Our-Varying-Header', 'request 2')])
self.assertStatus('200 OK')
self.assertBody('visit #2')
self.getPage('/varying_headers/',
headers=[('Our-Varying-Header', 'request 2')])
self.assertStatus('200 OK')
self.assertBody('visit #2')
self.getPage('/varying_headers/')
self.assertStatus('200 OK')
self.assertBody('visit #1')
def testExpiresTool(self):
# test setting an expires header
self.getPage('/expires/specific')
self.assertStatus('200 OK')
self.assertHeader('Expires')
# test exceptions for bad time values
self.getPage('/expires/wrongtype')
self.assertStatus(500)
self.assertInBody('TypeError')
# static content should not have "cache prevention" headers
self.getPage('/expires/index.html')
self.assertStatus('200 OK')
self.assertNoHeader('Pragma')
self.assertNoHeader('Cache-Control')
self.assertHeader('Expires')
# dynamic content that sets indicators should not have
# "cache prevention" headers
self.getPage('/expires/cacheable')
self.assertStatus('200 OK')
self.assertNoHeader('Pragma')
self.assertNoHeader('Cache-Control')
self.assertHeader('Expires')
self.getPage('/expires/dynamic')
self.assertBody('D-d-d-dynamic!')
# the Cache-Control header should be untouched
self.assertHeader('Cache-Control', 'private')
self.assertHeader('Expires')
# configure the tool to ignore indicators and replace existing headers
self.getPage('/expires/force')
self.assertStatus('200 OK')
# This also gives us a chance to test 0 expiry with no other headers
self.assertHeader('Pragma', 'no-cache')
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
# static content should now have "cache prevention" headers
self.getPage('/expires/index.html')
self.assertStatus('200 OK')
self.assertHeader('Pragma', 'no-cache')
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
# the cacheable handler should now have "cache prevention" headers
self.getPage('/expires/cacheable')
self.assertStatus('200 OK')
self.assertHeader('Pragma', 'no-cache')
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
self.getPage('/expires/dynamic')
self.assertBody('D-d-d-dynamic!')
# dynamic sets Cache-Control to private but it should be
# overwritten here ...
self.assertHeader('Pragma', 'no-cache')
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
def _assert_resp_len_and_enc_for_gzip(self, uri):
"""
Test that after querying gzipped content it's remains valid in
cache and available non-gzipped as well.
"""
ACCEPT_GZIP_HEADERS = [('Accept-Encoding', 'gzip')]
content_len = None
for _ in range(3):
self.getPage(uri, method='GET', headers=ACCEPT_GZIP_HEADERS)
if content_len is not None:
# all requests should get the same length
self.assertHeader('Content-Length', content_len)
self.assertHeader('Content-Encoding', 'gzip')
content_len = dict(self.headers)['Content-Length']
# check that we can still get non-gzipped version
self.getPage(uri, method='GET')
self.assertNoHeader('Content-Encoding')
# non-gzipped version should have a different content length
self.assertNoHeaderItemValue('Content-Length', content_len)
def testGzipStaticCache(self):
"""Test that cache and gzip tools play well together when both enabled.
Ref GitHub issue #1190.
"""
GZIP_STATIC_CACHE_TMPL = '/gzip_static_cache/{}'
resource_files = ('index.html', 'dirback.jpg')
for f in resource_files:
uri = GZIP_STATIC_CACHE_TMPL.format(f)
self._assert_resp_len_and_enc_for_gzip(uri)
def testLastModified(self):
self.getPage('/a.gif')
self.assertStatus(200)
self.assertBody(gif_bytes)
lm1 = self.assertHeader('Last-Modified')
# this request should get the cached copy.
self.getPage('/a.gif')
self.assertStatus(200)
self.assertBody(gif_bytes)
self.assertHeader('Age')
lm2 = self.assertHeader('Last-Modified')
self.assertEqual(lm1, lm2)
# this request should match the cached copy, but raise 304.
self.getPage('/a.gif', [('If-Modified-Since', lm1)])
self.assertStatus(304)
self.assertNoHeader('Last-Modified')
if not getattr(cherrypy.server, 'using_apache', False):
self.assertHeader('Age')
@pytest.mark.xfail(reason='#1536')
def test_antistampede(self):
SECONDS = 4
slow_url = '/long_process?seconds={SECONDS}'.format(**locals())
# We MUST make an initial synchronous request in order to create the
# AntiStampedeCache object, and populate its selecting_headers,
# before the actual stampede.
self.getPage(slow_url)
self.assertBody('success!')
path = urllib.parse.quote(slow_url, safe='')
self.getPage('/clear_cache?path=' + path)
self.assertStatus(200)
start = datetime.datetime.now()
def run():
self.getPage(slow_url)
# The response should be the same every time
self.assertBody('success!')
ts = [threading.Thread(target=run) for i in range(100)]
for t in ts:
t.start()
for t in ts:
t.join()
finish = datetime.datetime.now()
# Allow for overhead, two seconds for slow hosts
allowance = SECONDS + 2
self.assertEqualDates(start, finish, seconds=allowance)
def test_cache_control(self):
self.getPage('/control')
self.assertBody('visit #1')
self.getPage('/control')
self.assertBody('visit #1')
self.getPage('/control', headers=[('Cache-Control', 'no-cache')])
self.assertBody('visit #2')
self.getPage('/control')
self.assertBody('visit #2')
self.getPage('/control', headers=[('Pragma', 'no-cache')])
self.assertBody('visit #3')
self.getPage('/control')
self.assertBody('visit #3')
time.sleep(1)
self.getPage('/control', headers=[('Cache-Control', 'max-age=0')])
self.assertBody('visit #4')
self.getPage('/control')
self.assertBody('visit #4')

View file

@ -0,0 +1,34 @@
"""Test Python 2/3 compatibility module."""
from __future__ import unicode_literals
import unittest
import pytest
import six
from cherrypy import _cpcompat as compat
class StringTester(unittest.TestCase):
"""Tests for string conversion."""
@pytest.mark.skipif(six.PY3, reason='Only useful on Python 2')
def test_ntob_non_native(self):
"""ntob should raise an Exception on unicode.
(Python 2 only)
See #1132 for discussion.
"""
self.assertRaises(TypeError, compat.ntob, 'fight')
class EscapeTester(unittest.TestCase):
"""Class to test escape_html function from _cpcompat."""
def test_escape_quote(self):
"""test_escape_quote - Verify the output for &<>"' chars."""
self.assertEqual(
"""xx&amp;&lt;&gt;"aa'""",
compat.escape_html("""xx&<>"aa'"""),
)

View file

@ -0,0 +1,303 @@
"""Tests for the CherryPy configuration system."""
import io
import os
import sys
import unittest
import six
import cherrypy
from cherrypy.test import helper
localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
def StringIOFromNative(x):
return io.StringIO(six.text_type(x))
def setup_server():
@cherrypy.config(foo='this', bar='that')
class Root:
def __init__(self):
cherrypy.config.namespaces['db'] = self.db_namespace
def db_namespace(self, k, v):
if k == 'scheme':
self.db = v
@cherrypy.expose(alias=('global_', 'xyz'))
def index(self, key):
return cherrypy.request.config.get(key, 'None')
@cherrypy.expose
def repr(self, key):
return repr(cherrypy.request.config.get(key, None))
@cherrypy.expose
def dbscheme(self):
return self.db
@cherrypy.expose
@cherrypy.config(**{'request.body.attempt_charsets': ['utf-16']})
def plain(self, x):
return x
favicon_ico = cherrypy.tools.staticfile.handler(
filename=os.path.join(localDir, '../favicon.ico'))
@cherrypy.config(foo='this2', baz='that2')
class Foo:
@cherrypy.expose
def index(self, key):
return cherrypy.request.config.get(key, 'None')
nex = index
@cherrypy.expose
@cherrypy.config(**{'response.headers.X-silly': 'sillyval'})
def silly(self):
return 'Hello world'
# Test the expose and config decorators
@cherrypy.config(foo='this3', **{'bax': 'this4'})
@cherrypy.expose
def bar(self, key):
return repr(cherrypy.request.config.get(key, None))
class Another:
@cherrypy.expose
def index(self, key):
return str(cherrypy.request.config.get(key, 'None'))
def raw_namespace(key, value):
if key == 'input.map':
handler = cherrypy.request.handler
def wrapper():
params = cherrypy.request.params
for name, coercer in list(value.items()):
try:
params[name] = coercer(params[name])
except KeyError:
pass
return handler()
cherrypy.request.handler = wrapper
elif key == 'output':
handler = cherrypy.request.handler
def wrapper():
# 'value' is a type (like int or str).
return value(handler())
cherrypy.request.handler = wrapper
@cherrypy.config(**{'raw.output': repr})
class Raw:
@cherrypy.expose
@cherrypy.config(**{'raw.input.map': {'num': int}})
def incr(self, num):
return num + 1
if not six.PY3:
thing3 = "thing3: unicode('test', errors='ignore')"
else:
thing3 = ''
ioconf = StringIOFromNative("""
[/]
neg: -1234
filename: os.path.join(sys.prefix, "hello.py")
thing1: cherrypy.lib.httputil.response_codes[404]
thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2
%s
complex: 3+2j
mul: 6*3
ones: "11"
twos: "22"
stradd: %%(ones)s + %%(twos)s + "33"
[/favicon.ico]
tools.staticfile.filename = %r
""" % (thing3, os.path.join(localDir, 'static/dirback.jpg')))
root = Root()
root.foo = Foo()
root.raw = Raw()
app = cherrypy.tree.mount(root, config=ioconf)
app.request_class.namespaces['raw'] = raw_namespace
cherrypy.tree.mount(Another(), '/another')
cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove',
'db.scheme': r'sqlite///memory',
})
# Client-side code #
class ConfigTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def testConfig(self):
tests = [
('/', 'nex', 'None'),
('/', 'foo', 'this'),
('/', 'bar', 'that'),
('/xyz', 'foo', 'this'),
('/foo/', 'foo', 'this2'),
('/foo/', 'bar', 'that'),
('/foo/', 'bax', 'None'),
('/foo/bar', 'baz', "'that2'"),
('/foo/nex', 'baz', 'that2'),
# If 'foo' == 'this', then the mount point '/another' leaks into
# '/'.
('/another/', 'foo', 'None'),
]
for path, key, expected in tests:
self.getPage(path + '?key=' + key)
self.assertBody(expected)
expectedconf = {
# From CP defaults
'tools.log_headers.on': False,
'tools.log_tracebacks.on': True,
'request.show_tracebacks': True,
'log.screen': False,
'environment': 'test_suite',
'engine.autoreload.on': False,
# From global config
'luxuryyacht': 'throatwobblermangrove',
# From Root._cp_config
'bar': 'that',
# From Foo._cp_config
'baz': 'that2',
# From Foo.bar._cp_config
'foo': 'this3',
'bax': 'this4',
}
for key, expected in expectedconf.items():
self.getPage('/foo/bar?key=' + key)
self.assertBody(repr(expected))
def testUnrepr(self):
self.getPage('/repr?key=neg')
self.assertBody('-1234')
self.getPage('/repr?key=filename')
self.assertBody(repr(os.path.join(sys.prefix, 'hello.py')))
self.getPage('/repr?key=thing1')
self.assertBody(repr(cherrypy.lib.httputil.response_codes[404]))
if not getattr(cherrypy.server, 'using_apache', False):
# The object ID's won't match up when using Apache, since the
# server and client are running in different processes.
self.getPage('/repr?key=thing2')
from cherrypy.tutorial import thing2
self.assertBody(repr(thing2))
if not six.PY3:
self.getPage('/repr?key=thing3')
self.assertBody(repr(six.text_type('test')))
self.getPage('/repr?key=complex')
self.assertBody('(3+2j)')
self.getPage('/repr?key=mul')
self.assertBody('18')
self.getPage('/repr?key=stradd')
self.assertBody(repr('112233'))
def testRespNamespaces(self):
self.getPage('/foo/silly')
self.assertHeader('X-silly', 'sillyval')
self.assertBody('Hello world')
def testCustomNamespaces(self):
self.getPage('/raw/incr?num=12')
self.assertBody('13')
self.getPage('/dbscheme')
self.assertBody(r'sqlite///memory')
def testHandlerToolConfigOverride(self):
# Assert that config overrides tool constructor args. Above, we set
# the favicon in the page handler to be '../favicon.ico',
# but then overrode it in config to be './static/dirback.jpg'.
self.getPage('/favicon.ico')
self.assertBody(open(os.path.join(localDir, 'static/dirback.jpg'),
'rb').read())
def test_request_body_namespace(self):
self.getPage('/plain', method='POST', headers=[
('Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', '13')],
body=b'\xff\xfex\x00=\xff\xfea\x00b\x00c\x00')
self.assertBody('abc')
class VariableSubstitutionTests(unittest.TestCase):
setup_server = staticmethod(setup_server)
def test_config(self):
from textwrap import dedent
# variable substitution with [DEFAULT]
conf = dedent("""
[DEFAULT]
dir = "/some/dir"
my.dir = %(dir)s + "/sub"
[my]
my.dir = %(dir)s + "/my/dir"
my.dir2 = %(my.dir)s + '/dir2'
""")
fp = StringIOFromNative(conf)
cherrypy.config.update(fp)
self.assertEqual(cherrypy.config['my']['my.dir'], '/some/dir/my/dir')
self.assertEqual(cherrypy.config['my']
['my.dir2'], '/some/dir/my/dir/dir2')
class CallablesInConfigTest(unittest.TestCase):
setup_server = staticmethod(setup_server)
def test_call_with_literal_dict(self):
from textwrap import dedent
conf = dedent("""
[my]
value = dict(**{'foo': 'bar'})
""")
fp = StringIOFromNative(conf)
cherrypy.config.update(fp)
self.assertEqual(cherrypy.config['my']['value'], {'foo': 'bar'})
def test_call_with_kwargs(self):
from textwrap import dedent
conf = dedent("""
[my]
value = dict(foo="buzz", **cherrypy._test_dict)
""")
test_dict = {
'foo': 'bar',
'bar': 'foo',
'fizz': 'buzz'
}
cherrypy._test_dict = test_dict
fp = StringIOFromNative(conf)
cherrypy.config.update(fp)
test_dict['foo'] = 'buzz'
self.assertEqual(cherrypy.config['my']['value']['foo'], 'buzz')
self.assertEqual(cherrypy.config['my']['value'], test_dict)
del cherrypy._test_dict

View file

@ -0,0 +1,126 @@
"""Tests for the CherryPy configuration system."""
import os
import cherrypy
from cherrypy.test import helper
localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
# Client-side code #
class ServerConfigTests(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return cherrypy.request.wsgi_environ['SERVER_PORT']
@cherrypy.expose
def upload(self, file):
return 'Size: %s' % len(file.file.read())
@cherrypy.expose
@cherrypy.config(**{'request.body.maxbytes': 100})
def tinyupload(self):
return cherrypy.request.body.read()
cherrypy.tree.mount(Root())
cherrypy.config.update({
'server.socket_host': '0.0.0.0',
'server.socket_port': 9876,
'server.max_request_body_size': 200,
'server.max_request_header_size': 500,
'server.socket_timeout': 0.5,
# Test explicit server.instance
'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer',
'server.2.socket_port': 9877,
# Test non-numeric <servername>
# Also test default server.instance = builtin server
'server.yetanother.socket_port': 9878,
})
PORT = 9876
def testBasicConfig(self):
self.getPage('/')
self.assertBody(str(self.PORT))
def testAdditionalServers(self):
if self.scheme == 'https':
return self.skip('not available under ssl')
self.PORT = 9877
self.getPage('/')
self.assertBody(str(self.PORT))
self.PORT = 9878
self.getPage('/')
self.assertBody(str(self.PORT))
def testMaxRequestSizePerHandler(self):
if getattr(cherrypy.server, 'using_apache', False):
return self.skip('skipped due to known Apache differences... ')
self.getPage('/tinyupload', method='POST',
headers=[('Content-Type', 'text/plain'),
('Content-Length', '100')],
body='x' * 100)
self.assertStatus(200)
self.assertBody('x' * 100)
self.getPage('/tinyupload', method='POST',
headers=[('Content-Type', 'text/plain'),
('Content-Length', '101')],
body='x' * 101)
self.assertStatus(413)
def testMaxRequestSize(self):
if getattr(cherrypy.server, 'using_apache', False):
return self.skip('skipped due to known Apache differences... ')
for size in (500, 5000, 50000):
self.getPage('/', headers=[('From', 'x' * 500)])
self.assertStatus(413)
# Test for https://github.com/cherrypy/cherrypy/issues/421
# (Incorrect border condition in readline of SizeCheckWrapper).
# This hangs in rev 891 and earlier.
lines256 = 'x' * 248
self.getPage('/',
headers=[('Host', '%s:%s' % (self.HOST, self.PORT)),
('From', lines256)])
# Test upload
cd = (
'Content-Disposition: form-data; '
'name="file"; '
'filename="hello.txt"'
)
body = '\r\n'.join([
'--x',
cd,
'Content-Type: text/plain',
'',
'%s',
'--x--'])
partlen = 200 - len(body)
b = body % ('x' * partlen)
h = [('Content-type', 'multipart/form-data; boundary=x'),
('Content-Length', '%s' % len(b))]
self.getPage('/upload', h, 'POST', b)
self.assertBody('Size: %d' % partlen)
b = body % ('x' * 200)
h = [('Content-type', 'multipart/form-data; boundary=x'),
('Content-Length', '%s' % len(b))]
self.getPage('/upload', h, 'POST', b)
self.assertStatus(413)

View file

@ -0,0 +1,873 @@
"""Tests for TCP connection handling, including proper and timely close."""
import errno
import socket
import sys
import time
import six
from six.moves import urllib
from six.moves.http_client import BadStatusLine, HTTPConnection, NotConnected
import pytest
from cheroot.test import webtest
import cherrypy
from cherrypy._cpcompat import HTTPSConnection, ntob, tonative
from cherrypy.test import helper
timeout = 1
pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
def setup_server():
def raise500():
raise cherrypy.HTTPError(500)
class Root:
@cherrypy.expose
def index(self):
return pov
page1 = index
page2 = index
page3 = index
@cherrypy.expose
def hello(self):
return 'Hello, world!'
@cherrypy.expose
def timeout(self, t):
return str(cherrypy.server.httpserver.timeout)
@cherrypy.expose
@cherrypy.config(**{'response.stream': True})
def stream(self, set_cl=False):
if set_cl:
cherrypy.response.headers['Content-Length'] = 10
def content():
for x in range(10):
yield str(x)
return content()
@cherrypy.expose
def error(self, code=500):
raise cherrypy.HTTPError(code)
@cherrypy.expose
def upload(self):
if not cherrypy.request.method == 'POST':
raise AssertionError("'POST' != request.method %r" %
cherrypy.request.method)
return "thanks for '%s'" % cherrypy.request.body.read()
@cherrypy.expose
def custom(self, response_code):
cherrypy.response.status = response_code
return 'Code = %s' % response_code
@cherrypy.expose
@cherrypy.config(**{'hooks.on_start_resource': raise500})
def err_before_read(self):
return 'ok'
@cherrypy.expose
def one_megabyte_of_a(self):
return ['a' * 1024] * 1024
@cherrypy.expose
# Turn off the encoding tool so it doens't collapse
# our response body and reclaculate the Content-Length.
@cherrypy.config(**{'tools.encode.on': False})
def custom_cl(self, body, cl):
cherrypy.response.headers['Content-Length'] = cl
if not isinstance(body, list):
body = [body]
newbody = []
for chunk in body:
if isinstance(chunk, six.text_type):
chunk = chunk.encode('ISO-8859-1')
newbody.append(chunk)
return newbody
cherrypy.tree.mount(Root())
cherrypy.config.update({
'server.max_request_body_size': 1001,
'server.socket_timeout': timeout,
})
class ConnectionCloseTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_HTTP11(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
self.persistent = True
# Make the first request and assert there's no "Connection: close".
self.getPage('/')
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertNoHeader('Connection')
# Make another request on the same connection.
self.getPage('/page1')
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertNoHeader('Connection')
# Test client-side close.
self.getPage('/page2', headers=[('Connection', 'close')])
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertHeader('Connection', 'close')
# Make another request on the same connection, which should error.
self.assertRaises(NotConnected, self.getPage, '/')
def test_Streaming_no_len(self):
try:
self._streaming(set_cl=False)
finally:
try:
self.HTTP_CONN.close()
except (TypeError, AttributeError):
pass
def test_Streaming_with_len(self):
try:
self._streaming(set_cl=True)
finally:
try:
self.HTTP_CONN.close()
except (TypeError, AttributeError):
pass
def _streaming(self, set_cl):
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.PROTOCOL = 'HTTP/1.1'
self.persistent = True
# Make the first request and assert there's no "Connection: close".
self.getPage('/')
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertNoHeader('Connection')
# Make another, streamed request on the same connection.
if set_cl:
# When a Content-Length is provided, the content should stream
# without closing the connection.
self.getPage('/stream?set_cl=Yes')
self.assertHeader('Content-Length')
self.assertNoHeader('Connection', 'close')
self.assertNoHeader('Transfer-Encoding')
self.assertStatus('200 OK')
self.assertBody('0123456789')
else:
# When no Content-Length response header is provided,
# streamed output will either close the connection, or use
# chunked encoding, to determine transfer-length.
self.getPage('/stream')
self.assertNoHeader('Content-Length')
self.assertStatus('200 OK')
self.assertBody('0123456789')
chunked_response = False
for k, v in self.headers:
if k.lower() == 'transfer-encoding':
if str(v) == 'chunked':
chunked_response = True
if chunked_response:
self.assertNoHeader('Connection', 'close')
else:
self.assertHeader('Connection', 'close')
# Make another request on the same connection, which should
# error.
self.assertRaises(NotConnected, self.getPage, '/')
# Try HEAD. See
# https://github.com/cherrypy/cherrypy/issues/864.
self.getPage('/stream', method='HEAD')
self.assertStatus('200 OK')
self.assertBody('')
self.assertNoHeader('Transfer-Encoding')
else:
self.PROTOCOL = 'HTTP/1.0'
self.persistent = True
# Make the first request and assert Keep-Alive.
self.getPage('/', headers=[('Connection', 'Keep-Alive')])
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertHeader('Connection', 'Keep-Alive')
# Make another, streamed request on the same connection.
if set_cl:
# When a Content-Length is provided, the content should
# stream without closing the connection.
self.getPage('/stream?set_cl=Yes',
headers=[('Connection', 'Keep-Alive')])
self.assertHeader('Content-Length')
self.assertHeader('Connection', 'Keep-Alive')
self.assertNoHeader('Transfer-Encoding')
self.assertStatus('200 OK')
self.assertBody('0123456789')
else:
# When a Content-Length is not provided,
# the server should close the connection.
self.getPage('/stream', headers=[('Connection', 'Keep-Alive')])
self.assertStatus('200 OK')
self.assertBody('0123456789')
self.assertNoHeader('Content-Length')
self.assertNoHeader('Connection', 'Keep-Alive')
self.assertNoHeader('Transfer-Encoding')
# Make another request on the same connection, which should
# error.
self.assertRaises(NotConnected, self.getPage, '/')
def test_HTTP10_KeepAlive(self):
self.PROTOCOL = 'HTTP/1.0'
if self.scheme == 'https':
self.HTTP_CONN = HTTPSConnection
else:
self.HTTP_CONN = HTTPConnection
# Test a normal HTTP/1.0 request.
self.getPage('/page2')
self.assertStatus('200 OK')
self.assertBody(pov)
# Apache, for example, may emit a Connection header even for HTTP/1.0
# self.assertNoHeader("Connection")
# Test a keep-alive HTTP/1.0 request.
self.persistent = True
self.getPage('/page3', headers=[('Connection', 'Keep-Alive')])
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertHeader('Connection', 'Keep-Alive')
# Remove the keep-alive header again.
self.getPage('/page3')
self.assertStatus('200 OK')
self.assertBody(pov)
# Apache, for example, may emit a Connection header even for HTTP/1.0
# self.assertNoHeader("Connection")
class PipelineTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_HTTP11_Timeout(self):
# If we timeout without sending any data,
# the server will close the conn with a 408.
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Connect but send nothing.
self.persistent = True
conn = self.HTTP_CONN
conn.auto_open = False
conn.connect()
# Wait for our socket timeout
time.sleep(timeout * 2)
# The request should have returned 408 already.
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 408)
conn.close()
# Connect but send half the headers only.
self.persistent = True
conn = self.HTTP_CONN
conn.auto_open = False
conn.connect()
conn.send(b'GET /hello HTTP/1.1')
conn.send(('Host: %s' % self.HOST).encode('ascii'))
# Wait for our socket timeout
time.sleep(timeout * 2)
# The conn should have already sent 408.
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 408)
conn.close()
def test_HTTP11_Timeout_after_request(self):
# If we timeout after at least one request has succeeded,
# the server will close the conn without 408.
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Make an initial request
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(str(timeout))
# Make a second request on the same socket
conn._output(b'GET /hello HTTP/1.1')
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
conn._send_output()
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody('Hello, world!')
# Wait for our socket timeout
time.sleep(timeout * 2)
# Make another request on the same socket, which should error
conn._output(b'GET /hello HTTP/1.1')
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
conn._send_output()
response = conn.response_class(conn.sock, method='GET')
try:
response.begin()
except Exception:
if not isinstance(sys.exc_info()[1],
(socket.error, BadStatusLine)):
self.fail("Writing to timed out socket didn't fail"
' as it should have: %s' % sys.exc_info()[1])
else:
if response.status != 408:
self.fail("Writing to timed out socket didn't fail"
' as it should have: %s' %
response.read())
conn.close()
# Make another request on a new socket, which should work
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('GET', '/', skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(pov)
# Make another request on the same socket,
# but timeout on the headers
conn.send(b'GET /hello HTTP/1.1')
# Wait for our socket timeout
time.sleep(timeout * 2)
response = conn.response_class(conn.sock, method='GET')
try:
response.begin()
except Exception:
if not isinstance(sys.exc_info()[1],
(socket.error, BadStatusLine)):
self.fail("Writing to timed out socket didn't fail"
' as it should have: %s' % sys.exc_info()[1])
else:
self.fail("Writing to timed out socket didn't fail"
' as it should have: %s' %
response.read())
conn.close()
# Retry the request on a new connection, which should work
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('GET', '/', skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(pov)
conn.close()
def test_HTTP11_pipelining(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Test pipelining. httplib doesn't support this directly.
self.persistent = True
conn = self.HTTP_CONN
# Put request 1
conn.putrequest('GET', '/hello', skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
for trial in range(5):
# Put next request
conn._output(b'GET /hello HTTP/1.1')
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
conn._send_output()
# Retrieve previous response
response = conn.response_class(conn.sock, method='GET')
# there is a bug in python3 regarding the buffering of
# ``conn.sock``. Until that bug get's fixed we will
# monkey patch the ``response`` instance.
# https://bugs.python.org/issue23377
if six.PY3:
response.fp = conn.sock.makefile('rb', 0)
response.begin()
body = response.read(13)
self.assertEqual(response.status, 200)
self.assertEqual(body, b'Hello, world!')
# Retrieve final response
response = conn.response_class(conn.sock, method='GET')
response.begin()
body = response.read()
self.assertEqual(response.status, 200)
self.assertEqual(body, b'Hello, world!')
conn.close()
def test_100_Continue(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
self.persistent = True
conn = self.HTTP_CONN
# Try a page without an Expect request header first.
# Note that httplib's response.begin automatically ignores
# 100 Continue responses, so we must manually check for it.
try:
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Type', 'text/plain')
conn.putheader('Content-Length', '4')
conn.endheaders()
conn.send(ntob("d'oh"))
response = conn.response_class(conn.sock, method='POST')
version, status, reason = response._read_status()
self.assertNotEqual(status, 100)
finally:
conn.close()
# Now try a page with an Expect header...
try:
conn.connect()
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Type', 'text/plain')
conn.putheader('Content-Length', '17')
conn.putheader('Expect', '100-continue')
conn.endheaders()
response = conn.response_class(conn.sock, method='POST')
# ...assert and then skip the 100 response
version, status, reason = response._read_status()
self.assertEqual(status, 100)
while True:
line = response.fp.readline().strip()
if line:
self.fail(
'100 Continue should not output any headers. Got %r' %
line)
else:
break
# ...send the body
body = b'I am a small file'
conn.send(body)
# ...get the final response
response.begin()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(200)
self.assertBody("thanks for '%s'" % body)
finally:
conn.close()
class ConnectionTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_readall_or_close(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
if self.scheme == 'https':
self.HTTP_CONN = HTTPSConnection
else:
self.HTTP_CONN = HTTPConnection
# Test a max of 0 (the default) and then reset to what it was above.
old_max = cherrypy.server.max_request_body_size
for new_max in (0, old_max):
cherrypy.server.max_request_body_size = new_max
self.persistent = True
conn = self.HTTP_CONN
# Get a POST page with an error
conn.putrequest('POST', '/err_before_read', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Type', 'text/plain')
conn.putheader('Content-Length', '1000')
conn.putheader('Expect', '100-continue')
conn.endheaders()
response = conn.response_class(conn.sock, method='POST')
# ...assert and then skip the 100 response
version, status, reason = response._read_status()
self.assertEqual(status, 100)
while True:
skip = response.fp.readline().strip()
if not skip:
break
# ...send the body
conn.send(ntob('x' * 1000))
# ...get the final response
response.begin()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(500)
# Now try a working page with an Expect header...
conn._output(b'POST /upload HTTP/1.1')
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
conn._output(b'Content-Type: text/plain')
conn._output(b'Content-Length: 17')
conn._output(b'Expect: 100-continue')
conn._send_output()
response = conn.response_class(conn.sock, method='POST')
# ...assert and then skip the 100 response
version, status, reason = response._read_status()
self.assertEqual(status, 100)
while True:
skip = response.fp.readline().strip()
if not skip:
break
# ...send the body
body = b'I am a small file'
conn.send(body)
# ...get the final response
response.begin()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(200)
self.assertBody("thanks for '%s'" % body)
conn.close()
def test_No_Message_Body(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Set our HTTP_CONN to an instance so it persists between requests.
self.persistent = True
# Make the first request and assert there's no "Connection: close".
self.getPage('/')
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertNoHeader('Connection')
# Make a 204 request on the same connection.
self.getPage('/custom/204')
self.assertStatus(204)
self.assertNoHeader('Content-Length')
self.assertBody('')
self.assertNoHeader('Connection')
# Make a 304 request on the same connection.
self.getPage('/custom/304')
self.assertStatus(304)
self.assertNoHeader('Content-Length')
self.assertBody('')
self.assertNoHeader('Connection')
def test_Chunked_Encoding(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
if (hasattr(self, 'harness') and
'modpython' in self.harness.__class__.__name__.lower()):
# mod_python forbids chunked encoding
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Set our HTTP_CONN to an instance so it persists between requests.
self.persistent = True
conn = self.HTTP_CONN
# Try a normal chunked request (with extensions)
body = ntob('8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n'
'Content-Type: application/json\r\n'
'\r\n')
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Transfer-Encoding', 'chunked')
conn.putheader('Trailer', 'Content-Type')
# Note that this is somewhat malformed:
# we shouldn't be sending Content-Length.
# RFC 2616 says the server should ignore it.
conn.putheader('Content-Length', '3')
conn.endheaders()
conn.send(body)
response = conn.getresponse()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus('200 OK')
self.assertBody("thanks for '%s'" % b'xx\r\nxxxxyyyyy')
# Try a chunked request that exceeds server.max_request_body_size.
# Note that the delimiters and trailer are included.
body = ntob('3e3\r\n' + ('x' * 995) + '\r\n0\r\n\r\n')
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Transfer-Encoding', 'chunked')
conn.putheader('Content-Type', 'text/plain')
# Chunked requests don't need a content-length
# # conn.putheader("Content-Length", len(body))
conn.endheaders()
conn.send(body)
response = conn.getresponse()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(413)
conn.close()
def test_Content_Length_in(self):
# Try a non-chunked request where Content-Length exceeds
# server.max_request_body_size. Assert error before body send.
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Type', 'text/plain')
conn.putheader('Content-Length', '9999')
conn.endheaders()
response = conn.getresponse()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(413)
self.assertBody('The entity sent with the request exceeds '
'the maximum allowed bytes.')
conn.close()
def test_Content_Length_out_preheaders(self):
# Try a non-chunked response where Content-Length is less than
# the actual bytes in the response body.
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('GET', '/custom_cl?body=I+have+too+many+bytes&cl=5',
skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.getresponse()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(500)
self.assertBody(
'The requested resource returned more bytes than the '
'declared Content-Length.')
conn.close()
def test_Content_Length_out_postheaders(self):
# Try a non-chunked response where Content-Length is less than
# the actual bytes in the response body.
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest(
'GET', '/custom_cl?body=I+too&body=+have+too+many&cl=5',
skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.getresponse()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(200)
self.assertBody('I too')
conn.close()
def test_598(self):
tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/'
url = tmpl.format(
scheme=self.scheme,
host=self.HOST,
port=self.PORT,
)
remote_data_conn = urllib.request.urlopen(url)
buf = remote_data_conn.read(512)
time.sleep(timeout * 0.6)
remaining = (1024 * 1024) - 512
while remaining:
data = remote_data_conn.read(remaining)
if not data:
break
else:
buf += data
remaining -= len(data)
self.assertEqual(len(buf), 1024 * 1024)
self.assertEqual(buf, ntob('a' * 1024 * 1024))
self.assertEqual(remaining, 0)
remote_data_conn.close()
def setup_upload_server():
class Root:
@cherrypy.expose
def upload(self):
if not cherrypy.request.method == 'POST':
raise AssertionError("'POST' != request.method %r" %
cherrypy.request.method)
return "thanks for '%s'" % tonative(cherrypy.request.body.read())
cherrypy.tree.mount(Root())
cherrypy.config.update({
'server.max_request_body_size': 1001,
'server.socket_timeout': 10,
'server.accepted_queue_size': 5,
'server.accepted_queue_timeout': 0.1,
})
reset_names = 'ECONNRESET', 'WSAECONNRESET'
socket_reset_errors = [
getattr(errno, name)
for name in reset_names
if hasattr(errno, name)
]
'reset error numbers available on this platform'
socket_reset_errors += [
# Python 3.5 raises an http.client.RemoteDisconnected
# with this message
'Remote end closed connection without response',
]
class LimitedRequestQueueTests(helper.CPWebCase):
setup_server = staticmethod(setup_upload_server)
@pytest.mark.xfail(reason='#1535')
def test_queue_full(self):
conns = []
overflow_conn = None
try:
# Make 15 initial requests and leave them open, which should use
# all of wsgiserver's WorkerThreads and fill its Queue.
for i in range(15):
conn = self.HTTP_CONN(self.HOST, self.PORT)
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Type', 'text/plain')
conn.putheader('Content-Length', '4')
conn.endheaders()
conns.append(conn)
# Now try a 16th conn, which should be closed by the
# server immediately.
overflow_conn = self.HTTP_CONN(self.HOST, self.PORT)
# Manually connect since httplib won't let us set a timeout
for res in socket.getaddrinfo(self.HOST, self.PORT, 0,
socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
overflow_conn.sock = socket.socket(af, socktype, proto)
overflow_conn.sock.settimeout(5)
overflow_conn.sock.connect(sa)
break
overflow_conn.putrequest('GET', '/', skip_host=True)
overflow_conn.putheader('Host', self.HOST)
overflow_conn.endheaders()
response = overflow_conn.response_class(
overflow_conn.sock,
method='GET',
)
try:
response.begin()
except socket.error as exc:
if exc.args[0] in socket_reset_errors:
pass # Expected.
else:
tmpl = (
'Overflow conn did not get RST. '
'Got {exc.args!r} instead'
)
raise AssertionError(tmpl.format(**locals()))
except BadStatusLine:
# This is a special case in OS X. Linux and Windows will
# RST correctly.
assert sys.platform == 'darwin'
else:
raise AssertionError('Overflow conn did not get RST ')
finally:
for conn in conns:
conn.send(b'done')
response = conn.response_class(conn.sock, method='POST')
response.begin()
self.body = response.read()
self.assertBody("thanks for 'done'")
self.assertEqual(response.status, 200)
conn.close()
if overflow_conn:
overflow_conn.close()
class BadRequestTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_No_CRLF(self):
self.persistent = True
conn = self.HTTP_CONN
conn.send(b'GET /hello HTTP/1.1\n\n')
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.body = response.read()
self.assertBody('HTTP requires CRLF terminators')
conn.close()
conn.connect()
conn.send(b'GET /hello HTTP/1.1\r\n\n')
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.body = response.read()
self.assertBody('HTTP requires CRLF terminators')
conn.close()

View file

@ -0,0 +1,823 @@
# coding: utf-8
"""Basic tests for the CherryPy core: request handling."""
import os
import sys
import types
import six
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy import _cptools, tools
from cherrypy.lib import httputil, static
from cherrypy.test._test_decorators import ExposeExamples
from cherrypy.test import helper
localDir = os.path.dirname(__file__)
favicon_path = os.path.join(os.getcwd(), localDir, '../favicon.ico')
# Client-side code #
class CoreRequestHandlingTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'hello'
favicon_ico = tools.staticfile.handler(filename=favicon_path)
@cherrypy.expose
def defct(self, newct):
newct = 'text/%s' % newct
cherrypy.config.update({'tools.response_headers.on': True,
'tools.response_headers.headers':
[('Content-Type', newct)]})
@cherrypy.expose
def baseurl(self, path_info, relative=None):
return cherrypy.url(path_info, relative=bool(relative))
root = Root()
root.expose_dec = ExposeExamples()
class TestType(type):
"""Metaclass which automatically exposes all functions in each
subclass, and adds an instance of the subclass as an attribute
of root.
"""
def __init__(cls, name, bases, dct):
type.__init__(cls, name, bases, dct)
for value in six.itervalues(dct):
if isinstance(value, types.FunctionType):
value.exposed = True
setattr(root, name.lower(), cls())
Test = TestType('Test', (object, ), {})
@cherrypy.config(**{'tools.trailing_slash.on': False})
class URL(Test):
def index(self, path_info, relative=None):
if relative != 'server':
relative = bool(relative)
return cherrypy.url(path_info, relative=relative)
def leaf(self, path_info, relative=None):
if relative != 'server':
relative = bool(relative)
return cherrypy.url(path_info, relative=relative)
def qs(self, qs):
return cherrypy.url(qs=qs)
def log_status():
Status.statuses.append(cherrypy.response.status)
cherrypy.tools.log_status = cherrypy.Tool(
'on_end_resource', log_status)
class Status(Test):
def index(self):
return 'normal'
def blank(self):
cherrypy.response.status = ''
# According to RFC 2616, new status codes are OK as long as they
# are between 100 and 599.
# Here is an illegal code...
def illegal(self):
cherrypy.response.status = 781
return 'oops'
# ...and here is an unknown but legal code.
def unknown(self):
cherrypy.response.status = '431 My custom error'
return 'funky'
# Non-numeric code
def bad(self):
cherrypy.response.status = 'error'
return 'bad news'
statuses = []
@cherrypy.config(**{'tools.log_status.on': True})
def on_end_resource_stage(self):
return repr(self.statuses)
class Redirect(Test):
@cherrypy.config(**{
'tools.err_redirect.on': True,
'tools.err_redirect.url': '/errpage',
'tools.err_redirect.internal': False,
})
class Error:
@cherrypy.expose
def index(self):
raise NameError('redirect_test')
error = Error()
def index(self):
return 'child'
def custom(self, url, code):
raise cherrypy.HTTPRedirect(url, code)
@cherrypy.config(**{'tools.trailing_slash.extra': True})
def by_code(self, code):
raise cherrypy.HTTPRedirect('somewhere%20else', code)
def nomodify(self):
raise cherrypy.HTTPRedirect('', 304)
def proxy(self):
raise cherrypy.HTTPRedirect('proxy', 305)
def stringify(self):
return str(cherrypy.HTTPRedirect('/'))
def fragment(self, frag):
raise cherrypy.HTTPRedirect('/some/url#%s' % frag)
def url_with_quote(self):
raise cherrypy.HTTPRedirect("/some\"url/that'we/want")
def url_with_xss(self):
raise cherrypy.HTTPRedirect(
"/some<script>alert(1);</script>url/that'we/want")
def url_with_unicode(self):
raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8'))
def login_redir():
if not getattr(cherrypy.request, 'login', None):
raise cherrypy.InternalRedirect('/internalredirect/login')
tools.login_redir = _cptools.Tool('before_handler', login_redir)
def redir_custom():
raise cherrypy.InternalRedirect('/internalredirect/custom_err')
class InternalRedirect(Test):
def index(self):
raise cherrypy.InternalRedirect('/')
@cherrypy.expose
@cherrypy.config(**{'hooks.before_error_response': redir_custom})
def choke(self):
return 3 / 0
def relative(self, a, b):
raise cherrypy.InternalRedirect('cousin?t=6')
def cousin(self, t):
assert cherrypy.request.prev.closed
return cherrypy.request.prev.query_string
def petshop(self, user_id):
if user_id == 'parrot':
# Trade it for a slug when redirecting
raise cherrypy.InternalRedirect(
'/image/getImagesByUser?user_id=slug')
elif user_id == 'terrier':
# Trade it for a fish when redirecting
raise cherrypy.InternalRedirect(
'/image/getImagesByUser?user_id=fish')
else:
# This should pass the user_id through to getImagesByUser
raise cherrypy.InternalRedirect(
'/image/getImagesByUser?user_id=%s' % str(user_id))
# We support Python 2.3, but the @-deco syntax would look like
# this:
# @tools.login_redir()
def secure(self):
return 'Welcome!'
secure = tools.login_redir()(secure)
# Since calling the tool returns the same function you pass in,
# you could skip binding the return value, and just write:
# tools.login_redir()(secure)
def login(self):
return 'Please log in'
def custom_err(self):
return 'Something went horribly wrong.'
@cherrypy.config(**{'hooks.before_request_body': redir_custom})
def early_ir(self, arg):
return 'whatever'
class Image(Test):
def getImagesByUser(self, user_id):
return '0 images for %s' % user_id
class Flatten(Test):
def as_string(self):
return 'content'
def as_list(self):
return ['con', 'tent']
def as_yield(self):
yield b'content'
@cherrypy.config(**{'tools.flatten.on': True})
def as_dblyield(self):
yield self.as_yield()
def as_refyield(self):
for chunk in self.as_yield():
yield chunk
class Ranges(Test):
def get_ranges(self, bytes):
return repr(httputil.get_ranges('bytes=%s' % bytes, 8))
def slice_file(self):
path = os.path.join(os.getcwd(), os.path.dirname(__file__))
return static.serve_file(
os.path.join(path, 'static/index.html'))
class Cookies(Test):
def single(self, name):
cookie = cherrypy.request.cookie[name]
# Python2's SimpleCookie.__setitem__ won't take unicode keys.
cherrypy.response.cookie[str(name)] = cookie.value
def multiple(self, names):
list(map(self.single, names))
def append_headers(header_list, debug=False):
if debug:
cherrypy.log(
'Extending response headers with %s' % repr(header_list),
'TOOLS.APPEND_HEADERS')
cherrypy.serving.response.header_list.extend(header_list)
cherrypy.tools.append_headers = cherrypy.Tool(
'on_end_resource', append_headers)
class MultiHeader(Test):
def header_list(self):
pass
header_list = cherrypy.tools.append_headers(header_list=[
(b'WWW-Authenticate', b'Negotiate'),
(b'WWW-Authenticate', b'Basic realm="foo"'),
])(header_list)
def commas(self):
cherrypy.response.headers[
'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"'
cherrypy.tree.mount(root)
def testStatus(self):
self.getPage('/status/')
self.assertBody('normal')
self.assertStatus(200)
self.getPage('/status/blank')
self.assertBody('')
self.assertStatus(200)
self.getPage('/status/illegal')
self.assertStatus(500)
msg = 'Illegal response status from server (781 is out of range).'
self.assertErrorPage(500, msg)
if not getattr(cherrypy.server, 'using_apache', False):
self.getPage('/status/unknown')
self.assertBody('funky')
self.assertStatus(431)
self.getPage('/status/bad')
self.assertStatus(500)
msg = "Illegal response status from server ('error' is non-numeric)."
self.assertErrorPage(500, msg)
def test_on_end_resource_status(self):
self.getPage('/status/on_end_resource_stage')
self.assertBody('[]')
self.getPage('/status/on_end_resource_stage')
self.assertBody(repr(['200 OK']))
def testSlashes(self):
# Test that requests for index methods without a trailing slash
# get redirected to the same URI path with a trailing slash.
# Make sure GET params are preserved.
self.getPage('/redirect?id=3')
self.assertStatus(301)
self.assertMatchesBody(
'<a href=([\'"])%s/redirect/[?]id=3\\1>'
'%s/redirect/[?]id=3</a>' % (self.base(), self.base())
)
if self.prefix():
# Corner case: the "trailing slash" redirect could be tricky if
# we're using a virtual root and the URI is "/vroot" (no slash).
self.getPage('')
self.assertStatus(301)
self.assertMatchesBody("<a href=(['\"])%s/\\1>%s/</a>" %
(self.base(), self.base()))
# Test that requests for NON-index methods WITH a trailing slash
# get redirected to the same URI path WITHOUT a trailing slash.
# Make sure GET params are preserved.
self.getPage('/redirect/by_code/?code=307')
self.assertStatus(301)
self.assertMatchesBody(
"<a href=(['\"])%s/redirect/by_code[?]code=307\\1>"
'%s/redirect/by_code[?]code=307</a>'
% (self.base(), self.base())
)
# If the trailing_slash tool is off, CP should just continue
# as if the slashes were correct. But it needs some help
# inside cherrypy.url to form correct output.
self.getPage('/url?path_info=page1')
self.assertBody('%s/url/page1' % self.base())
self.getPage('/url/leaf/?path_info=page1')
self.assertBody('%s/url/page1' % self.base())
def testRedirect(self):
self.getPage('/redirect/')
self.assertBody('child')
self.assertStatus(200)
self.getPage('/redirect/by_code?code=300')
self.assertMatchesBody(
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
self.assertStatus(300)
self.getPage('/redirect/by_code?code=301')
self.assertMatchesBody(
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
self.assertStatus(301)
self.getPage('/redirect/by_code?code=302')
self.assertMatchesBody(
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
self.assertStatus(302)
self.getPage('/redirect/by_code?code=303')
self.assertMatchesBody(
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
self.assertStatus(303)
self.getPage('/redirect/by_code?code=307')
self.assertMatchesBody(
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
self.assertStatus(307)
self.getPage('/redirect/nomodify')
self.assertBody('')
self.assertStatus(304)
self.getPage('/redirect/proxy')
self.assertBody('')
self.assertStatus(305)
# HTTPRedirect on error
self.getPage('/redirect/error/')
self.assertStatus(('302 Found', '303 See Other'))
self.assertInBody('/errpage')
# Make sure str(HTTPRedirect()) works.
self.getPage('/redirect/stringify', protocol='HTTP/1.0')
self.assertStatus(200)
self.assertBody("(['%s/'], 302)" % self.base())
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.getPage('/redirect/stringify', protocol='HTTP/1.1')
self.assertStatus(200)
self.assertBody("(['%s/'], 303)" % self.base())
# check that #fragments are handled properly
# http://skrb.org/ietf/http_errata.html#location-fragments
frag = 'foo'
self.getPage('/redirect/fragment/%s' % frag)
self.assertMatchesBody(
r"<a href=(['\"])(.*)\/some\/url\#%s\1>\2\/some\/url\#%s</a>" % (
frag, frag))
loc = self.assertHeader('Location')
assert loc.endswith('#%s' % frag)
self.assertStatus(('302 Found', '303 See Other'))
# check injection protection
# See https://github.com/cherrypy/cherrypy/issues/1003
self.getPage(
'/redirect/custom?'
'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval')
self.assertStatus(303)
loc = self.assertHeader('Location')
assert 'Set-Cookie' in loc
self.assertNoHeader('Set-Cookie')
def assertValidXHTML():
from xml.etree import ElementTree
try:
ElementTree.fromstring(
'<html><body>%s</body></html>' % self.body,
)
except ElementTree.ParseError:
self._handlewebError(
'automatically generated redirect did not '
'generate well-formed html',
)
# check redirects to URLs generated valid HTML - we check this
# by seeing if it appears as valid XHTML.
self.getPage('/redirect/by_code?code=303')
self.assertStatus(303)
assertValidXHTML()
# do the same with a url containing quote characters.
self.getPage('/redirect/url_with_quote')
self.assertStatus(303)
assertValidXHTML()
def test_redirect_with_xss(self):
"""A redirect to a URL with HTML injected should result
in page contents escaped."""
self.getPage('/redirect/url_with_xss')
self.assertStatus(303)
assert b'<script>' not in self.body
assert b'&lt;script&gt;' in self.body
def test_redirect_with_unicode(self):
"""
A redirect to a URL with Unicode should return a Location
header containing that Unicode URL.
"""
# test disabled due to #1440
return
self.getPage('/redirect/url_with_unicode')
self.assertStatus(303)
loc = self.assertHeader('Location')
assert ntou('тест', encoding='utf-8') in loc
def test_InternalRedirect(self):
# InternalRedirect
self.getPage('/internalredirect/')
self.assertBody('hello')
self.assertStatus(200)
# Test passthrough
self.getPage(
'/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film')
self.assertBody('0 images for Sir-not-appearing-in-this-film')
self.assertStatus(200)
# Test args
self.getPage('/internalredirect/petshop?user_id=parrot')
self.assertBody('0 images for slug')
self.assertStatus(200)
# Test POST
self.getPage('/internalredirect/petshop', method='POST',
body='user_id=terrier')
self.assertBody('0 images for fish')
self.assertStatus(200)
# Test ir before body read
self.getPage('/internalredirect/early_ir', method='POST',
body='arg=aha!')
self.assertBody('Something went horribly wrong.')
self.assertStatus(200)
self.getPage('/internalredirect/secure')
self.assertBody('Please log in')
self.assertStatus(200)
# Relative path in InternalRedirect.
# Also tests request.prev.
self.getPage('/internalredirect/relative?a=3&b=5')
self.assertBody('a=3&b=5')
self.assertStatus(200)
# InternalRedirect on error
self.getPage('/internalredirect/choke')
self.assertStatus(200)
self.assertBody('Something went horribly wrong.')
def testFlatten(self):
for url in ['/flatten/as_string', '/flatten/as_list',
'/flatten/as_yield', '/flatten/as_dblyield',
'/flatten/as_refyield']:
self.getPage(url)
self.assertBody('content')
def testRanges(self):
self.getPage('/ranges/get_ranges?bytes=3-6')
self.assertBody('[(3, 7)]')
# Test multiple ranges and a suffix-byte-range-spec, for good measure.
self.getPage('/ranges/get_ranges?bytes=2-4,-1')
self.assertBody('[(2, 5), (7, 8)]')
# Test a suffix-byte-range longer than the content
# length. Note that in this test, the content length
# is 8 bytes.
self.getPage('/ranges/get_ranges?bytes=-100')
self.assertBody('[(0, 8)]')
# Get a partial file.
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')])
self.assertStatus(206)
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
self.assertHeader('Content-Range', 'bytes 2-5/14')
self.assertBody('llo,')
# What happens with overlapping ranges (and out of order, too)?
self.getPage('/ranges/slice_file', [('Range', 'bytes=4-6,2-5')])
self.assertStatus(206)
ct = self.assertHeader('Content-Type')
expected_type = 'multipart/byteranges; boundary='
self.assert_(ct.startswith(expected_type))
boundary = ct[len(expected_type):]
expected_body = ('\r\n--%s\r\n'
'Content-type: text/html\r\n'
'Content-range: bytes 4-6/14\r\n'
'\r\n'
'o, \r\n'
'--%s\r\n'
'Content-type: text/html\r\n'
'Content-range: bytes 2-5/14\r\n'
'\r\n'
'llo,\r\n'
'--%s--\r\n' % (boundary, boundary, boundary))
self.assertBody(expected_body)
self.assertHeader('Content-Length')
# Test "416 Requested Range Not Satisfiable"
self.getPage('/ranges/slice_file', [('Range', 'bytes=2300-2900')])
self.assertStatus(416)
# "When this status code is returned for a byte-range request,
# the response SHOULD include a Content-Range entity-header
# field specifying the current length of the selected resource"
self.assertHeader('Content-Range', 'bytes */14')
elif cherrypy.server.protocol_version == 'HTTP/1.0':
# Test Range behavior with HTTP/1.0 request
self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')])
self.assertStatus(200)
self.assertBody('Hello, world\r\n')
def testFavicon(self):
# favicon.ico is served by staticfile.
icofilename = os.path.join(localDir, '../favicon.ico')
icofile = open(icofilename, 'rb')
data = icofile.read()
icofile.close()
self.getPage('/favicon.ico')
self.assertBody(data)
def skip_if_bad_cookies(self):
"""
cookies module fails to reject invalid cookies
https://github.com/cherrypy/cherrypy/issues/1405
"""
cookies = sys.modules.get('http.cookies')
_is_legal_key = getattr(cookies, '_is_legal_key', lambda x: False)
if not _is_legal_key(','):
return
issue = 'http://bugs.python.org/issue26302'
tmpl = 'Broken cookies module ({issue})'
self.skip(tmpl.format(**locals()))
def testCookies(self):
self.skip_if_bad_cookies()
self.getPage('/cookies/single?name=First',
[('Cookie', 'First=Dinsdale;')])
self.assertHeader('Set-Cookie', 'First=Dinsdale')
self.getPage('/cookies/multiple?names=First&names=Last',
[('Cookie', 'First=Dinsdale; Last=Piranha;'),
])
self.assertHeader('Set-Cookie', 'First=Dinsdale')
self.assertHeader('Set-Cookie', 'Last=Piranha')
self.getPage('/cookies/single?name=Something-With%2CComma',
[('Cookie', 'Something-With,Comma=some-value')])
self.assertStatus(400)
def testDefaultContentType(self):
self.getPage('/')
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
self.getPage('/defct/plain')
self.getPage('/')
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
self.getPage('/defct/html')
def test_multiple_headers(self):
self.getPage('/multiheader/header_list')
self.assertEqual(
[(k, v) for k, v in self.headers if k == 'WWW-Authenticate'],
[('WWW-Authenticate', 'Negotiate'),
('WWW-Authenticate', 'Basic realm="foo"'),
])
self.getPage('/multiheader/commas')
self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"')
def test_cherrypy_url(self):
# Input relative to current
self.getPage('/url/leaf?path_info=page1')
self.assertBody('%s/url/page1' % self.base())
self.getPage('/url/?path_info=page1')
self.assertBody('%s/url/page1' % self.base())
# Other host header
host = 'www.mydomain.example'
self.getPage('/url/leaf?path_info=page1',
headers=[('Host', host)])
self.assertBody('%s://%s/url/page1' % (self.scheme, host))
# Input is 'absolute'; that is, relative to script_name
self.getPage('/url/leaf?path_info=/page1')
self.assertBody('%s/page1' % self.base())
self.getPage('/url/?path_info=/page1')
self.assertBody('%s/page1' % self.base())
# Single dots
self.getPage('/url/leaf?path_info=./page1')
self.assertBody('%s/url/page1' % self.base())
self.getPage('/url/leaf?path_info=other/./page1')
self.assertBody('%s/url/other/page1' % self.base())
self.getPage('/url/?path_info=/other/./page1')
self.assertBody('%s/other/page1' % self.base())
self.getPage('/url/?path_info=/other/././././page1')
self.assertBody('%s/other/page1' % self.base())
# Double dots
self.getPage('/url/leaf?path_info=../page1')
self.assertBody('%s/page1' % self.base())
self.getPage('/url/leaf?path_info=other/../page1')
self.assertBody('%s/url/page1' % self.base())
self.getPage('/url/leaf?path_info=/other/../page1')
self.assertBody('%s/page1' % self.base())
self.getPage('/url/leaf?path_info=/other/../../../page1')
self.assertBody('%s/page1' % self.base())
self.getPage('/url/leaf?path_info=/other/../../../../../page1')
self.assertBody('%s/page1' % self.base())
# qs param is not normalized as a path
self.getPage('/url/qs?qs=/other')
self.assertBody('%s/url/qs?/other' % self.base())
self.getPage('/url/qs?qs=/other/../page1')
self.assertBody('%s/url/qs?/other/../page1' % self.base())
self.getPage('/url/qs?qs=../page1')
self.assertBody('%s/url/qs?../page1' % self.base())
self.getPage('/url/qs?qs=../../page1')
self.assertBody('%s/url/qs?../../page1' % self.base())
# Output relative to current path or script_name
self.getPage('/url/?path_info=page1&relative=True')
self.assertBody('page1')
self.getPage('/url/leaf?path_info=/page1&relative=True')
self.assertBody('../page1')
self.getPage('/url/leaf?path_info=page1&relative=True')
self.assertBody('page1')
self.getPage('/url/leaf?path_info=leaf/page1&relative=True')
self.assertBody('leaf/page1')
self.getPage('/url/leaf?path_info=../page1&relative=True')
self.assertBody('../page1')
self.getPage('/url/?path_info=other/../page1&relative=True')
self.assertBody('page1')
# Output relative to /
self.getPage('/baseurl?path_info=ab&relative=True')
self.assertBody('ab')
# Output relative to /
self.getPage('/baseurl?path_info=/ab&relative=True')
self.assertBody('ab')
# absolute-path references ("server-relative")
# Input relative to current
self.getPage('/url/leaf?path_info=page1&relative=server')
self.assertBody('/url/page1')
self.getPage('/url/?path_info=page1&relative=server')
self.assertBody('/url/page1')
# Input is 'absolute'; that is, relative to script_name
self.getPage('/url/leaf?path_info=/page1&relative=server')
self.assertBody('/page1')
self.getPage('/url/?path_info=/page1&relative=server')
self.assertBody('/page1')
def test_expose_decorator(self):
# Test @expose
self.getPage('/expose_dec/no_call')
self.assertStatus(200)
self.assertBody('Mr E. R. Bradshaw')
# Test @expose()
self.getPage('/expose_dec/call_empty')
self.assertStatus(200)
self.assertBody('Mrs. B.J. Smegma')
# Test @expose("alias")
self.getPage('/expose_dec/call_alias')
self.assertStatus(200)
self.assertBody('Mr Nesbitt')
# Does the original name work?
self.getPage('/expose_dec/nesbitt')
self.assertStatus(200)
self.assertBody('Mr Nesbitt')
# Test @expose(["alias1", "alias2"])
self.getPage('/expose_dec/alias1')
self.assertStatus(200)
self.assertBody('Mr Ken Andrews')
self.getPage('/expose_dec/alias2')
self.assertStatus(200)
self.assertBody('Mr Ken Andrews')
# Does the original name work?
self.getPage('/expose_dec/andrews')
self.assertStatus(200)
self.assertBody('Mr Ken Andrews')
# Test @expose(alias="alias")
self.getPage('/expose_dec/alias3')
self.assertStatus(200)
self.assertBody('Mr. and Mrs. Watson')
class ErrorTests(helper.CPWebCase):
@staticmethod
def setup_server():
def break_header():
# Add a header after finalize that is invalid
cherrypy.serving.response.header_list.append((2, 3))
cherrypy.tools.break_header = cherrypy.Tool(
'on_end_resource', break_header)
class Root:
@cherrypy.expose
def index(self):
return 'hello'
@cherrypy.config(**{'tools.break_header.on': True})
def start_response_error(self):
return 'salud!'
@cherrypy.expose
def stat(self, path):
with cherrypy.HTTPError.handle(OSError, 404):
os.stat(path)
root = Root()
cherrypy.tree.mount(root)
def test_start_response_error(self):
self.getPage('/start_response_error')
self.assertStatus(500)
self.assertInBody(
'TypeError: response.header_list key 2 is not a byte string.')
def test_contextmanager(self):
self.getPage('/stat/missing')
self.assertStatus(404)
body_text = self.body.decode('utf-8')
assert (
'No such file or directory' in body_text or
'cannot find the file specified' in body_text
)
class TestBinding:
def test_bind_ephemeral_port(self):
"""
A server configured to bind to port 0 will bind to an ephemeral
port and indicate that port number on startup.
"""
cherrypy.config.reset()
bind_ephemeral_conf = {
'server.socket_port': 0,
}
cherrypy.config.update(bind_ephemeral_conf)
cherrypy.engine.start()
assert cherrypy.server.bound_addr != cherrypy.server.bind_addr
_host, port = cherrypy.server.bound_addr
assert port > 0
cherrypy.engine.stop()
assert cherrypy.server.bind_addr == cherrypy.server.bound_addr

View file

@ -0,0 +1,424 @@
import six
import cherrypy
from cherrypy.test import helper
script_names = ['', '/foo', '/users/fred/blog', '/corp/blog']
def setup_server():
class SubSubRoot:
@cherrypy.expose
def index(self):
return 'SubSubRoot index'
@cherrypy.expose
def default(self, *args):
return 'SubSubRoot default'
@cherrypy.expose
def handler(self):
return 'SubSubRoot handler'
@cherrypy.expose
def dispatch(self):
return 'SubSubRoot dispatch'
subsubnodes = {
'1': SubSubRoot(),
'2': SubSubRoot(),
}
class SubRoot:
@cherrypy.expose
def index(self):
return 'SubRoot index'
@cherrypy.expose
def default(self, *args):
return 'SubRoot %s' % (args,)
@cherrypy.expose
def handler(self):
return 'SubRoot handler'
def _cp_dispatch(self, vpath):
return subsubnodes.get(vpath[0], None)
subnodes = {
'1': SubRoot(),
'2': SubRoot(),
}
class Root:
@cherrypy.expose
def index(self):
return 'index'
@cherrypy.expose
def default(self, *args):
return 'default %s' % (args,)
@cherrypy.expose
def handler(self):
return 'handler'
def _cp_dispatch(self, vpath):
return subnodes.get(vpath[0])
# -------------------------------------------------------------------------
# DynamicNodeAndMethodDispatcher example.
# This example exposes a fairly naive HTTP api
class User(object):
def __init__(self, id, name):
self.id = id
self.name = name
def __unicode__(self):
return six.text_type(self.name)
def __str__(self):
return str(self.name)
user_lookup = {
1: User(1, 'foo'),
2: User(2, 'bar'),
}
def make_user(name, id=None):
if not id:
id = max(*list(user_lookup.keys())) + 1
user_lookup[id] = User(id, name)
return id
@cherrypy.expose
class UserContainerNode(object):
def POST(self, name):
"""
Allow the creation of a new Object
"""
return 'POST %d' % make_user(name)
def GET(self):
return six.text_type(sorted(user_lookup.keys()))
def dynamic_dispatch(self, vpath):
try:
id = int(vpath[0])
except (ValueError, IndexError):
return None
return UserInstanceNode(id)
@cherrypy.expose
class UserInstanceNode(object):
def __init__(self, id):
self.id = id
self.user = user_lookup.get(id, None)
# For all but PUT methods there MUST be a valid user identified
# by self.id
if not self.user and cherrypy.request.method != 'PUT':
raise cherrypy.HTTPError(404)
def GET(self, *args, **kwargs):
"""
Return the appropriate representation of the instance.
"""
return six.text_type(self.user)
def POST(self, name):
"""
Update the fields of the user instance.
"""
self.user.name = name
return 'POST %d' % self.user.id
def PUT(self, name):
"""
Create a new user with the specified id, or edit it if it already
exists
"""
if self.user:
# Edit the current user
self.user.name = name
return 'PUT %d' % self.user.id
else:
# Make a new user with said attributes.
return 'PUT %d' % make_user(name, self.id)
def DELETE(self):
"""
Delete the user specified at the id.
"""
id = self.user.id
del user_lookup[self.user.id]
del self.user
return 'DELETE %d' % id
class ABHandler:
class CustomDispatch:
@cherrypy.expose
def index(self, a, b):
return 'custom'
def _cp_dispatch(self, vpath):
"""Make sure that if we don't pop anything from vpath,
processing still works.
"""
return self.CustomDispatch()
@cherrypy.expose
def index(self, a, b=None):
body = ['a:' + str(a)]
if b is not None:
body.append(',b:' + str(b))
return ''.join(body)
@cherrypy.expose
def delete(self, a, b):
return 'deleting ' + str(a) + ' and ' + str(b)
class IndexOnly:
def _cp_dispatch(self, vpath):
"""Make sure that popping ALL of vpath still shows the index
handler.
"""
while vpath:
vpath.pop()
return self
@cherrypy.expose
def index(self):
return 'IndexOnly index'
class DecoratedPopArgs:
"""Test _cp_dispatch with @cherrypy.popargs."""
@cherrypy.expose
def index(self):
return 'no params'
@cherrypy.expose
def hi(self):
return "hi was not interpreted as 'a' param"
DecoratedPopArgs = cherrypy.popargs(
'a', 'b', handler=ABHandler())(DecoratedPopArgs)
class NonDecoratedPopArgs:
"""Test _cp_dispatch = cherrypy.popargs()"""
_cp_dispatch = cherrypy.popargs('a')
@cherrypy.expose
def index(self, a):
return 'index: ' + str(a)
class ParameterizedHandler:
"""Special handler created for each request"""
def __init__(self, a):
self.a = a
@cherrypy.expose
def index(self):
if 'a' in cherrypy.request.params:
raise Exception(
'Parameterized handler argument ended up in '
'request.params')
return self.a
class ParameterizedPopArgs:
"""Test cherrypy.popargs() with a function call handler"""
ParameterizedPopArgs = cherrypy.popargs(
'a', handler=ParameterizedHandler)(ParameterizedPopArgs)
Root.decorated = DecoratedPopArgs()
Root.undecorated = NonDecoratedPopArgs()
Root.index_only = IndexOnly()
Root.parameter_test = ParameterizedPopArgs()
Root.users = UserContainerNode()
md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch')
for url in script_names:
conf = {
'/': {
'user': (url or '/').split('/')[-2],
},
'/users': {
'request.dispatch': md
},
}
cherrypy.tree.mount(Root(), url, conf)
class DynamicObjectMappingTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def testObjectMapping(self):
for url in script_names:
self.script_name = url
self.getPage('/')
self.assertBody('index')
self.getPage('/handler')
self.assertBody('handler')
# Dynamic dispatch will succeed here for the subnodes
# so the subroot gets called
self.getPage('/1/')
self.assertBody('SubRoot index')
self.getPage('/2/')
self.assertBody('SubRoot index')
self.getPage('/1/handler')
self.assertBody('SubRoot handler')
self.getPage('/2/handler')
self.assertBody('SubRoot handler')
# Dynamic dispatch will fail here for the subnodes
# so the default gets called
self.getPage('/asdf/')
self.assertBody("default ('asdf',)")
self.getPage('/asdf/asdf')
self.assertBody("default ('asdf', 'asdf')")
self.getPage('/asdf/handler')
self.assertBody("default ('asdf', 'handler')")
# Dynamic dispatch will succeed here for the subsubnodes
# so the subsubroot gets called
self.getPage('/1/1/')
self.assertBody('SubSubRoot index')
self.getPage('/2/2/')
self.assertBody('SubSubRoot index')
self.getPage('/1/1/handler')
self.assertBody('SubSubRoot handler')
self.getPage('/2/2/handler')
self.assertBody('SubSubRoot handler')
self.getPage('/2/2/dispatch')
self.assertBody('SubSubRoot dispatch')
# The exposed dispatch will not be called as a dispatch
# method.
self.getPage('/2/2/foo/foo')
self.assertBody('SubSubRoot default')
# Dynamic dispatch will fail here for the subsubnodes
# so the SubRoot gets called
self.getPage('/1/asdf/')
self.assertBody("SubRoot ('asdf',)")
self.getPage('/1/asdf/asdf')
self.assertBody("SubRoot ('asdf', 'asdf')")
self.getPage('/1/asdf/handler')
self.assertBody("SubRoot ('asdf', 'handler')")
def testMethodDispatch(self):
# GET acts like a container
self.getPage('/users')
self.assertBody('[1, 2]')
self.assertHeader('Allow', 'GET, HEAD, POST')
# POST to the container URI allows creation
self.getPage('/users', method='POST', body='name=baz')
self.assertBody('POST 3')
self.assertHeader('Allow', 'GET, HEAD, POST')
# POST to a specific instanct URI results in a 404
# as the resource does not exit.
self.getPage('/users/5', method='POST', body='name=baz')
self.assertStatus(404)
# PUT to a specific instanct URI results in creation
self.getPage('/users/5', method='PUT', body='name=boris')
self.assertBody('PUT 5')
self.assertHeader('Allow', 'DELETE, GET, HEAD, POST, PUT')
# GET acts like a container
self.getPage('/users')
self.assertBody('[1, 2, 3, 5]')
self.assertHeader('Allow', 'GET, HEAD, POST')
test_cases = (
(1, 'foo', 'fooupdated', 'DELETE, GET, HEAD, POST, PUT'),
(2, 'bar', 'barupdated', 'DELETE, GET, HEAD, POST, PUT'),
(3, 'baz', 'bazupdated', 'DELETE, GET, HEAD, POST, PUT'),
(5, 'boris', 'borisupdated', 'DELETE, GET, HEAD, POST, PUT'),
)
for id, name, updatedname, headers in test_cases:
self.getPage('/users/%d' % id)
self.assertBody(name)
self.assertHeader('Allow', headers)
# Make sure POSTs update already existings resources
self.getPage('/users/%d' %
id, method='POST', body='name=%s' % updatedname)
self.assertBody('POST %d' % id)
self.assertHeader('Allow', headers)
# Make sure PUTs Update already existing resources.
self.getPage('/users/%d' %
id, method='PUT', body='name=%s' % updatedname)
self.assertBody('PUT %d' % id)
self.assertHeader('Allow', headers)
# Make sure DELETES Remove already existing resources.
self.getPage('/users/%d' % id, method='DELETE')
self.assertBody('DELETE %d' % id)
self.assertHeader('Allow', headers)
# GET acts like a container
self.getPage('/users')
self.assertBody('[]')
self.assertHeader('Allow', 'GET, HEAD, POST')
def testVpathDispatch(self):
self.getPage('/decorated/')
self.assertBody('no params')
self.getPage('/decorated/hi')
self.assertBody("hi was not interpreted as 'a' param")
self.getPage('/decorated/yo/')
self.assertBody('a:yo')
self.getPage('/decorated/yo/there/')
self.assertBody('a:yo,b:there')
self.getPage('/decorated/yo/there/delete')
self.assertBody('deleting yo and there')
self.getPage('/decorated/yo/there/handled_by_dispatch/')
self.assertBody('custom')
self.getPage('/undecorated/blah/')
self.assertBody('index: blah')
self.getPage('/index_only/a/b/c/d/e/f/g/')
self.assertBody('IndexOnly index')
self.getPage('/parameter_test/argument2/')
self.assertBody('argument2')

View file

@ -0,0 +1,433 @@
# coding: utf-8
import gzip
import io
from unittest import mock
from six.moves.http_client import IncompleteRead
from six.moves.urllib.parse import quote as url_quote
import cherrypy
from cherrypy._cpcompat import ntob, ntou
from cherrypy.test import helper
europoundUnicode = ntou('£', encoding='utf-8')
sing = ntou('毛泽东: Sing, Little Birdie?', encoding='utf-8')
sing8 = sing.encode('utf-8')
sing16 = sing.encode('utf-16')
class EncodingTests(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self, param):
assert param == europoundUnicode, '%r != %r' % (
param, europoundUnicode)
yield europoundUnicode
@cherrypy.expose
def mao_zedong(self):
return sing
@cherrypy.expose
@cherrypy.config(**{'tools.encode.encoding': 'utf-8'})
def utf8(self):
return sing8
@cherrypy.expose
def cookies_and_headers(self):
# if the headers have non-ascii characters and a cookie has
# any part which is unicode (even ascii), the response
# should not fail.
cherrypy.response.cookie['candy'] = 'bar'
cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org'
cherrypy.response.headers[
'Some-Header'] = 'My d\xc3\xb6g has fleas'
cherrypy.response.headers[
'Bytes-Header'] = b'Bytes given header'
return 'Any content'
@cherrypy.expose
def reqparams(self, *args, **kwargs):
return b', '.join(
[': '.join((k, v)).encode('utf8')
for k, v in sorted(cherrypy.request.params.items())]
)
@cherrypy.expose
@cherrypy.config(**{
'tools.encode.text_only': False,
'tools.encode.add_charset': True,
})
def nontext(self, *args, **kwargs):
cherrypy.response.headers[
'Content-Type'] = 'application/binary'
return '\x00\x01\x02\x03'
class GZIP:
@cherrypy.expose
def index(self):
yield 'Hello, world'
@cherrypy.expose
# Turn encoding off so the gzip tool is the one doing the collapse.
@cherrypy.config(**{'tools.encode.on': False})
def noshow(self):
# Test for ticket #147, where yield showed no exceptions
# (content-encoding was still gzip even though traceback
# wasn't zipped).
raise IndexError()
yield 'Here be dragons'
@cherrypy.expose
@cherrypy.config(**{'response.stream': True})
def noshow_stream(self):
# Test for ticket #147, where yield showed no exceptions
# (content-encoding was still gzip even though traceback
# wasn't zipped).
raise IndexError()
yield 'Here be dragons'
class Decode:
@cherrypy.expose
@cherrypy.config(**{
'tools.decode.on': True,
'tools.decode.default_encoding': ['utf-16'],
})
def extra_charset(self, *args, **kwargs):
return ', '.join([': '.join((k, v))
for k, v in cherrypy.request.params.items()])
@cherrypy.expose
@cherrypy.config(**{
'tools.decode.on': True,
'tools.decode.encoding': 'utf-16',
})
def force_charset(self, *args, **kwargs):
return ', '.join([': '.join((k, v))
for k, v in cherrypy.request.params.items()])
root = Root()
root.gzip = GZIP()
root.decode = Decode()
cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}})
def test_query_string_decoding(self):
URI_TMPL = '/reqparams?q={q}'
europoundUtf8_2_bytes = europoundUnicode.encode('utf-8')
europoundUtf8_2nd_byte = europoundUtf8_2_bytes[1:2]
# Encoded utf8 query strings MUST be parsed correctly.
# Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX
self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2_bytes)))
# The return value will be encoded as utf8.
self.assertBody(b'q: ' + europoundUtf8_2_bytes)
# Query strings that are incorrectly encoded MUST raise 404.
# Here, q is the second byte of POUND SIGN U+A3 encoded in utf8
# and then %HEX
# TODO: check whether this shouldn't raise 400 Bad Request instead
self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2nd_byte)))
self.assertStatus(404)
self.assertErrorPage(
404,
'The given query string could not be processed. Query '
"strings for this resource must be encoded with 'utf8'.")
def test_urlencoded_decoding(self):
# Test the decoding of an application/x-www-form-urlencoded entity.
europoundUtf8 = europoundUnicode.encode('utf-8')
body = b'param=' + europoundUtf8
self.getPage('/',
method='POST',
headers=[
('Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(europoundUtf8)
# Encoded utf8 entities MUST be parsed and decoded correctly.
# Here, q is the POUND SIGN U+00A3 encoded in utf8
body = b'q=\xc2\xa3'
self.getPage('/reqparams', method='POST',
headers=[(
'Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'q: \xc2\xa3')
# ...and in utf16, which is not in the default attempt_charsets list:
body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
self.getPage('/reqparams',
method='POST',
headers=[
('Content-Type',
'application/x-www-form-urlencoded;charset=utf-16'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'q: \xc2\xa3')
# Entities that are incorrectly encoded MUST raise 400.
# Here, q is the POUND SIGN U+00A3 encoded in utf16, but
# the Content-Type incorrectly labels it utf-8.
body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
self.getPage('/reqparams',
method='POST',
headers=[
('Content-Type',
'application/x-www-form-urlencoded;charset=utf-8'),
('Content-Length', str(len(body))),
],
body=body),
self.assertStatus(400)
self.assertErrorPage(
400,
'The request entity could not be decoded. The following charsets '
"were attempted: ['utf-8']")
def test_decode_tool(self):
# An extra charset should be tried first, and succeed if it matches.
# Here, we add utf-16 as a charset and pass a utf-16 body.
body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
self.getPage('/decode/extra_charset', method='POST',
headers=[(
'Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'q: \xc2\xa3')
# An extra charset should be tried first, and continue to other default
# charsets if it doesn't match.
# Here, we add utf-16 as a charset but still pass a utf-8 body.
body = b'q=\xc2\xa3'
self.getPage('/decode/extra_charset', method='POST',
headers=[(
'Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'q: \xc2\xa3')
# An extra charset should error if force is True and it doesn't match.
# Here, we force utf-16 as a charset but still pass a utf-8 body.
body = b'q=\xc2\xa3'
self.getPage('/decode/force_charset', method='POST',
headers=[(
'Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', str(len(body))),
],
body=body),
self.assertErrorPage(
400,
'The request entity could not be decoded. The following charsets '
"were attempted: ['utf-16']")
def test_multipart_decoding(self):
# Test the decoding of a multipart entity when the charset (utf16) is
# explicitly given.
body = ntob('\r\n'.join([
'--X',
'Content-Type: text/plain;charset=utf-16',
'Content-Disposition: form-data; name="text"',
'',
'\xff\xfea\x00b\x00\x1c c\x00',
'--X',
'Content-Type: text/plain;charset=utf-16',
'Content-Disposition: form-data; name="submit"',
'',
'\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
'--X--'
]))
self.getPage('/reqparams', method='POST',
headers=[(
'Content-Type', 'multipart/form-data;boundary=X'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'submit: Create, text: ab\xe2\x80\x9cc')
@mock.patch('cherrypy._cpreqbody.Part.maxrambytes', 1)
def test_multipart_decoding_bigger_maxrambytes(self):
"""
Decoding of a multipart entity should also pass when
the entity is bigger than maxrambytes. See ticket #1352.
"""
self.test_multipart_decoding()
def test_multipart_decoding_no_charset(self):
# Test the decoding of a multipart entity when the charset (utf8) is
# NOT explicitly given, but is in the list of charsets to attempt.
body = ntob('\r\n'.join([
'--X',
'Content-Disposition: form-data; name="text"',
'',
'\xe2\x80\x9c',
'--X',
'Content-Disposition: form-data; name="submit"',
'',
'Create',
'--X--'
]))
self.getPage('/reqparams', method='POST',
headers=[(
'Content-Type', 'multipart/form-data;boundary=X'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'submit: Create, text: \xe2\x80\x9c')
def test_multipart_decoding_no_successful_charset(self):
# Test the decoding of a multipart entity when the charset (utf16) is
# NOT explicitly given, and is NOT in the list of charsets to attempt.
body = ntob('\r\n'.join([
'--X',
'Content-Disposition: form-data; name="text"',
'',
'\xff\xfea\x00b\x00\x1c c\x00',
'--X',
'Content-Disposition: form-data; name="submit"',
'',
'\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
'--X--'
]))
self.getPage('/reqparams', method='POST',
headers=[(
'Content-Type', 'multipart/form-data;boundary=X'),
('Content-Length', str(len(body))),
],
body=body),
self.assertStatus(400)
self.assertErrorPage(
400,
'The request entity could not be decoded. The following charsets '
"were attempted: ['us-ascii', 'utf-8']")
def test_nontext(self):
self.getPage('/nontext')
self.assertHeader('Content-Type', 'application/binary;charset=utf-8')
self.assertBody('\x00\x01\x02\x03')
def testEncoding(self):
# Default encoding should be utf-8
self.getPage('/mao_zedong')
self.assertBody(sing8)
# Ask for utf-16.
self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')])
self.assertHeader('Content-Type', 'text/html;charset=utf-16')
self.assertBody(sing16)
# Ask for multiple encodings. ISO-8859-1 should fail, and utf-16
# should be produced.
self.getPage('/mao_zedong', [('Accept-Charset',
'iso-8859-1;q=1, utf-16;q=0.5')])
self.assertBody(sing16)
# The "*" value should default to our default_encoding, utf-8
self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')])
self.assertBody(sing8)
# Only allow iso-8859-1, which should fail and raise 406.
self.getPage('/mao_zedong', [('Accept-Charset', 'iso-8859-1, *;q=0')])
self.assertStatus('406 Not Acceptable')
self.assertInBody('Your client sent this Accept-Charset header: '
'iso-8859-1, *;q=0. We tried these charsets: '
'iso-8859-1.')
# Ask for x-mac-ce, which should be unknown. See ticket #569.
self.getPage('/mao_zedong', [('Accept-Charset',
'us-ascii, ISO-8859-1, x-mac-ce')])
self.assertStatus('406 Not Acceptable')
self.assertInBody('Your client sent this Accept-Charset header: '
'us-ascii, ISO-8859-1, x-mac-ce. We tried these '
'charsets: ISO-8859-1, us-ascii, x-mac-ce.')
# Test the 'encoding' arg to encode.
self.getPage('/utf8')
self.assertBody(sing8)
self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')])
self.assertStatus('406 Not Acceptable')
# Test malformed quality value, which should raise 400.
self.getPage('/mao_zedong', [('Accept-Charset',
'ISO-8859-1,utf-8;q=0.7,*;q=0.7)')])
self.assertStatus('400 Bad Request')
def testGzip(self):
zbuf = io.BytesIO()
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
zfile.write(b'Hello, world')
zfile.close()
self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip')])
self.assertInBody(zbuf.getvalue()[:3])
self.assertHeader('Vary', 'Accept-Encoding')
self.assertHeader('Content-Encoding', 'gzip')
# Test when gzip is denied.
self.getPage('/gzip/', headers=[('Accept-Encoding', 'identity')])
self.assertHeader('Vary', 'Accept-Encoding')
self.assertNoHeader('Content-Encoding')
self.assertBody('Hello, world')
self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip;q=0')])
self.assertHeader('Vary', 'Accept-Encoding')
self.assertNoHeader('Content-Encoding')
self.assertBody('Hello, world')
# Test that trailing comma doesn't cause IndexError
# Ref: https://github.com/cherrypy/cherrypy/issues/988
self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip,deflate,')])
self.assertStatus(200)
self.assertNotInBody('IndexError')
self.getPage('/gzip/', headers=[('Accept-Encoding', '*;q=0')])
self.assertStatus(406)
self.assertNoHeader('Content-Encoding')
self.assertErrorPage(406, 'identity, gzip')
# Test for ticket #147
self.getPage('/gzip/noshow', headers=[('Accept-Encoding', 'gzip')])
self.assertNoHeader('Content-Encoding')
self.assertStatus(500)
self.assertErrorPage(500, pattern='IndexError\n')
# In this case, there's nothing we can do to deliver a
# readable page, since 1) the gzip header is already set,
# and 2) we may have already written some of the body.
# The fix is to never stream yields when using gzip.
if (cherrypy.server.protocol_version == 'HTTP/1.0' or
getattr(cherrypy.server, 'using_apache', False)):
self.getPage('/gzip/noshow_stream',
headers=[('Accept-Encoding', 'gzip')])
self.assertHeader('Content-Encoding', 'gzip')
self.assertInBody('\x1f\x8b\x08\x00')
else:
# The wsgiserver will simply stop sending data, and the HTTP client
# will error due to an incomplete chunk-encoded stream.
self.assertRaises((ValueError, IncompleteRead), self.getPage,
'/gzip/noshow_stream',
headers=[('Accept-Encoding', 'gzip')])
def test_UnicodeHeaders(self):
self.getPage('/cookies_and_headers')
self.assertBody('Any content')
def test_BytesHeaders(self):
self.getPage('/cookies_and_headers')
self.assertBody('Any content')
self.assertHeader('Bytes-Header', 'Bytes given header')

View file

@ -0,0 +1,84 @@
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy.test import helper
class ETagTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def resource(self):
return 'Oh wah ta goo Siam.'
@cherrypy.expose
def fail(self, code):
code = int(code)
if 300 <= code <= 399:
raise cherrypy.HTTPRedirect([], code)
else:
raise cherrypy.HTTPError(code)
@cherrypy.expose
# In Python 3, tools.encode is on by default
@cherrypy.config(**{'tools.encode.on': True})
def unicoded(self):
return ntou('I am a \u1ee4nicode string.', 'escape')
conf = {'/': {'tools.etags.on': True,
'tools.etags.autotags': True,
}}
cherrypy.tree.mount(Root(), config=conf)
def test_etags(self):
self.getPage('/resource')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
self.assertBody('Oh wah ta goo Siam.')
etag = self.assertHeader('ETag')
# Test If-Match (both valid and invalid)
self.getPage('/resource', headers=[('If-Match', etag)])
self.assertStatus('200 OK')
self.getPage('/resource', headers=[('If-Match', '*')])
self.assertStatus('200 OK')
self.getPage('/resource', headers=[('If-Match', '*')], method='POST')
self.assertStatus('200 OK')
self.getPage('/resource', headers=[('If-Match', 'a bogus tag')])
self.assertStatus('412 Precondition Failed')
# Test If-None-Match (both valid and invalid)
self.getPage('/resource', headers=[('If-None-Match', etag)])
self.assertStatus(304)
self.getPage('/resource', method='POST',
headers=[('If-None-Match', etag)])
self.assertStatus('412 Precondition Failed')
self.getPage('/resource', headers=[('If-None-Match', '*')])
self.assertStatus(304)
self.getPage('/resource', headers=[('If-None-Match', 'a bogus tag')])
self.assertStatus('200 OK')
def test_errors(self):
self.getPage('/resource')
self.assertStatus(200)
etag = self.assertHeader('ETag')
# Test raising errors in page handler
self.getPage('/fail/412', headers=[('If-Match', etag)])
self.assertStatus(412)
self.getPage('/fail/304', headers=[('If-Match', etag)])
self.assertStatus(304)
self.getPage('/fail/412', headers=[('If-None-Match', '*')])
self.assertStatus(412)
self.getPage('/fail/304', headers=[('If-None-Match', '*')])
self.assertStatus(304)
def test_unicode_body(self):
self.getPage('/unicoded')
self.assertStatus(200)
etag1 = self.assertHeader('ETag')
self.getPage('/unicoded', headers=[('If-Match', etag1)])
self.assertStatus(200)
self.assertHeader('ETag', etag1)

View file

@ -0,0 +1,307 @@
# coding: utf-8
"""Tests for managing HTTP issues (malformed requests, etc)."""
import errno
import mimetypes
import socket
import sys
from unittest import mock
import six
from six.moves.http_client import HTTPConnection
from six.moves import urllib
import cherrypy
from cherrypy._cpcompat import HTTPSConnection, quote
from cherrypy.test import helper
def is_ascii(text):
"""
Return True if the text encodes as ascii.
"""
try:
text.encode('ascii')
return True
except Exception:
pass
return False
def encode_filename(filename):
"""
Given a filename to be used in a multipart/form-data,
encode the name. Return the key and encoded filename.
"""
if is_ascii(filename):
return 'filename', '"{filename}"'.format(**locals())
encoded = quote(filename, encoding='utf-8')
return 'filename*', "'".join((
'UTF-8',
'', # lang
encoded,
))
def encode_multipart_formdata(files):
"""Return (content_type, body) ready for httplib.HTTP instance.
files: a sequence of (name, filename, value) tuples for multipart uploads.
filename can be a string or a tuple ('filename string', 'encoding')
"""
BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$'
L = []
for key, filename, value in files:
L.append('--' + BOUNDARY)
fn_key, encoded = encode_filename(filename)
tmpl = \
'Content-Disposition: form-data; name="{key}"; {fn_key}={encoded}'
L.append(tmpl.format(**locals()))
ct = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
L.append('Content-Type: %s' % ct)
L.append('')
L.append(value)
L.append('--' + BOUNDARY + '--')
L.append('')
body = '\r\n'.join(L)
content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
return content_type, body
class HTTPTests(helper.CPWebCase):
def make_connection(self):
if self.scheme == 'https':
return HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
else:
return HTTPConnection('%s:%s' % (self.interface(), self.PORT))
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self, *args, **kwargs):
return 'Hello world!'
@cherrypy.expose
@cherrypy.config(**{'request.process_request_body': False})
def no_body(self, *args, **kwargs):
return 'Hello world!'
@cherrypy.expose
def post_multipart(self, file):
"""Return a summary ("a * 65536\nb * 65536") of the uploaded
file.
"""
contents = file.file.read()
summary = []
curchar = None
count = 0
for c in contents:
if c == curchar:
count += 1
else:
if count:
if six.PY3:
curchar = chr(curchar)
summary.append('%s * %d' % (curchar, count))
count = 1
curchar = c
if count:
if six.PY3:
curchar = chr(curchar)
summary.append('%s * %d' % (curchar, count))
return ', '.join(summary)
@cherrypy.expose
def post_filename(self, myfile):
'''Return the name of the file which was uploaded.'''
return myfile.filename
cherrypy.tree.mount(Root())
cherrypy.config.update({'server.max_request_body_size': 30000000})
def test_no_content_length(self):
# "The presence of a message-body in a request is signaled by the
# inclusion of a Content-Length or Transfer-Encoding header field in
# the request's message-headers."
#
# Send a message with neither header and no body. Even though
# the request is of method POST, this should be OK because we set
# request.process_request_body to False for our handler.
c = self.make_connection()
c.request('POST', '/no_body')
response = c.getresponse()
self.body = response.fp.read()
self.status = str(response.status)
self.assertStatus(200)
self.assertBody(b'Hello world!')
# Now send a message that has no Content-Length, but does send a body.
# Verify that CP times out the socket and responds
# with 411 Length Required.
if self.scheme == 'https':
c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
else:
c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
# `_get_content_length` is needed for Python 3.6+
with mock.patch.object(
c,
'_get_content_length',
lambda body, method: None,
create=True):
# `_set_content_length` is needed for Python 2.7-3.5
with mock.patch.object(c, '_set_content_length', create=True):
c.request('POST', '/')
response = c.getresponse()
self.body = response.fp.read()
self.status = str(response.status)
self.assertStatus(411)
def test_post_multipart(self):
alphabet = 'abcdefghijklmnopqrstuvwxyz'
# generate file contents for a large post
contents = ''.join([c * 65536 for c in alphabet])
# encode as multipart form data
files = [('file', 'file.txt', contents)]
content_type, body = encode_multipart_formdata(files)
body = body.encode('Latin-1')
# post file
c = self.make_connection()
c.putrequest('POST', '/post_multipart')
c.putheader('Content-Type', content_type)
c.putheader('Content-Length', str(len(body)))
c.endheaders()
c.send(body)
response = c.getresponse()
self.body = response.fp.read()
self.status = str(response.status)
self.assertStatus(200)
parts = ['%s * 65536' % ch for ch in alphabet]
self.assertBody(', '.join(parts))
def test_post_filename_with_special_characters(self):
'''Testing that we can handle filenames with special characters. This
was reported as a bug in:
https://github.com/cherrypy/cherrypy/issues/1146/
https://github.com/cherrypy/cherrypy/issues/1397/
https://github.com/cherrypy/cherrypy/issues/1694/
'''
# We'll upload a bunch of files with differing names.
fnames = [
'boop.csv', 'foo, bar.csv', 'bar, xxxx.csv', 'file"name.csv',
'file;name.csv', 'file; name.csv', u'test_łóąä.txt',
]
for fname in fnames:
files = [('myfile', fname, 'yunyeenyunyue')]
content_type, body = encode_multipart_formdata(files)
body = body.encode('Latin-1')
# post file
c = self.make_connection()
c.putrequest('POST', '/post_filename')
c.putheader('Content-Type', content_type)
c.putheader('Content-Length', str(len(body)))
c.endheaders()
c.send(body)
response = c.getresponse()
self.body = response.fp.read()
self.status = str(response.status)
self.assertStatus(200)
self.assertBody(fname)
def test_malformed_request_line(self):
if getattr(cherrypy.server, 'using_apache', False):
return self.skip('skipped due to known Apache differences...')
# Test missing version in Request-Line
c = self.make_connection()
c._output(b'geT /')
c._send_output()
if hasattr(c, 'strict'):
response = c.response_class(c.sock, strict=c.strict, method='GET')
else:
# Python 3.2 removed the 'strict' feature, saying:
# "http.client now always assumes HTTP/1.x compliant servers."
response = c.response_class(c.sock, method='GET')
response.begin()
self.assertEqual(response.status, 400)
self.assertEqual(response.fp.read(22), b'Malformed Request-Line')
c.close()
def test_request_line_split_issue_1220(self):
params = {
'intervenant-entreprise-evenement_classaction':
'evenement-mailremerciements',
'_path': 'intervenant-entreprise-evenement',
'intervenant-entreprise-evenement_action-id': 19404,
'intervenant-entreprise-evenement_id': 19404,
'intervenant-entreprise_id': 28092,
}
Request_URI = '/index?' + urllib.parse.urlencode(params)
self.assertEqual(len('GET %s HTTP/1.1\r\n' % Request_URI), 256)
self.getPage(Request_URI)
self.assertBody('Hello world!')
def test_malformed_header(self):
c = self.make_connection()
c.putrequest('GET', '/')
c.putheader('Content-Type', 'text/plain')
# See https://github.com/cherrypy/cherrypy/issues/941
c._output(b're, 1.2.3.4#015#012')
c.endheaders()
response = c.getresponse()
self.status = str(response.status)
self.assertStatus(400)
self.body = response.fp.read(20)
self.assertBody('Illegal header line.')
def test_http_over_https(self):
if self.scheme != 'https':
return self.skip('skipped (not running HTTPS)... ')
# Try connecting without SSL.
conn = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
conn.putrequest('GET', '/', skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.response_class(conn.sock, method='GET')
try:
response.begin()
self.assertEqual(response.status, 400)
self.body = response.read()
self.assertBody('The client sent a plain HTTP request, but this '
'server only speaks HTTPS on this port.')
except socket.error:
e = sys.exc_info()[1]
# "Connection reset by peer" is also acceptable.
if e.errno != errno.ECONNRESET:
raise
def test_garbage_in(self):
# Connect without SSL regardless of server.scheme
c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
c._output(b'gjkgjklsgjklsgjkljklsg')
c._send_output()
response = c.response_class(c.sock, method='GET')
try:
response.begin()
self.assertEqual(response.status, 400)
self.assertEqual(response.fp.read(22),
b'Malformed Request-Line')
c.close()
except socket.error:
e = sys.exc_info()[1]
# "Connection reset by peer" is also acceptable.
if e.errno != errno.ECONNRESET:
raise

View file

@ -0,0 +1,80 @@
"""Test helpers from ``cherrypy.lib.httputil`` module."""
import pytest
from six.moves import http_client
from cherrypy.lib import httputil
@pytest.mark.parametrize(
'script_name,path_info,expected_url',
[
('/sn/', '/pi/', '/sn/pi/'),
('/sn/', '/pi', '/sn/pi'),
('/sn/', '/', '/sn/'),
('/sn/', '', '/sn/'),
('/sn', '/pi/', '/sn/pi/'),
('/sn', '/pi', '/sn/pi'),
('/sn', '/', '/sn/'),
('/sn', '', '/sn'),
('/', '/pi/', '/pi/'),
('/', '/pi', '/pi'),
('/', '/', '/'),
('/', '', '/'),
('', '/pi/', '/pi/'),
('', '/pi', '/pi'),
('', '/', '/'),
('', '', '/'),
]
)
def test_urljoin(script_name, path_info, expected_url):
"""Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO."""
actual_url = httputil.urljoin(script_name, path_info)
assert actual_url == expected_url
EXPECTED_200 = (200, 'OK', 'Request fulfilled, document follows')
EXPECTED_500 = (
500,
'Internal Server Error',
'The server encountered an unexpected condition which '
'prevented it from fulfilling the request.',
)
EXPECTED_404 = (404, 'Not Found', 'Nothing matches the given URI')
EXPECTED_444 = (444, 'Non-existent reason', '')
@pytest.mark.parametrize(
'status,expected_status',
[
(None, EXPECTED_200),
(200, EXPECTED_200),
('500', EXPECTED_500),
(http_client.NOT_FOUND, EXPECTED_404),
('444 Non-existent reason', EXPECTED_444),
]
)
def test_valid_status(status, expected_status):
"""Check valid int, string and http_client-constants
statuses processing."""
assert httputil.valid_status(status) == expected_status
@pytest.mark.parametrize(
'status_code,error_msg',
[
('hey', "Illegal response status from server ('hey' is non-numeric)."),
(
{'hey': 'hi'},
'Illegal response status from server '
"({'hey': 'hi'} is non-numeric).",
),
(1, 'Illegal response status from server (1 is out of range).'),
(600, 'Illegal response status from server (600 is out of range).'),
]
)
def test_invalid_status(status_code, error_msg):
"""Check that invalid status cause certain errors."""
with pytest.raises(ValueError) as excinfo:
httputil.valid_status(status_code)
assert error_msg in str(excinfo)

View file

@ -0,0 +1,196 @@
import six
import cherrypy
from cherrypy.test import helper
class IteratorBase(object):
created = 0
datachunk = 'butternut squash' * 256
@classmethod
def incr(cls):
cls.created += 1
@classmethod
def decr(cls):
cls.created -= 1
class OurGenerator(IteratorBase):
def __iter__(self):
self.incr()
try:
for i in range(1024):
yield self.datachunk
finally:
self.decr()
class OurIterator(IteratorBase):
started = False
closed_off = False
count = 0
def increment(self):
self.incr()
def decrement(self):
if not self.closed_off:
self.closed_off = True
self.decr()
def __iter__(self):
return self
def __next__(self):
if not self.started:
self.started = True
self.increment()
self.count += 1
if self.count > 1024:
raise StopIteration
return self.datachunk
next = __next__
def __del__(self):
self.decrement()
class OurClosableIterator(OurIterator):
def close(self):
self.decrement()
class OurNotClosableIterator(OurIterator):
# We can't close something which requires an additional argument.
def close(self, somearg):
self.decrement()
class OurUnclosableIterator(OurIterator):
close = 'close' # not callable!
class IteratorTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root(object):
@cherrypy.expose
def count(self, clsname):
cherrypy.response.headers['Content-Type'] = 'text/plain'
return six.text_type(globals()[clsname].created)
@cherrypy.expose
def getall(self, clsname):
cherrypy.response.headers['Content-Type'] = 'text/plain'
return globals()[clsname]()
@cherrypy.expose
@cherrypy.config(**{'response.stream': True})
def stream(self, clsname):
return self.getall(clsname)
cherrypy.tree.mount(Root())
def test_iterator(self):
try:
self._test_iterator()
except Exception:
'Test fails intermittently. See #1419'
def _test_iterator(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Check the counts of all the classes, they should be zero.
closables = ['OurClosableIterator', 'OurGenerator']
unclosables = ['OurUnclosableIterator', 'OurNotClosableIterator']
all_classes = closables + unclosables
import random
random.shuffle(all_classes)
for clsname in all_classes:
self.getPage('/count/' + clsname)
self.assertStatus(200)
self.assertBody('0')
# We should also be able to read the entire content body
# successfully, though we don't need to, we just want to
# check the header.
for clsname in all_classes:
itr_conn = self.get_conn()
itr_conn.putrequest('GET', '/getall/' + clsname)
itr_conn.endheaders()
response = itr_conn.getresponse()
self.assertEqual(response.status, 200)
headers = response.getheaders()
for header_name, header_value in headers:
if header_name.lower() == 'content-length':
expected = six.text_type(1024 * 16 * 256)
assert header_value == expected, header_value
break
else:
raise AssertionError('No Content-Length header found')
# As the response should be fully consumed by CherryPy
# before sending back, the count should still be at zero
# by the time the response has been sent.
self.getPage('/count/' + clsname)
self.assertStatus(200)
self.assertBody('0')
# Now we do the same check with streaming - some classes will
# be automatically closed, while others cannot.
stream_counts = {}
for clsname in all_classes:
itr_conn = self.get_conn()
itr_conn.putrequest('GET', '/stream/' + clsname)
itr_conn.endheaders()
response = itr_conn.getresponse()
self.assertEqual(response.status, 200)
response.fp.read(65536)
# Let's check the count - this should always be one.
self.getPage('/count/' + clsname)
self.assertBody('1')
# Now if we close the connection, the count should go back
# to zero.
itr_conn.close()
self.getPage('/count/' + clsname)
# If this is a response which should be easily closed, then
# we will test to see if the value has gone back down to
# zero.
if clsname in closables:
# Sometimes we try to get the answer too quickly - we
# will wait for 100 ms before asking again if we didn't
# get the answer we wanted.
if self.body != '0':
import time
time.sleep(0.1)
self.getPage('/count/' + clsname)
stream_counts[clsname] = int(self.body)
# Check that we closed off the classes which should provide
# easy mechanisms for doing so.
for clsname in closables:
assert stream_counts[clsname] == 0, (
'did not close off stream response correctly, expected '
'count of zero for %s: %s' % (clsname, stream_counts)
)

View file

@ -0,0 +1,102 @@
import cherrypy
from cherrypy.test import helper
from cherrypy._cpcompat import json
json_out = cherrypy.config(**{'tools.json_out.on': True})
json_in = cherrypy.config(**{'tools.json_in.on': True})
class JsonTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root(object):
@cherrypy.expose
def plain(self):
return 'hello'
@cherrypy.expose
@json_out
def json_string(self):
return 'hello'
@cherrypy.expose
@json_out
def json_list(self):
return ['a', 'b', 42]
@cherrypy.expose
@json_out
def json_dict(self):
return {'answer': 42}
@cherrypy.expose
@json_in
def json_post(self):
if cherrypy.request.json == [13, 'c']:
return 'ok'
else:
return 'nok'
@cherrypy.expose
@json_out
@cherrypy.config(**{'tools.caching.on': True})
def json_cached(self):
return 'hello there'
root = Root()
cherrypy.tree.mount(root)
def test_json_output(self):
if json is None:
self.skip('json not found ')
return
self.getPage('/plain')
self.assertBody('hello')
self.getPage('/json_string')
self.assertBody('"hello"')
self.getPage('/json_list')
self.assertBody('["a", "b", 42]')
self.getPage('/json_dict')
self.assertBody('{"answer": 42}')
def test_json_input(self):
if json is None:
self.skip('json not found ')
return
body = '[13, "c"]'
headers = [('Content-Type', 'application/json'),
('Content-Length', str(len(body)))]
self.getPage('/json_post', method='POST', headers=headers, body=body)
self.assertBody('ok')
body = '[13, "c"]'
headers = [('Content-Type', 'text/plain'),
('Content-Length', str(len(body)))]
self.getPage('/json_post', method='POST', headers=headers, body=body)
self.assertStatus(415, 'Expected an application/json content type')
body = '[13, -]'
headers = [('Content-Type', 'application/json'),
('Content-Length', str(len(body)))]
self.getPage('/json_post', method='POST', headers=headers, body=body)
self.assertStatus(400, 'Invalid JSON document')
def test_cached(self):
if json is None:
self.skip('json not found ')
return
self.getPage('/json_cached')
self.assertStatus(200, '"hello"')
self.getPage('/json_cached') # 2'nd time to hit cache
self.assertStatus(200, '"hello"')

View file

@ -0,0 +1,209 @@
"""Basic tests for the CherryPy core: request handling."""
import os
from unittest import mock
import six
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy.test import helper, logtest
localDir = os.path.dirname(__file__)
access_log = os.path.join(localDir, 'access.log')
error_log = os.path.join(localDir, 'error.log')
# Some unicode strings.
tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape')
erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape')
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'hello'
@cherrypy.expose
def uni_code(self):
cherrypy.request.login = tartaros
cherrypy.request.remote.name = erebos
@cherrypy.expose
def slashes(self):
cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1'
@cherrypy.expose
def whitespace(self):
# User-Agent = "User-Agent" ":" 1*( product | comment )
# comment = "(" *( ctext | quoted-pair | comment ) ")"
# ctext = <any TEXT excluding "(" and ")">
# TEXT = <any OCTET except CTLs, but including LWS>
# LWS = [CRLF] 1*( SP | HT )
cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)'
@cherrypy.expose
def as_string(self):
return 'content'
@cherrypy.expose
def as_yield(self):
yield 'content'
@cherrypy.expose
@cherrypy.config(**{'tools.log_tracebacks.on': True})
def error(self):
raise ValueError()
root = Root()
cherrypy.config.update({
'log.error_file': error_log,
'log.access_file': access_log,
})
cherrypy.tree.mount(root)
class AccessLogTests(helper.CPWebCase, logtest.LogCase):
setup_server = staticmethod(setup_server)
logfile = access_log
def testNormalReturn(self):
self.markLog()
self.getPage('/as_string',
headers=[('Referer', 'http://www.cherrypy.org/'),
('User-Agent', 'Mozilla/5.0')])
self.assertBody('content')
self.assertStatus(200)
intro = '%s - - [' % self.interface()
self.assertLog(-1, intro)
if [k for k, v in self.headers if k.lower() == 'content-length']:
self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 '
'"http://www.cherrypy.org/" "Mozilla/5.0"'
% self.prefix())
else:
self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - '
'"http://www.cherrypy.org/" "Mozilla/5.0"'
% self.prefix())
def testNormalYield(self):
self.markLog()
self.getPage('/as_yield')
self.assertBody('content')
self.assertStatus(200)
intro = '%s - - [' % self.interface()
self.assertLog(-1, intro)
if [k for k, v in self.headers if k.lower() == 'content-length']:
self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' %
self.prefix())
else:
self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""'
% self.prefix())
@mock.patch(
'cherrypy._cplogging.LogManager.access_log_format',
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}'
if six.PY3 else
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
)
def testCustomLogFormat(self):
"""Test a customized access_log_format string, which is a
feature of _cplogging.LogManager.access()."""
self.markLog()
self.getPage('/as_string', headers=[('Referer', 'REFERER'),
('User-Agent', 'USERAGENT'),
('Host', 'HOST')])
self.assertLog(-1, '%s - - [' % self.interface())
self.assertLog(-1, '] "GET /as_string HTTP/1.1" '
'200 7 "REFERER" "USERAGENT" HOST')
@mock.patch(
'cherrypy._cplogging.LogManager.access_log_format',
'{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}'
if six.PY3 else
'%(h)s %(l)s %(u)s %(z)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
)
def testTimezLogFormat(self):
"""Test a customized access_log_format string, which is a
feature of _cplogging.LogManager.access()."""
self.markLog()
expected_time = str(cherrypy._cplogging.LazyRfc3339UtcTime())
with mock.patch(
'cherrypy._cplogging.LazyRfc3339UtcTime',
lambda: expected_time):
self.getPage('/as_string', headers=[('Referer', 'REFERER'),
('User-Agent', 'USERAGENT'),
('Host', 'HOST')])
self.assertLog(-1, '%s - - ' % self.interface())
self.assertLog(-1, expected_time)
self.assertLog(-1, ' "GET /as_string HTTP/1.1" '
'200 7 "REFERER" "USERAGENT" HOST')
@mock.patch(
'cherrypy._cplogging.LogManager.access_log_format',
'{i}' if six.PY3 else '%(i)s'
)
def testUUIDv4ParameterLogFormat(self):
"""Test rendering of UUID4 within access log."""
self.markLog()
self.getPage('/as_string')
self.assertValidUUIDv4()
def testEscapedOutput(self):
# Test unicode in access log pieces.
self.markLog()
self.getPage('/uni_code')
self.assertStatus(200)
if six.PY3:
# The repr of a bytestring in six.PY3 includes a b'' prefix
self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1])
else:
self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1])
# Test the erebos value. Included inline for your enlightenment.
# Note the 'r' prefix--those backslashes are literals.
self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82')
# Test backslashes in output.
self.markLog()
self.getPage('/slashes')
self.assertStatus(200)
if six.PY3:
self.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"')
else:
self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"')
# Test whitespace in output.
self.markLog()
self.getPage('/whitespace')
self.assertStatus(200)
# Again, note the 'r' prefix.
self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"')
class ErrorLogTests(helper.CPWebCase, logtest.LogCase):
setup_server = staticmethod(setup_server)
logfile = error_log
def testTracebacks(self):
# Test that tracebacks get written to the error log.
self.markLog()
ignore = helper.webtest.ignored_exceptions
ignore.append(ValueError)
try:
self.getPage('/error')
self.assertInBody('raise ValueError()')
self.assertLog(0, 'HTTP')
self.assertLog(1, 'Traceback (most recent call last):')
self.assertLog(-2, 'raise ValueError()')
finally:
ignore.pop()

View file

@ -0,0 +1,134 @@
"""Tests for various MIME issues, including the safe_multipart Tool."""
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy.test import helper
def setup_server():
class Root:
@cherrypy.expose
def multipart(self, parts):
return repr(parts)
@cherrypy.expose
def multipart_form_data(self, **kwargs):
return repr(list(sorted(kwargs.items())))
@cherrypy.expose
def flashupload(self, Filedata, Upload, Filename):
return ('Upload: %s, Filename: %s, Filedata: %r' %
(Upload, Filename, Filedata.file.read()))
cherrypy.config.update({'server.max_request_body_size': 0})
cherrypy.tree.mount(Root())
# Client-side code #
class MultipartTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_multipart(self):
text_part = ntou('This is the text version')
html_part = ntou(
"""<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta content="text/html;charset=ISO-8859-1" http-equiv="Content-Type">
</head>
<body bgcolor="#ffffff" text="#000000">
This is the <strong>HTML</strong> version
</body>
</html>
""")
body = '\r\n'.join([
'--123456789',
"Content-Type: text/plain; charset='ISO-8859-1'",
'Content-Transfer-Encoding: 7bit',
'',
text_part,
'--123456789',
"Content-Type: text/html; charset='ISO-8859-1'",
'',
html_part,
'--123456789--'])
headers = [
('Content-Type', 'multipart/mixed; boundary=123456789'),
('Content-Length', str(len(body))),
]
self.getPage('/multipart', headers, 'POST', body)
self.assertBody(repr([text_part, html_part]))
def test_multipart_form_data(self):
body = '\r\n'.join([
'--X',
'Content-Disposition: form-data; name="foo"',
'',
'bar',
'--X',
# Test a param with more than one value.
# See
# https://github.com/cherrypy/cherrypy/issues/1028
'Content-Disposition: form-data; name="baz"',
'',
'111',
'--X',
'Content-Disposition: form-data; name="baz"',
'',
'333',
'--X--'
])
self.getPage('/multipart_form_data', method='POST',
headers=[(
'Content-Type', 'multipart/form-data;boundary=X'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(
repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))]))
class SafeMultipartHandlingTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_Flash_Upload(self):
headers = [
('Accept', 'text/*'),
('Content-Type', 'multipart/form-data; '
'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'),
('User-Agent', 'Shockwave Flash'),
('Host', 'www.example.com:54583'),
('Content-Length', '499'),
('Connection', 'Keep-Alive'),
('Cache-Control', 'no-cache'),
]
filedata = (b'<?xml version="1.0" encoding="UTF-8"?>\r\n'
b'<projectDescription>\r\n'
b'</projectDescription>\r\n')
body = (
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
b'Content-Disposition: form-data; name="Filename"\r\n'
b'\r\n'
b'.project\r\n'
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
b'Content-Disposition: form-data; '
b'name="Filedata"; filename=".project"\r\n'
b'Content-Type: application/octet-stream\r\n'
b'\r\n' +
filedata +
b'\r\n'
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
b'Content-Disposition: form-data; name="Upload"\r\n'
b'\r\n'
b'Submit Query\r\n'
# Flash apps omit the trailing \r\n on the last line:
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--'
)
self.getPage('/flashupload', headers, 'POST', body)
self.assertBody('Upload: Submit Query, Filename: .project, '
'Filedata: %r' % filedata)

View file

@ -0,0 +1,210 @@
import os
import cherrypy
from cherrypy import tools
from cherrypy.test import helper
localDir = os.path.dirname(__file__)
logfile = os.path.join(localDir, 'test_misc_tools.log')
def setup_server():
class Root:
@cherrypy.expose
def index(self):
yield 'Hello, world'
h = [('Content-Language', 'en-GB'), ('Content-Type', 'text/plain')]
tools.response_headers(headers=h)(index)
@cherrypy.expose
@cherrypy.config(**{
'tools.response_headers.on': True,
'tools.response_headers.headers': [
('Content-Language', 'fr'),
('Content-Type', 'text/plain'),
],
'tools.log_hooks.on': True,
})
def other(self):
return 'salut'
@cherrypy.config(**{'tools.accept.on': True})
class Accept:
@cherrypy.expose
def index(self):
return '<a href="feed">Atom feed</a>'
@cherrypy.expose
@tools.accept(media='application/atom+xml')
def feed(self):
return """<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Unknown Blog</title>
</feed>"""
@cherrypy.expose
def select(self):
# We could also write this: mtype = cherrypy.lib.accept.accept(...)
mtype = tools.accept.callable(['text/html', 'text/plain'])
if mtype == 'text/html':
return '<h2>Page Title</h2>'
else:
return 'PAGE TITLE'
class Referer:
@cherrypy.expose
def accept(self):
return 'Accepted!'
reject = accept
class AutoVary:
@cherrypy.expose
def index(self):
# Read a header directly with 'get'
cherrypy.request.headers.get('Accept-Encoding')
# Read a header directly with '__getitem__'
cherrypy.request.headers['Host']
# Read a header directly with '__contains__'
'If-Modified-Since' in cherrypy.request.headers
# Read a header directly
'Range' in cherrypy.request.headers
# Call a lib function
tools.accept.callable(['text/html', 'text/plain'])
return 'Hello, world!'
conf = {'/referer': {'tools.referer.on': True,
'tools.referer.pattern': r'http://[^/]*example\.com',
},
'/referer/reject': {'tools.referer.accept': False,
'tools.referer.accept_missing': True,
},
'/autovary': {'tools.autovary.on': True},
}
root = Root()
root.referer = Referer()
root.accept = Accept()
root.autovary = AutoVary()
cherrypy.tree.mount(root, config=conf)
cherrypy.config.update({'log.error_file': logfile})
class ResponseHeadersTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def testResponseHeadersDecorator(self):
self.getPage('/')
self.assertHeader('Content-Language', 'en-GB')
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
def testResponseHeaders(self):
self.getPage('/other')
self.assertHeader('Content-Language', 'fr')
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
class RefererTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def testReferer(self):
self.getPage('/referer/accept')
self.assertErrorPage(403, 'Forbidden Referer header.')
self.getPage('/referer/accept',
headers=[('Referer', 'http://www.example.com/')])
self.assertStatus(200)
self.assertBody('Accepted!')
# Reject
self.getPage('/referer/reject')
self.assertStatus(200)
self.assertBody('Accepted!')
self.getPage('/referer/reject',
headers=[('Referer', 'http://www.example.com/')])
self.assertErrorPage(403, 'Forbidden Referer header.')
class AcceptTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_Accept_Tool(self):
# Test with no header provided
self.getPage('/accept/feed')
self.assertStatus(200)
self.assertInBody('<title>Unknown Blog</title>')
# Specify exact media type
self.getPage('/accept/feed',
headers=[('Accept', 'application/atom+xml')])
self.assertStatus(200)
self.assertInBody('<title>Unknown Blog</title>')
# Specify matching media range
self.getPage('/accept/feed', headers=[('Accept', 'application/*')])
self.assertStatus(200)
self.assertInBody('<title>Unknown Blog</title>')
# Specify all media ranges
self.getPage('/accept/feed', headers=[('Accept', '*/*')])
self.assertStatus(200)
self.assertInBody('<title>Unknown Blog</title>')
# Specify unacceptable media types
self.getPage('/accept/feed', headers=[('Accept', 'text/html')])
self.assertErrorPage(406,
'Your client sent this Accept header: text/html. '
'But this resource only emits these media types: '
'application/atom+xml.')
# Test resource where tool is 'on' but media is None (not set).
self.getPage('/accept/')
self.assertStatus(200)
self.assertBody('<a href="feed">Atom feed</a>')
def test_accept_selection(self):
# Try both our expected media types
self.getPage('/accept/select', [('Accept', 'text/html')])
self.assertStatus(200)
self.assertBody('<h2>Page Title</h2>')
self.getPage('/accept/select', [('Accept', 'text/plain')])
self.assertStatus(200)
self.assertBody('PAGE TITLE')
self.getPage('/accept/select',
[('Accept', 'text/plain, text/*;q=0.5')])
self.assertStatus(200)
self.assertBody('PAGE TITLE')
# text/* and */* should prefer text/html since it comes first
# in our 'media' argument to tools.accept
self.getPage('/accept/select', [('Accept', 'text/*')])
self.assertStatus(200)
self.assertBody('<h2>Page Title</h2>')
self.getPage('/accept/select', [('Accept', '*/*')])
self.assertStatus(200)
self.assertBody('<h2>Page Title</h2>')
# Try unacceptable media types
self.getPage('/accept/select', [('Accept', 'application/xml')])
self.assertErrorPage(
406,
'Your client sent this Accept header: application/xml. '
'But this resource only emits these media types: '
'text/html, text/plain.')
class AutoVaryTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def testAutoVary(self):
self.getPage('/autovary/')
self.assertHeader(
'Vary',
'Accept, Accept-Charset, Accept-Encoding, '
'Host, If-Modified-Since, Range'
)

View file

@ -0,0 +1,38 @@
"""Test the native server."""
import pytest
from requests_toolbelt import sessions
import cherrypy._cpnative_server
pytestmark = pytest.mark.skipif(
'sys.platform == "win32"',
reason='tests fail on Windows',
)
@pytest.fixture
def cp_native_server(request):
"""A native server."""
class Root(object):
@cherrypy.expose
def index(self):
return 'Hello World!'
cls = cherrypy._cpnative_server.CPHTTPServer
cherrypy.server.httpserver = cls(cherrypy.server)
cherrypy.tree.mount(Root(), '/')
cherrypy.engine.start()
request.addfinalizer(cherrypy.engine.stop)
url = 'http://localhost:{cherrypy.server.socket_port}'.format(**globals())
return sessions.BaseUrlSession(url)
def test_basic_request(cp_native_server):
"""A request to a native server should succeed."""
resp = cp_native_server.get('/')
assert resp.ok
assert resp.status_code == 200
assert resp.text == 'Hello World!'

View file

@ -0,0 +1,430 @@
import sys
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy._cptree import Application
from cherrypy.test import helper
script_names = ['', '/foo', '/users/fred/blog', '/corp/blog']
class ObjectMappingTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self, name='world'):
return name
@cherrypy.expose
def foobar(self):
return 'bar'
@cherrypy.expose
def default(self, *params, **kwargs):
return 'default:' + repr(params)
@cherrypy.expose
def other(self):
return 'other'
@cherrypy.expose
def extra(self, *p):
return repr(p)
@cherrypy.expose
def redirect(self):
raise cherrypy.HTTPRedirect('dir1/', 302)
def notExposed(self):
return 'not exposed'
@cherrypy.expose
def confvalue(self):
return cherrypy.request.config.get('user')
@cherrypy.expose
def redirect_via_url(self, path):
raise cherrypy.HTTPRedirect(cherrypy.url(path))
@cherrypy.expose
def translate_html(self):
return 'OK'
@cherrypy.expose
def mapped_func(self, ID=None):
return 'ID is %s' % ID
setattr(Root, 'Von B\xfclow', mapped_func)
class Exposing:
@cherrypy.expose
def base(self):
return 'expose works!'
cherrypy.expose(base, '1')
cherrypy.expose(base, '2')
class ExposingNewStyle(object):
@cherrypy.expose
def base(self):
return 'expose works!'
cherrypy.expose(base, '1')
cherrypy.expose(base, '2')
class Dir1:
@cherrypy.expose
def index(self):
return 'index for dir1'
@cherrypy.expose
@cherrypy.config(**{'tools.trailing_slash.extra': True})
def myMethod(self):
return 'myMethod from dir1, path_info is:' + repr(
cherrypy.request.path_info)
@cherrypy.expose
def default(self, *params):
return 'default for dir1, param is:' + repr(params)
class Dir2:
@cherrypy.expose
def index(self):
return 'index for dir2, path is:' + cherrypy.request.path_info
@cherrypy.expose
def script_name(self):
return cherrypy.tree.script_name()
@cherrypy.expose
def cherrypy_url(self):
return cherrypy.url('/extra')
@cherrypy.expose
def posparam(self, *vpath):
return '/'.join(vpath)
class Dir3:
def default(self):
return 'default for dir3, not exposed'
class Dir4:
def index(self):
return 'index for dir4, not exposed'
class DefNoIndex:
@cherrypy.expose
def default(self, *args):
raise cherrypy.HTTPRedirect('contact')
# MethodDispatcher code
@cherrypy.expose
class ByMethod:
def __init__(self, *things):
self.things = list(things)
def GET(self):
return repr(self.things)
def POST(self, thing):
self.things.append(thing)
class Collection:
default = ByMethod('a', 'bit')
Root.exposing = Exposing()
Root.exposingnew = ExposingNewStyle()
Root.dir1 = Dir1()
Root.dir1.dir2 = Dir2()
Root.dir1.dir2.dir3 = Dir3()
Root.dir1.dir2.dir3.dir4 = Dir4()
Root.defnoindex = DefNoIndex()
Root.bymethod = ByMethod('another')
Root.collection = Collection()
d = cherrypy.dispatch.MethodDispatcher()
for url in script_names:
conf = {'/': {'user': (url or '/').split('/')[-2]},
'/bymethod': {'request.dispatch': d},
'/collection': {'request.dispatch': d},
}
cherrypy.tree.mount(Root(), url, conf)
class Isolated:
@cherrypy.expose
def index(self):
return 'made it!'
cherrypy.tree.mount(Isolated(), '/isolated')
@cherrypy.expose
class AnotherApp:
def GET(self):
return 'milk'
cherrypy.tree.mount(AnotherApp(), '/app',
{'/': {'request.dispatch': d}})
def testObjectMapping(self):
for url in script_names:
self.script_name = url
self.getPage('/')
self.assertBody('world')
self.getPage('/dir1/myMethod')
self.assertBody(
"myMethod from dir1, path_info is:'/dir1/myMethod'")
self.getPage('/this/method/does/not/exist')
self.assertBody(
"default:('this', 'method', 'does', 'not', 'exist')")
self.getPage('/extra/too/much')
self.assertBody("('too', 'much')")
self.getPage('/other')
self.assertBody('other')
self.getPage('/notExposed')
self.assertBody("default:('notExposed',)")
self.getPage('/dir1/dir2/')
self.assertBody('index for dir2, path is:/dir1/dir2/')
# Test omitted trailing slash (should be redirected by default).
self.getPage('/dir1/dir2')
self.assertStatus(301)
self.assertHeader('Location', '%s/dir1/dir2/' % self.base())
# Test extra trailing slash (should be redirected if configured).
self.getPage('/dir1/myMethod/')
self.assertStatus(301)
self.assertHeader('Location', '%s/dir1/myMethod' % self.base())
# Test that default method must be exposed in order to match.
self.getPage('/dir1/dir2/dir3/dir4/index')
self.assertBody(
"default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')")
# Test *vpath when default() is defined but not index()
# This also tests HTTPRedirect with default.
self.getPage('/defnoindex')
self.assertStatus((302, 303))
self.assertHeader('Location', '%s/contact' % self.base())
self.getPage('/defnoindex/')
self.assertStatus((302, 303))
self.assertHeader('Location', '%s/defnoindex/contact' %
self.base())
self.getPage('/defnoindex/page')
self.assertStatus((302, 303))
self.assertHeader('Location', '%s/defnoindex/contact' %
self.base())
self.getPage('/redirect')
self.assertStatus('302 Found')
self.assertHeader('Location', '%s/dir1/' % self.base())
if not getattr(cherrypy.server, 'using_apache', False):
# Test that we can use URL's which aren't all valid Python
# identifiers
# This should also test the %XX-unquoting of URL's.
self.getPage('/Von%20B%fclow?ID=14')
self.assertBody('ID is 14')
# Test that %2F in the path doesn't get unquoted too early;
# that is, it should not be used to separate path components.
# See ticket #393.
self.getPage('/page%2Fname')
self.assertBody("default:('page/name',)")
self.getPage('/dir1/dir2/script_name')
self.assertBody(url)
self.getPage('/dir1/dir2/cherrypy_url')
self.assertBody('%s/extra' % self.base())
# Test that configs don't overwrite each other from different apps
self.getPage('/confvalue')
self.assertBody((url or '/').split('/')[-2])
self.script_name = ''
# Test absoluteURI's in the Request-Line
self.getPage('http://%s:%s/' % (self.interface(), self.PORT))
self.assertBody('world')
self.getPage('http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' %
(self.interface(), self.PORT))
self.assertBody("default:('abs',)")
self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z')
self.assertBody("default:('rel',)")
# Test that the "isolated" app doesn't leak url's into the root app.
# If it did leak, Root.default() would answer with
# "default:('isolated', 'doesnt', 'exist')".
self.getPage('/isolated/')
self.assertStatus('200 OK')
self.assertBody('made it!')
self.getPage('/isolated/doesnt/exist')
self.assertStatus('404 Not Found')
# Make sure /foobar maps to Root.foobar and not to the app
# mounted at /foo. See
# https://github.com/cherrypy/cherrypy/issues/573
self.getPage('/foobar')
self.assertBody('bar')
def test_translate(self):
self.getPage('/translate_html')
self.assertStatus('200 OK')
self.assertBody('OK')
self.getPage('/translate.html')
self.assertStatus('200 OK')
self.assertBody('OK')
self.getPage('/translate-html')
self.assertStatus('200 OK')
self.assertBody('OK')
def test_redir_using_url(self):
for url in script_names:
self.script_name = url
# Test the absolute path to the parent (leading slash)
self.getPage('/redirect_via_url?path=./')
self.assertStatus(('302 Found', '303 See Other'))
self.assertHeader('Location', '%s/' % self.base())
# Test the relative path to the parent (no leading slash)
self.getPage('/redirect_via_url?path=./')
self.assertStatus(('302 Found', '303 See Other'))
self.assertHeader('Location', '%s/' % self.base())
# Test the absolute path to the parent (leading slash)
self.getPage('/redirect_via_url/?path=./')
self.assertStatus(('302 Found', '303 See Other'))
self.assertHeader('Location', '%s/' % self.base())
# Test the relative path to the parent (no leading slash)
self.getPage('/redirect_via_url/?path=./')
self.assertStatus(('302 Found', '303 See Other'))
self.assertHeader('Location', '%s/' % self.base())
def testPositionalParams(self):
self.getPage('/dir1/dir2/posparam/18/24/hut/hike')
self.assertBody('18/24/hut/hike')
# intermediate index methods should not receive posparams;
# only the "final" index method should do so.
self.getPage('/dir1/dir2/5/3/sir')
self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')")
# test that extra positional args raises an 404 Not Found
# See https://github.com/cherrypy/cherrypy/issues/733.
self.getPage('/dir1/dir2/script_name/extra/stuff')
self.assertStatus(404)
def testExpose(self):
# Test the cherrypy.expose function/decorator
self.getPage('/exposing/base')
self.assertBody('expose works!')
self.getPage('/exposing/1')
self.assertBody('expose works!')
self.getPage('/exposing/2')
self.assertBody('expose works!')
self.getPage('/exposingnew/base')
self.assertBody('expose works!')
self.getPage('/exposingnew/1')
self.assertBody('expose works!')
self.getPage('/exposingnew/2')
self.assertBody('expose works!')
def testMethodDispatch(self):
self.getPage('/bymethod')
self.assertBody("['another']")
self.assertHeader('Allow', 'GET, HEAD, POST')
self.getPage('/bymethod', method='HEAD')
self.assertBody('')
self.assertHeader('Allow', 'GET, HEAD, POST')
self.getPage('/bymethod', method='POST', body='thing=one')
self.assertBody('')
self.assertHeader('Allow', 'GET, HEAD, POST')
self.getPage('/bymethod')
self.assertBody(repr(['another', ntou('one')]))
self.assertHeader('Allow', 'GET, HEAD, POST')
self.getPage('/bymethod', method='PUT')
self.assertErrorPage(405)
self.assertHeader('Allow', 'GET, HEAD, POST')
# Test default with posparams
self.getPage('/collection/silly', method='POST')
self.getPage('/collection', method='GET')
self.assertBody("['a', 'bit', 'silly']")
# Test custom dispatcher set on app root (see #737).
self.getPage('/app')
self.assertBody('milk')
def testTreeMounting(self):
class Root(object):
@cherrypy.expose
def hello(self):
return 'Hello world!'
# When mounting an application instance,
# we can't specify a different script name in the call to mount.
a = Application(Root(), '/somewhere')
self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse')
# When mounting an application instance...
a = Application(Root(), '/somewhere')
# ...we MUST allow in identical script name in the call to mount...
cherrypy.tree.mount(a, '/somewhere')
self.getPage('/somewhere/hello')
self.assertStatus(200)
# ...and MUST allow a missing script_name.
del cherrypy.tree.apps['/somewhere']
cherrypy.tree.mount(a)
self.getPage('/somewhere/hello')
self.assertStatus(200)
# In addition, we MUST be able to create an Application using
# script_name == None for access to the wsgi_environ.
a = Application(Root(), script_name=None)
# However, this does not apply to tree.mount
self.assertRaises(TypeError, cherrypy.tree.mount, a, None)
def testKeywords(self):
if sys.version_info < (3,):
return self.skip('skipped (Python 3 only)')
exec("""class Root(object):
@cherrypy.expose
def hello(self, *, name='world'):
return 'Hello %s!' % name
cherrypy.tree.mount(Application(Root(), '/keywords'))""")
self.getPage('/keywords/hello')
self.assertStatus(200)
self.getPage('/keywords/hello/extra')
self.assertStatus(404)

View file

@ -0,0 +1,61 @@
import sys
import textwrap
import cherrypy
from cherrypy.test import helper
class ParamsTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
@cherrypy.tools.json_out()
@cherrypy.tools.params()
def resource(self, limit=None, sort=None):
return type(limit).__name__
# for testing on Py 2
resource.__annotations__ = {'limit': int}
conf = {'/': {'tools.params.on': True}}
cherrypy.tree.mount(Root(), config=conf)
def test_pass(self):
self.getPage('/resource')
self.assertStatus(200)
self.assertBody('"NoneType"')
self.getPage('/resource?limit=0')
self.assertStatus(200)
self.assertBody('"int"')
def test_error(self):
self.getPage('/resource?limit=')
self.assertStatus(400)
self.assertInBody('invalid literal for int')
cherrypy.config['tools.params.error'] = 422
self.getPage('/resource?limit=')
self.assertStatus(422)
self.assertInBody('invalid literal for int')
cherrypy.config['tools.params.exception'] = TypeError
self.getPage('/resource?limit=')
self.assertStatus(500)
def test_syntax(self):
if sys.version_info < (3,):
return self.skip('skipped (Python 3 only)')
code = textwrap.dedent("""
class Root:
@cherrypy.expose
@cherrypy.tools.params()
def resource(self, limit: int):
return type(limit).__name__
conf = {'/': {'tools.params.on': True}}
cherrypy.tree.mount(Root(), config=conf)
""")
exec(code)
self.getPage('/resource?limit=0')
self.assertStatus(200)
self.assertBody('int')

View file

@ -0,0 +1,14 @@
from cherrypy.process import plugins
__metaclass__ = type
class TestAutoreloader:
def test_file_for_file_module_when_None(self):
"""No error when module.__file__ is None.
"""
class test_module:
__file__ = None
assert plugins.Autoreloader._file_for_file_module(test_module) is None

View file

@ -0,0 +1,154 @@
import cherrypy
from cherrypy.test import helper
script_names = ['', '/path/to/myapp']
class ProxyTest(helper.CPWebCase):
@staticmethod
def setup_server():
# Set up site
cherrypy.config.update({
'tools.proxy.on': True,
'tools.proxy.base': 'www.mydomain.test',
})
# Set up application
class Root:
def __init__(self, sn):
# Calculate a URL outside of any requests.
self.thisnewpage = cherrypy.url(
'/this/new/page', script_name=sn)
@cherrypy.expose
def pageurl(self):
return self.thisnewpage
@cherrypy.expose
def index(self):
raise cherrypy.HTTPRedirect('dummy')
@cherrypy.expose
def remoteip(self):
return cherrypy.request.remote.ip
@cherrypy.expose
@cherrypy.config(**{
'tools.proxy.local': 'X-Host',
'tools.trailing_slash.extra': True,
})
def xhost(self):
raise cherrypy.HTTPRedirect('blah')
@cherrypy.expose
def base(self):
return cherrypy.request.base
@cherrypy.expose
@cherrypy.config(**{'tools.proxy.scheme': 'X-Forwarded-Ssl'})
def ssl(self):
return cherrypy.request.base
@cherrypy.expose
def newurl(self):
return ("Browse to <a href='%s'>this page</a>."
% cherrypy.url('/this/new/page'))
@cherrypy.expose
@cherrypy.config(**{
'tools.proxy.base': None,
})
def base_no_base(self):
return cherrypy.request.base
for sn in script_names:
cherrypy.tree.mount(Root(sn), sn)
def testProxy(self):
self.getPage('/')
self.assertHeader('Location',
'%s://www.mydomain.test%s/dummy' %
(self.scheme, self.prefix()))
# Test X-Forwarded-Host (Apache 1.3.33+ and Apache 2)
self.getPage(
'/', headers=[('X-Forwarded-Host', 'http://www.example.test')])
self.assertHeader('Location', 'http://www.example.test/dummy')
self.getPage('/', headers=[('X-Forwarded-Host', 'www.example.test')])
self.assertHeader('Location', '%s://www.example.test/dummy' %
self.scheme)
# Test multiple X-Forwarded-Host headers
self.getPage('/', headers=[
('X-Forwarded-Host', 'http://www.example.test, www.cherrypy.test'),
])
self.assertHeader('Location', 'http://www.example.test/dummy')
# Test X-Forwarded-For (Apache2)
self.getPage('/remoteip',
headers=[('X-Forwarded-For', '192.168.0.20')])
self.assertBody('192.168.0.20')
# Fix bug #1268
self.getPage('/remoteip',
headers=[
('X-Forwarded-For', '67.15.36.43, 192.168.0.20')
])
self.assertBody('67.15.36.43')
# Test X-Host (lighttpd; see https://trac.lighttpd.net/trac/ticket/418)
self.getPage('/xhost', headers=[('X-Host', 'www.example.test')])
self.assertHeader('Location', '%s://www.example.test/blah' %
self.scheme)
# Test X-Forwarded-Proto (lighttpd)
self.getPage('/base', headers=[('X-Forwarded-Proto', 'https')])
self.assertBody('https://www.mydomain.test')
# Test X-Forwarded-Ssl (webfaction?)
self.getPage('/ssl', headers=[('X-Forwarded-Ssl', 'on')])
self.assertBody('https://www.mydomain.test')
# Test cherrypy.url()
for sn in script_names:
# Test the value inside requests
self.getPage(sn + '/newurl')
self.assertBody(
"Browse to <a href='%s://www.mydomain.test" % self.scheme +
sn + "/this/new/page'>this page</a>.")
self.getPage(sn + '/newurl', headers=[('X-Forwarded-Host',
'http://www.example.test')])
self.assertBody("Browse to <a href='http://www.example.test" +
sn + "/this/new/page'>this page</a>.")
# Test the value outside requests
port = ''
if self.scheme == 'http' and self.PORT != 80:
port = ':%s' % self.PORT
elif self.scheme == 'https' and self.PORT != 443:
port = ':%s' % self.PORT
host = self.HOST
if host in ('0.0.0.0', '::'):
import socket
host = socket.gethostname()
expected = ('%s://%s%s%s/this/new/page'
% (self.scheme, host, port, sn))
self.getPage(sn + '/pageurl')
self.assertBody(expected)
# Test trailing slash (see
# https://github.com/cherrypy/cherrypy/issues/562).
self.getPage('/xhost/', headers=[('X-Host', 'www.example.test')])
self.assertHeader('Location', '%s://www.example.test/xhost'
% self.scheme)
def test_no_base_port_in_host(self):
"""
If no base is indicated, and the host header is used to resolve
the base, it should rely on the host header for the port also.
"""
headers = {'Host': 'localhost:8080'}.items()
self.getPage('/base_no_base', headers=headers)
self.assertBody('http://localhost:8080')

View file

@ -0,0 +1,66 @@
"""Tests for refleaks."""
import itertools
import platform
import threading
from six.moves.http_client import HTTPConnection
import cherrypy
from cherrypy._cpcompat import HTTPSConnection
from cherrypy.test import helper
data = object()
class ReferenceTests(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self, *args, **kwargs):
cherrypy.request.thing = data
return 'Hello world!'
cherrypy.tree.mount(Root())
def test_threadlocal_garbage(self):
if platform.system() == 'Darwin':
self.skip('queue issues; see #1474')
success = itertools.count()
def getpage():
host = '%s:%s' % (self.interface(), self.PORT)
if self.scheme == 'https':
c = HTTPSConnection(host)
else:
c = HTTPConnection(host)
try:
c.putrequest('GET', '/')
c.endheaders()
response = c.getresponse()
body = response.read()
self.assertEqual(response.status, 200)
self.assertEqual(body, b'Hello world!')
finally:
c.close()
next(success)
ITERATIONS = 25
ts = [
threading.Thread(target=getpage)
for _ in range(ITERATIONS)
]
for t in ts:
t.start()
for t in ts:
t.join()
self.assertEqual(next(success), ITERATIONS)

View file

@ -0,0 +1,932 @@
"""Basic tests for the cherrypy.Request object."""
from functools import wraps
import os
import sys
import types
import uuid
import six
from six.moves.http_client import IncompleteRead
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy.lib import httputil
from cherrypy.test import helper
localDir = os.path.dirname(__file__)
defined_http_methods = ('OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE',
'TRACE', 'PROPFIND', 'PATCH')
# Client-side code #
class RequestObjectTests(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'hello'
@cherrypy.expose
def scheme(self):
return cherrypy.request.scheme
@cherrypy.expose
def created_example_com_3128(self):
"""Handle CONNECT method."""
cherrypy.response.status = 204
@cherrypy.expose
def body_example_com_3128(self):
"""Handle CONNECT method."""
return (
cherrypy.request.method
+ 'ed to '
+ cherrypy.request.path_info
)
@cherrypy.expose
def request_uuid4(self):
return [
str(cherrypy.request.unique_id),
' ',
str(cherrypy.request.unique_id),
]
root = Root()
class TestType(type):
"""Metaclass which automatically exposes all functions in each
subclass, and adds an instance of the subclass as an attribute
of root.
"""
def __init__(cls, name, bases, dct):
type.__init__(cls, name, bases, dct)
for value in dct.values():
if isinstance(value, types.FunctionType):
value.exposed = True
setattr(root, name.lower(), cls())
Test = TestType('Test', (object,), {})
class PathInfo(Test):
def default(self, *args):
return cherrypy.request.path_info
class Params(Test):
def index(self, thing):
return repr(thing)
def ismap(self, x, y):
return 'Coordinates: %s, %s' % (x, y)
@cherrypy.config(**{'request.query_string_encoding': 'latin1'})
def default(self, *args, **kwargs):
return 'args: %s kwargs: %s' % (args, sorted(kwargs.items()))
@cherrypy.expose
class ParamErrorsCallable(object):
def __call__(self):
return 'data'
def handler_dec(f):
@wraps(f)
def wrapper(handler, *args, **kwargs):
return f(handler, *args, **kwargs)
return wrapper
class ParamErrors(Test):
@cherrypy.expose
def one_positional(self, param1):
return 'data'
@cherrypy.expose
def one_positional_args(self, param1, *args):
return 'data'
@cherrypy.expose
def one_positional_args_kwargs(self, param1, *args, **kwargs):
return 'data'
@cherrypy.expose
def one_positional_kwargs(self, param1, **kwargs):
return 'data'
@cherrypy.expose
def no_positional(self):
return 'data'
@cherrypy.expose
def no_positional_args(self, *args):
return 'data'
@cherrypy.expose
def no_positional_args_kwargs(self, *args, **kwargs):
return 'data'
@cherrypy.expose
def no_positional_kwargs(self, **kwargs):
return 'data'
callable_object = ParamErrorsCallable()
@cherrypy.expose
def raise_type_error(self, **kwargs):
raise TypeError('Client Error')
@cherrypy.expose
def raise_type_error_with_default_param(self, x, y=None):
return '%d' % 'a' # throw an exception
@cherrypy.expose
@handler_dec
def raise_type_error_decorated(self, *args, **kwargs):
raise TypeError('Client Error')
def callable_error_page(status, **kwargs):
return "Error %s - Well, I'm very sorry but you haven't paid!" % (
status)
@cherrypy.config(**{'tools.log_tracebacks.on': True})
class Error(Test):
def reason_phrase(self):
raise cherrypy.HTTPError("410 Gone fishin'")
@cherrypy.config(**{
'error_page.404': os.path.join(localDir, 'static/index.html'),
'error_page.401': callable_error_page,
})
def custom(self, err='404'):
raise cherrypy.HTTPError(
int(err), 'No, <b>really</b>, not found!')
@cherrypy.config(**{
'error_page.default': callable_error_page,
})
def custom_default(self):
return 1 + 'a' # raise an unexpected error
@cherrypy.config(**{'error_page.404': 'nonexistent.html'})
def noexist(self):
raise cherrypy.HTTPError(404, 'No, <b>really</b>, not found!')
def page_method(self):
raise ValueError()
def page_yield(self):
yield 'howdy'
raise ValueError()
@cherrypy.config(**{'response.stream': True})
def page_streamed(self):
yield 'word up'
raise ValueError()
yield 'very oops'
@cherrypy.config(**{'request.show_tracebacks': False})
def cause_err_in_finalize(self):
# Since status must start with an int, this should error.
cherrypy.response.status = 'ZOO OK'
@cherrypy.config(**{'request.throw_errors': True})
def rethrow(self):
"""Test that an error raised here will be thrown out to
the server.
"""
raise ValueError()
class Expect(Test):
def expectation_failed(self):
expect = cherrypy.request.headers.elements('Expect')
if expect and expect[0].value != '100-continue':
raise cherrypy.HTTPError(400)
raise cherrypy.HTTPError(417, 'Expectation Failed')
class Headers(Test):
def default(self, headername):
"""Spit back out the value for the requested header."""
return cherrypy.request.headers[headername]
def doubledheaders(self):
# From https://github.com/cherrypy/cherrypy/issues/165:
# "header field names should not be case sensitive sayes the
# rfc. if i set a headerfield in complete lowercase i end up
# with two header fields, one in lowercase, the other in
# mixed-case."
# Set the most common headers
hMap = cherrypy.response.headers
hMap['content-type'] = 'text/html'
hMap['content-length'] = 18
hMap['server'] = 'CherryPy headertest'
hMap['location'] = ('%s://%s:%s/headers/'
% (cherrypy.request.local.ip,
cherrypy.request.local.port,
cherrypy.request.scheme))
# Set a rare header for fun
hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT'
return 'double header test'
def ifmatch(self):
val = cherrypy.request.headers['If-Match']
assert isinstance(val, six.text_type)
cherrypy.response.headers['ETag'] = val
return val
class HeaderElements(Test):
def get_elements(self, headername):
e = cherrypy.request.headers.elements(headername)
return '\n'.join([six.text_type(x) for x in e])
class Method(Test):
def index(self):
m = cherrypy.request.method
if m in defined_http_methods or m == 'CONNECT':
return m
if m == 'LINK':
raise cherrypy.HTTPError(405)
else:
raise cherrypy.HTTPError(501)
def parameterized(self, data):
return data
def request_body(self):
# This should be a file object (temp file),
# which CP will just pipe back out if we tell it to.
return cherrypy.request.body
def reachable(self):
return 'success'
class Divorce(Test):
"""HTTP Method handlers shouldn't collide with normal method names.
For example, a GET-handler shouldn't collide with a method named
'get'.
If you build HTTP method dispatching into CherryPy, rewrite this
class to use your new dispatch mechanism and make sure that:
"GET /divorce HTTP/1.1" maps to divorce.index() and
"GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get()
"""
documents = {}
@cherrypy.expose
def index(self):
yield '<h1>Choose your document</h1>\n'
yield '<ul>\n'
for id, contents in self.documents.items():
yield (
" <li><a href='/divorce/get?ID=%s'>%s</a>:"
' %s</li>\n' % (id, id, contents))
yield '</ul>'
@cherrypy.expose
def get(self, ID):
return ('Divorce document %s: %s' %
(ID, self.documents.get(ID, 'empty')))
class ThreadLocal(Test):
def index(self):
existing = repr(getattr(cherrypy.request, 'asdf', None))
cherrypy.request.asdf = 'rassfrassin'
return existing
appconf = {
'/method': {
'request.methods_with_bodies': ('POST', 'PUT', 'PROPFIND',
'PATCH')
},
}
cherrypy.tree.mount(root, config=appconf)
def test_scheme(self):
self.getPage('/scheme')
self.assertBody(self.scheme)
def test_per_request_uuid4(self):
self.getPage('/request_uuid4')
first_uuid4, _, second_uuid4 = self.body.decode().partition(' ')
assert (
uuid.UUID(first_uuid4, version=4)
== uuid.UUID(second_uuid4, version=4)
)
self.getPage('/request_uuid4')
third_uuid4, _, _ = self.body.decode().partition(' ')
assert (
uuid.UUID(first_uuid4, version=4)
!= uuid.UUID(third_uuid4, version=4)
)
def testRelativeURIPathInfo(self):
self.getPage('/pathinfo/foo/bar')
self.assertBody('/pathinfo/foo/bar')
def testAbsoluteURIPathInfo(self):
# http://cherrypy.org/ticket/1061
self.getPage('http://localhost/pathinfo/foo/bar')
self.assertBody('/pathinfo/foo/bar')
def testParams(self):
self.getPage('/params/?thing=a')
self.assertBody(repr(ntou('a')))
self.getPage('/params/?thing=a&thing=b&thing=c')
self.assertBody(repr([ntou('a'), ntou('b'), ntou('c')]))
# Test friendly error message when given params are not accepted.
cherrypy.config.update({'request.show_mismatched_params': True})
self.getPage('/params/?notathing=meeting')
self.assertInBody('Missing parameters: thing')
self.getPage('/params/?thing=meeting&notathing=meeting')
self.assertInBody('Unexpected query string parameters: notathing')
# Test ability to turn off friendly error messages
cherrypy.config.update({'request.show_mismatched_params': False})
self.getPage('/params/?notathing=meeting')
self.assertInBody('Not Found')
self.getPage('/params/?thing=meeting&notathing=meeting')
self.assertInBody('Not Found')
# Test "% HEX HEX"-encoded URL, param keys, and values
self.getPage('/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville')
self.assertBody('args: %s kwargs: %s' %
(('\xd4 \xe3', 'cheese'),
[('Gruy\xe8re', ntou('Bulgn\xe9ville'))]))
# Make sure that encoded = and & get parsed correctly
self.getPage(
'/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2')
self.assertBody('args: %s kwargs: %s' %
(('code',),
[('url', ntou('http://cherrypy.org/index?a=1&b=2'))]))
# Test coordinates sent by <img ismap>
self.getPage('/params/ismap?223,114')
self.assertBody('Coordinates: 223, 114')
# Test "name[key]" dict-like params
self.getPage('/params/dictlike?a[1]=1&a[2]=2&b=foo&b[bar]=baz')
self.assertBody('args: %s kwargs: %s' %
(('dictlike',),
[('a[1]', ntou('1')), ('a[2]', ntou('2')),
('b', ntou('foo')), ('b[bar]', ntou('baz'))]))
def testParamErrors(self):
# test that all of the handlers work when given
# the correct parameters in order to ensure that the
# errors below aren't coming from some other source.
for uri in (
'/paramerrors/one_positional?param1=foo',
'/paramerrors/one_positional_args?param1=foo',
'/paramerrors/one_positional_args/foo',
'/paramerrors/one_positional_args/foo/bar/baz',
'/paramerrors/one_positional_args_kwargs?'
'param1=foo&param2=bar',
'/paramerrors/one_positional_args_kwargs/foo?'
'param2=bar&param3=baz',
'/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
'param2=bar&param3=baz',
'/paramerrors/one_positional_kwargs?'
'param1=foo&param2=bar&param3=baz',
'/paramerrors/one_positional_kwargs/foo?'
'param4=foo&param2=bar&param3=baz',
'/paramerrors/no_positional',
'/paramerrors/no_positional_args/foo',
'/paramerrors/no_positional_args/foo/bar/baz',
'/paramerrors/no_positional_args_kwargs?param1=foo&param2=bar',
'/paramerrors/no_positional_args_kwargs/foo?param2=bar',
'/paramerrors/no_positional_args_kwargs/foo/bar/baz?'
'param2=bar&param3=baz',
'/paramerrors/no_positional_kwargs?param1=foo&param2=bar',
'/paramerrors/callable_object',
):
self.getPage(uri)
self.assertStatus(200)
error_msgs = [
'Missing parameters',
'Nothing matches the given URI',
'Multiple values for parameters',
'Unexpected query string parameters',
'Unexpected body parameters',
'Invalid path in Request-URI',
'Illegal #fragment in Request-URI',
]
# uri should be tested for valid absolute path, the status must be 400.
for uri, error_idx in (
('invalid/path/without/leading/slash', 5),
('/valid/path#invalid=fragment', 6),
):
self.getPage(uri)
self.assertStatus(400)
self.assertInBody(error_msgs[error_idx])
# query string parameters are part of the URI, so if they are wrong
# for a particular handler, the status MUST be a 404.
for uri, msg in (
('/paramerrors/one_positional', error_msgs[0]),
('/paramerrors/one_positional?foo=foo', error_msgs[0]),
('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]),
('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]),
('/paramerrors/one_positional/foo?param1=foo&param2=foo',
error_msgs[2]),
('/paramerrors/one_positional_args/foo?param1=foo&param2=foo',
error_msgs[2]),
('/paramerrors/one_positional_args/foo/bar/baz?param2=foo',
error_msgs[3]),
('/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
'param1=bar&param3=baz',
error_msgs[2]),
('/paramerrors/one_positional_kwargs/foo?'
'param1=foo&param2=bar&param3=baz',
error_msgs[2]),
('/paramerrors/no_positional/boo', error_msgs[1]),
('/paramerrors/no_positional?param1=foo', error_msgs[3]),
('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]),
('/paramerrors/no_positional_kwargs/boo?param1=foo',
error_msgs[1]),
('/paramerrors/callable_object?param1=foo', error_msgs[3]),
('/paramerrors/callable_object/boo', error_msgs[1]),
):
for show_mismatched_params in (True, False):
cherrypy.config.update(
{'request.show_mismatched_params': show_mismatched_params})
self.getPage(uri)
self.assertStatus(404)
if show_mismatched_params:
self.assertInBody(msg)
else:
self.assertInBody('Not Found')
# if body parameters are wrong, a 400 must be returned.
for uri, body, msg in (
('/paramerrors/one_positional/foo',
'param1=foo', error_msgs[2]),
('/paramerrors/one_positional/foo',
'param1=foo&param2=foo', error_msgs[2]),
('/paramerrors/one_positional_args/foo',
'param1=foo&param2=foo', error_msgs[2]),
('/paramerrors/one_positional_args/foo/bar/baz',
'param2=foo', error_msgs[4]),
('/paramerrors/one_positional_args_kwargs/foo/bar/baz',
'param1=bar&param3=baz', error_msgs[2]),
('/paramerrors/one_positional_kwargs/foo',
'param1=foo&param2=bar&param3=baz', error_msgs[2]),
('/paramerrors/no_positional', 'param1=foo', error_msgs[4]),
('/paramerrors/no_positional_args/boo',
'param1=foo', error_msgs[4]),
('/paramerrors/callable_object', 'param1=foo', error_msgs[4]),
):
for show_mismatched_params in (True, False):
cherrypy.config.update(
{'request.show_mismatched_params': show_mismatched_params})
self.getPage(uri, method='POST', body=body)
self.assertStatus(400)
if show_mismatched_params:
self.assertInBody(msg)
else:
self.assertInBody('400 Bad')
# even if body parameters are wrong, if we get the uri wrong, then
# it's a 404
for uri, body, msg in (
('/paramerrors/one_positional?param2=foo',
'param1=foo', error_msgs[3]),
('/paramerrors/one_positional/foo/bar',
'param2=foo', error_msgs[1]),
('/paramerrors/one_positional_args/foo/bar?param2=foo',
'param3=foo', error_msgs[3]),
('/paramerrors/one_positional_kwargs/foo/bar',
'param2=bar&param3=baz', error_msgs[1]),
('/paramerrors/no_positional?param1=foo',
'param2=foo', error_msgs[3]),
('/paramerrors/no_positional_args/boo?param2=foo',
'param1=foo', error_msgs[3]),
('/paramerrors/callable_object?param2=bar',
'param1=foo', error_msgs[3]),
):
for show_mismatched_params in (True, False):
cherrypy.config.update(
{'request.show_mismatched_params': show_mismatched_params})
self.getPage(uri, method='POST', body=body)
self.assertStatus(404)
if show_mismatched_params:
self.assertInBody(msg)
else:
self.assertInBody('Not Found')
# In the case that a handler raises a TypeError we should
# let that type error through.
for uri in (
'/paramerrors/raise_type_error',
'/paramerrors/raise_type_error_with_default_param?x=0',
'/paramerrors/raise_type_error_with_default_param?x=0&y=0',
'/paramerrors/raise_type_error_decorated',
):
self.getPage(uri, method='GET')
self.assertStatus(500)
self.assertTrue('Client Error', self.body)
def testErrorHandling(self):
self.getPage('/error/missing')
self.assertStatus(404)
self.assertErrorPage(404, "The path '/error/missing' was not found.")
ignore = helper.webtest.ignored_exceptions
ignore.append(ValueError)
try:
valerr = '\n raise ValueError()\nValueError'
self.getPage('/error/page_method')
self.assertErrorPage(500, pattern=valerr)
self.getPage('/error/page_yield')
self.assertErrorPage(500, pattern=valerr)
if (cherrypy.server.protocol_version == 'HTTP/1.0' or
getattr(cherrypy.server, 'using_apache', False)):
self.getPage('/error/page_streamed')
# Because this error is raised after the response body has
# started, the status should not change to an error status.
self.assertStatus(200)
self.assertBody('word up')
else:
# Under HTTP/1.1, the chunked transfer-coding is used.
# The HTTP client will choke when the output is incomplete.
self.assertRaises((ValueError, IncompleteRead), self.getPage,
'/error/page_streamed')
# No traceback should be present
self.getPage('/error/cause_err_in_finalize')
msg = "Illegal response status from server ('ZOO' is non-numeric)."
self.assertErrorPage(500, msg, None)
finally:
ignore.pop()
# Test HTTPError with a reason-phrase in the status arg.
self.getPage('/error/reason_phrase')
self.assertStatus("410 Gone fishin'")
# Test custom error page for a specific error.
self.getPage('/error/custom')
self.assertStatus(404)
self.assertBody('Hello, world\r\n' + (' ' * 499))
# Test custom error page for a specific error.
self.getPage('/error/custom?err=401')
self.assertStatus(401)
self.assertBody(
'Error 401 Unauthorized - '
"Well, I'm very sorry but you haven't paid!")
# Test default custom error page.
self.getPage('/error/custom_default')
self.assertStatus(500)
self.assertBody(
'Error 500 Internal Server Error - '
"Well, I'm very sorry but you haven't paid!".ljust(513))
# Test error in custom error page (ticket #305).
# Note that the message is escaped for HTML (ticket #310).
self.getPage('/error/noexist')
self.assertStatus(404)
if sys.version_info >= (3, 3):
exc_name = 'FileNotFoundError'
else:
exc_name = 'IOError'
msg = ('No, &lt;b&gt;really&lt;/b&gt;, not found!<br />'
'In addition, the custom error page failed:\n<br />'
'%s: [Errno 2] '
"No such file or directory: 'nonexistent.html'") % (exc_name,)
self.assertInBody(msg)
if getattr(cherrypy.server, 'using_apache', False):
pass
else:
# Test throw_errors (ticket #186).
self.getPage('/error/rethrow')
self.assertInBody('raise ValueError()')
def testExpect(self):
e = ('Expect', '100-continue')
self.getPage('/headerelements/get_elements?headername=Expect', [e])
self.assertBody('100-continue')
self.getPage('/expect/expectation_failed', [e])
self.assertStatus(417)
def testHeaderElements(self):
# Accept-* header elements should be sorted, with most preferred first.
h = [('Accept', 'audio/*; q=0.2, audio/basic')]
self.getPage('/headerelements/get_elements?headername=Accept', h)
self.assertStatus(200)
self.assertBody('audio/basic\n'
'audio/*;q=0.2')
h = [
('Accept',
'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')
]
self.getPage('/headerelements/get_elements?headername=Accept', h)
self.assertStatus(200)
self.assertBody('text/x-c\n'
'text/html\n'
'text/x-dvi;q=0.8\n'
'text/plain;q=0.5')
# Test that more specific media ranges get priority.
h = [('Accept', 'text/*, text/html, text/html;level=1, */*')]
self.getPage('/headerelements/get_elements?headername=Accept', h)
self.assertStatus(200)
self.assertBody('text/html;level=1\n'
'text/html\n'
'text/*\n'
'*/*')
# Test Accept-Charset
h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')]
self.getPage(
'/headerelements/get_elements?headername=Accept-Charset', h)
self.assertStatus('200 OK')
self.assertBody('iso-8859-5\n'
'unicode-1-1;q=0.8')
# Test Accept-Encoding
h = [('Accept-Encoding', 'gzip;q=1.0, identity; q=0.5, *;q=0')]
self.getPage(
'/headerelements/get_elements?headername=Accept-Encoding', h)
self.assertStatus('200 OK')
self.assertBody('gzip;q=1.0\n'
'identity;q=0.5\n'
'*;q=0')
# Test Accept-Language
h = [('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7')]
self.getPage(
'/headerelements/get_elements?headername=Accept-Language', h)
self.assertStatus('200 OK')
self.assertBody('da\n'
'en-gb;q=0.8\n'
'en;q=0.7')
# Test malformed header parsing. See
# https://github.com/cherrypy/cherrypy/issues/763.
self.getPage('/headerelements/get_elements?headername=Content-Type',
# Note the illegal trailing ";"
headers=[('Content-Type', 'text/html; charset=utf-8;')])
self.assertStatus(200)
self.assertBody('text/html;charset=utf-8')
def test_repeated_headers(self):
# Test that two request headers are collapsed into one.
# See https://github.com/cherrypy/cherrypy/issues/542.
self.getPage('/headers/Accept-Charset',
headers=[('Accept-Charset', 'iso-8859-5'),
('Accept-Charset', 'unicode-1-1;q=0.8')])
self.assertBody('iso-8859-5, unicode-1-1;q=0.8')
# Tests that each header only appears once, regardless of case.
self.getPage('/headers/doubledheaders')
self.assertBody('double header test')
hnames = [name.title() for name, val in self.headers]
for key in ['Content-Length', 'Content-Type', 'Date',
'Expires', 'Location', 'Server']:
self.assertEqual(hnames.count(key), 1, self.headers)
def test_encoded_headers(self):
# First, make sure the innards work like expected.
self.assertEqual(
httputil.decode_TEXT(ntou('=?utf-8?q?f=C3=BCr?=')), ntou('f\xfcr'))
if cherrypy.server.protocol_version == 'HTTP/1.1':
# Test RFC-2047-encoded request and response header values
u = ntou('\u212bngstr\xf6m', 'escape')
c = ntou('=E2=84=ABngstr=C3=B6m')
self.getPage('/headers/ifmatch',
[('If-Match', ntou('=?utf-8?q?%s?=') % c)])
# The body should be utf-8 encoded.
self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m')
# But the Etag header should be RFC-2047 encoded (binary)
self.assertHeader('ETag', ntou('=?utf-8?b?4oSrbmdzdHLDtm0=?='))
# Test a *LONG* RFC-2047-encoded request and response header value
self.getPage('/headers/ifmatch',
[('If-Match', ntou('=?utf-8?q?%s?=') % (c * 10))])
self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m' * 10)
# Note: this is different output for Python3, but it decodes fine.
etag = self.assertHeader(
'ETag',
'=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
'4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
'4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
'4oSrbmdzdHLDtm0=?=')
self.assertEqual(httputil.decode_TEXT(etag), u * 10)
def test_header_presence(self):
# If we don't pass a Content-Type header, it should not be present
# in cherrypy.request.headers
self.getPage('/headers/Content-Type',
headers=[])
self.assertStatus(500)
# If Content-Type is present in the request, it should be present in
# cherrypy.request.headers
self.getPage('/headers/Content-Type',
headers=[('Content-type', 'application/json')])
self.assertBody('application/json')
def test_basic_HTTPMethods(self):
helper.webtest.methods_with_bodies = ('POST', 'PUT', 'PROPFIND',
'PATCH')
# Test that all defined HTTP methods work.
for m in defined_http_methods:
self.getPage('/method/', method=m)
# HEAD requests should not return any body.
if m == 'HEAD':
self.assertBody('')
elif m == 'TRACE':
# Some HTTP servers (like modpy) have their own TRACE support
self.assertEqual(self.body[:5], b'TRACE')
else:
self.assertBody(m)
# test of PATCH requests
# Request a PATCH method with a form-urlencoded body
self.getPage('/method/parameterized', method='PATCH',
body='data=on+top+of+other+things')
self.assertBody('on top of other things')
# Request a PATCH method with a file body
b = 'one thing on top of another'
h = [('Content-Type', 'text/plain'),
('Content-Length', str(len(b)))]
self.getPage('/method/request_body', headers=h, method='PATCH', body=b)
self.assertStatus(200)
self.assertBody(b)
# Request a PATCH method with a file body but no Content-Type.
# See https://github.com/cherrypy/cherrypy/issues/790.
b = b'one thing on top of another'
self.persistent = True
try:
conn = self.HTTP_CONN
conn.putrequest('PATCH', '/method/request_body', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Length', str(len(b)))
conn.endheaders()
conn.send(b)
response = conn.response_class(conn.sock, method='PATCH')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(b)
finally:
self.persistent = False
# Request a PATCH method with no body whatsoever (not an empty one).
# See https://github.com/cherrypy/cherrypy/issues/650.
# Provide a C-T or webtest will provide one (and a C-L) for us.
h = [('Content-Type', 'text/plain')]
self.getPage('/method/reachable', headers=h, method='PATCH')
self.assertStatus(411)
# HTTP PUT tests
# Request a PUT method with a form-urlencoded body
self.getPage('/method/parameterized', method='PUT',
body='data=on+top+of+other+things')
self.assertBody('on top of other things')
# Request a PUT method with a file body
b = 'one thing on top of another'
h = [('Content-Type', 'text/plain'),
('Content-Length', str(len(b)))]
self.getPage('/method/request_body', headers=h, method='PUT', body=b)
self.assertStatus(200)
self.assertBody(b)
# Request a PUT method with a file body but no Content-Type.
# See https://github.com/cherrypy/cherrypy/issues/790.
b = b'one thing on top of another'
self.persistent = True
try:
conn = self.HTTP_CONN
conn.putrequest('PUT', '/method/request_body', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Length', str(len(b)))
conn.endheaders()
conn.send(b)
response = conn.response_class(conn.sock, method='PUT')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(b)
finally:
self.persistent = False
# Request a PUT method with no body whatsoever (not an empty one).
# See https://github.com/cherrypy/cherrypy/issues/650.
# Provide a C-T or webtest will provide one (and a C-L) for us.
h = [('Content-Type', 'text/plain')]
self.getPage('/method/reachable', headers=h, method='PUT')
self.assertStatus(411)
# Request a custom method with a request body
b = ('<?xml version="1.0" encoding="utf-8" ?>\n\n'
'<propfind xmlns="DAV:"><prop><getlastmodified/>'
'</prop></propfind>')
h = [('Content-Type', 'text/xml'),
('Content-Length', str(len(b)))]
self.getPage('/method/request_body', headers=h,
method='PROPFIND', body=b)
self.assertStatus(200)
self.assertBody(b)
# Request a disallowed method
self.getPage('/method/', method='LINK')
self.assertStatus(405)
# Request an unknown method
self.getPage('/method/', method='SEARCH')
self.assertStatus(501)
# For method dispatchers: make sure that an HTTP method doesn't
# collide with a virtual path atom. If you build HTTP-method
# dispatching into the core, rewrite these handlers to use
# your dispatch idioms.
self.getPage('/divorce/get?ID=13')
self.assertBody('Divorce document 13: empty')
self.assertStatus(200)
self.getPage('/divorce/', method='GET')
self.assertBody('<h1>Choose your document</h1>\n<ul>\n</ul>')
self.assertStatus(200)
def test_CONNECT_method(self):
self.persistent = True
try:
conn = self.HTTP_CONN
conn.request('CONNECT', 'created.example.com:3128')
response = conn.response_class(conn.sock, method='CONNECT')
response.begin()
self.assertEqual(response.status, 204)
finally:
self.persistent = False
self.persistent = True
try:
conn = self.HTTP_CONN
conn.request('CONNECT', 'body.example.com:3128')
response = conn.response_class(conn.sock, method='CONNECT')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(b'CONNECTed to /body.example.com:3128')
finally:
self.persistent = False
def test_CONNECT_method_invalid_authority(self):
for request_target in ['example.com', 'http://example.com:33',
'/path/', 'path/', '/?q=f', '#f']:
self.persistent = True
try:
conn = self.HTTP_CONN
conn.request('CONNECT', request_target)
response = conn.response_class(conn.sock, method='CONNECT')
response.begin()
self.assertEqual(response.status, 400)
self.body = response.read()
self.assertBody(b'Invalid path in Request-URI: request-target '
b'must match authority-form.')
finally:
self.persistent = False
def testEmptyThreadlocals(self):
results = []
for x in range(20):
self.getPage('/threadlocal/')
results.append(self.body)
self.assertEqual(results, [b'None'] * 20)

View file

@ -0,0 +1,80 @@
"""Test Routes dispatcher."""
import os
import importlib
import pytest
import cherrypy
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
class RoutesDispatchTest(helper.CPWebCase):
"""Routes dispatcher test suite."""
@staticmethod
def setup_server():
"""Set up cherrypy test instance."""
try:
importlib.import_module('routes')
except ImportError:
pytest.skip('Install routes to test RoutesDispatcher code')
class Dummy:
def index(self):
return 'I said good day!'
class City:
def __init__(self, name):
self.name = name
self.population = 10000
@cherrypy.config(**{
'tools.response_headers.on': True,
'tools.response_headers.headers': [
('Content-Language', 'en-GB'),
],
})
def index(self, **kwargs):
return 'Welcome to %s, pop. %s' % (self.name, self.population)
def update(self, **kwargs):
self.population = kwargs['pop']
return 'OK'
d = cherrypy.dispatch.RoutesDispatcher()
d.connect(action='index', name='hounslow', route='/hounslow',
controller=City('Hounslow'))
d.connect(
name='surbiton', route='/surbiton', controller=City('Surbiton'),
action='index', conditions=dict(method=['GET']))
d.mapper.connect('/surbiton', controller='surbiton',
action='update', conditions=dict(method=['POST']))
d.connect('main', ':action', controller=Dummy())
conf = {'/': {'request.dispatch': d}}
cherrypy.tree.mount(root=None, config=conf)
def test_Routes_Dispatch(self):
"""Check that routes package based URI dispatching works correctly."""
self.getPage('/hounslow')
self.assertStatus('200 OK')
self.assertBody('Welcome to Hounslow, pop. 10000')
self.getPage('/foo')
self.assertStatus('404 Not Found')
self.getPage('/surbiton')
self.assertStatus('200 OK')
self.assertBody('Welcome to Surbiton, pop. 10000')
self.getPage('/surbiton', method='POST', body='pop=1327')
self.assertStatus('200 OK')
self.assertBody('OK')
self.getPage('/surbiton')
self.assertStatus('200 OK')
self.assertHeader('Content-Language', 'en-GB')
self.assertBody('Welcome to Surbiton, pop. 1327')

View file

@ -0,0 +1,512 @@
import os
import threading
import time
import socket
import importlib
from six.moves.http_client import HTTPConnection
import pytest
from path import Path
import cherrypy
from cherrypy._cpcompat import (
json_decode,
HTTPSConnection,
)
from cherrypy.lib import sessions
from cherrypy.lib import reprconf
from cherrypy.lib.httputil import response_codes
from cherrypy.test import helper
localDir = os.path.dirname(__file__)
def http_methods_allowed(methods=['GET', 'HEAD']):
method = cherrypy.request.method.upper()
if method not in methods:
cherrypy.response.headers['Allow'] = ', '.join(methods)
raise cherrypy.HTTPError(405)
cherrypy.tools.allow = cherrypy.Tool('on_start_resource', http_methods_allowed)
def setup_server():
@cherrypy.config(**{
'tools.sessions.on': True,
'tools.sessions.storage_class': sessions.RamSession,
'tools.sessions.storage_path': localDir,
'tools.sessions.timeout': (1.0 / 60),
'tools.sessions.clean_freq': (1.0 / 60),
})
class Root:
@cherrypy.expose
def clear(self):
cherrypy.session.cache.clear()
@cherrypy.expose
def data(self):
cherrypy.session['aha'] = 'foo'
return repr(cherrypy.session._data)
@cherrypy.expose
def testGen(self):
counter = cherrypy.session.get('counter', 0) + 1
cherrypy.session['counter'] = counter
yield str(counter)
@cherrypy.expose
def testStr(self):
counter = cherrypy.session.get('counter', 0) + 1
cherrypy.session['counter'] = counter
return str(counter)
@cherrypy.expose
@cherrypy.config(**{'tools.sessions.on': False})
def set_session_cls(self, new_cls_name):
new_cls = reprconf.attributes(new_cls_name)
cfg = {'tools.sessions.storage_class': new_cls}
self.__class__._cp_config.update(cfg)
if hasattr(cherrypy, 'session'):
del cherrypy.session
if new_cls.clean_thread:
new_cls.clean_thread.stop()
new_cls.clean_thread.unsubscribe()
del new_cls.clean_thread
@cherrypy.expose
def index(self):
sess = cherrypy.session
c = sess.get('counter', 0) + 1
time.sleep(0.01)
sess['counter'] = c
return str(c)
@cherrypy.expose
def keyin(self, key):
return str(key in cherrypy.session)
@cherrypy.expose
def delete(self):
cherrypy.session.delete()
sessions.expire()
return 'done'
@cherrypy.expose
def delkey(self, key):
del cherrypy.session[key]
return 'OK'
@cherrypy.expose
def redir_target(self):
return self._cp_config['tools.sessions.storage_class'].__name__
@cherrypy.expose
def iredir(self):
raise cherrypy.InternalRedirect('/redir_target')
@cherrypy.expose
@cherrypy.config(**{
'tools.allow.on': True,
'tools.allow.methods': ['GET'],
})
def restricted(self):
return cherrypy.request.method
@cherrypy.expose
def regen(self):
cherrypy.tools.sessions.regenerate()
return 'logged in'
@cherrypy.expose
def length(self):
return str(len(cherrypy.session))
@cherrypy.expose
@cherrypy.config(**{
'tools.sessions.path': '/session_cookie',
'tools.sessions.name': 'temp',
'tools.sessions.persistent': False,
})
def session_cookie(self):
# Must load() to start the clean thread.
cherrypy.session.load()
return cherrypy.session.id
cherrypy.tree.mount(Root())
class SessionTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def tearDown(self):
# Clean up sessions.
for fname in os.listdir(localDir):
if fname.startswith(sessions.FileSession.SESSION_PREFIX):
path = Path(localDir) / fname
path.remove_p()
@pytest.mark.xfail(reason='#1534')
def test_0_Session(self):
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
self.getPage('/clear')
# Test that a normal request gets the same id in the cookies.
# Note: this wouldn't work if /data didn't load the session.
self.getPage('/data')
self.assertBody("{'aha': 'foo'}")
c = self.cookies[0]
self.getPage('/data', self.cookies)
self.assertEqual(self.cookies[0], c)
self.getPage('/testStr')
self.assertBody('1')
cookie_parts = dict([p.strip().split('=')
for p in self.cookies[0][1].split(';')])
# Assert there is an 'expires' param
self.assertEqual(set(cookie_parts.keys()),
set(['session_id', 'expires', 'Path']))
self.getPage('/testGen', self.cookies)
self.assertBody('2')
self.getPage('/testStr', self.cookies)
self.assertBody('3')
self.getPage('/data', self.cookies)
self.assertDictEqual(json_decode(self.body),
{'counter': 3, 'aha': 'foo'})
self.getPage('/length', self.cookies)
self.assertBody('2')
self.getPage('/delkey?key=counter', self.cookies)
self.assertStatus(200)
self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
self.getPage('/testStr')
self.assertBody('1')
self.getPage('/testGen', self.cookies)
self.assertBody('2')
self.getPage('/testStr', self.cookies)
self.assertBody('3')
self.getPage('/delkey?key=counter', self.cookies)
self.assertStatus(200)
# Wait for the session.timeout (1 second)
time.sleep(2)
self.getPage('/')
self.assertBody('1')
self.getPage('/length', self.cookies)
self.assertBody('1')
# Test session __contains__
self.getPage('/keyin?key=counter', self.cookies)
self.assertBody('True')
cookieset1 = self.cookies
# Make a new session and test __len__ again
self.getPage('/')
self.getPage('/length', self.cookies)
self.assertBody('2')
# Test session delete
self.getPage('/delete', self.cookies)
self.assertBody('done')
self.getPage('/delete', cookieset1)
self.assertBody('done')
def f():
return [
x
for x in os.listdir(localDir)
if x.startswith('session-')
]
self.assertEqual(f(), [])
# Wait for the cleanup thread to delete remaining session files
self.getPage('/')
self.assertNotEqual(f(), [])
time.sleep(2)
self.assertEqual(f(), [])
def test_1_Ram_Concurrency(self):
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
self._test_Concurrency()
@pytest.mark.xfail(reason='#1306')
def test_2_File_Concurrency(self):
self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
self._test_Concurrency()
def _test_Concurrency(self):
client_thread_count = 5
request_count = 30
# Get initial cookie
self.getPage('/')
self.assertBody('1')
cookies = self.cookies
data_dict = {}
errors = []
def request(index):
if self.scheme == 'https':
c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
else:
c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
for i in range(request_count):
c.putrequest('GET', '/')
for k, v in cookies:
c.putheader(k, v)
c.endheaders()
response = c.getresponse()
body = response.read()
if response.status != 200 or not body.isdigit():
errors.append((response.status, body))
else:
data_dict[index] = max(data_dict[index], int(body))
# Uncomment the following line to prove threads overlap.
# sys.stdout.write("%d " % index)
# Start <request_count> requests from each of
# <client_thread_count> concurrent clients
ts = []
for c in range(client_thread_count):
data_dict[c] = 0
t = threading.Thread(target=request, args=(c,))
ts.append(t)
t.start()
for t in ts:
t.join()
hitcount = max(data_dict.values())
expected = 1 + (client_thread_count * request_count)
for e in errors:
print(e)
self.assertEqual(hitcount, expected)
def test_3_Redirect(self):
# Start a new session
self.getPage('/testStr')
self.getPage('/iredir', self.cookies)
self.assertBody('FileSession')
def test_4_File_deletion(self):
# Start a new session
self.getPage('/testStr')
# Delete the session file manually and retry.
id = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
path = os.path.join(localDir, 'session-' + id)
os.unlink(path)
self.getPage('/testStr', self.cookies)
def test_5_Error_paths(self):
self.getPage('/unknown/page')
self.assertErrorPage(404, "The path '/unknown/page' was not found.")
# Note: this path is *not* the same as above. The above
# takes a normal route through the session code; this one
# skips the session code's before_handler and only calls
# before_finalize (save) and on_end (close). So the session
# code has to survive calling save/close without init.
self.getPage('/restricted', self.cookies, method='POST')
self.assertErrorPage(405, response_codes[405][1])
def test_6_regenerate(self):
self.getPage('/testStr')
# grab the cookie ID
id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
self.getPage('/regen')
self.assertBody('logged in')
id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
self.assertNotEqual(id1, id2)
self.getPage('/testStr')
# grab the cookie ID
id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
self.getPage('/testStr',
headers=[
('Cookie',
'session_id=maliciousid; '
'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')])
id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
self.assertNotEqual(id1, id2)
self.assertNotEqual(id2, 'maliciousid')
def test_7_session_cookies(self):
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
self.getPage('/clear')
self.getPage('/session_cookie')
# grab the cookie ID
cookie_parts = dict([p.strip().split('=')
for p in self.cookies[0][1].split(';')])
# Assert there is no 'expires' param
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
id1 = cookie_parts['temp']
self.assertEqual(list(sessions.RamSession.cache), [id1])
# Send another request in the same "browser session".
self.getPage('/session_cookie', self.cookies)
cookie_parts = dict([p.strip().split('=')
for p in self.cookies[0][1].split(';')])
# Assert there is no 'expires' param
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
self.assertBody(id1)
self.assertEqual(list(sessions.RamSession.cache), [id1])
# Simulate a browser close by just not sending the cookies
self.getPage('/session_cookie')
# grab the cookie ID
cookie_parts = dict([p.strip().split('=')
for p in self.cookies[0][1].split(';')])
# Assert there is no 'expires' param
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
# Assert a new id has been generated...
id2 = cookie_parts['temp']
self.assertNotEqual(id1, id2)
self.assertEqual(set(sessions.RamSession.cache.keys()),
set([id1, id2]))
# Wait for the session.timeout on both sessions
time.sleep(2.5)
cache = list(sessions.RamSession.cache)
if cache:
if cache == [id2]:
self.fail('The second session did not time out.')
else:
self.fail('Unknown session id in cache: %r', cache)
def test_8_Ram_Cleanup(self):
def lock():
s1 = sessions.RamSession()
s1.acquire_lock()
time.sleep(1)
s1.release_lock()
t = threading.Thread(target=lock)
t.start()
start = time.time()
while not sessions.RamSession.locks and time.time() - start < 5:
time.sleep(0.01)
assert len(sessions.RamSession.locks) == 1, 'Lock not acquired'
s2 = sessions.RamSession()
s2.clean_up()
msg = 'Clean up should not remove active lock'
assert len(sessions.RamSession.locks) == 1, msg
t.join()
try:
importlib.import_module('memcache')
host, port = '127.0.0.1', 11211
for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
s = None
try:
s = socket.socket(af, socktype, proto)
# See http://groups.google.com/group/cherrypy-users/
# browse_frm/thread/bbfe5eb39c904fe0
s.settimeout(1.0)
s.connect((host, port))
s.close()
except socket.error:
if s:
s.close()
raise
break
except (ImportError, socket.error):
class MemcachedSessionTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test(self):
return self.skip('memcached not reachable ')
else:
class MemcachedSessionTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_0_Session(self):
self.getPage('/set_session_cls/cherrypy.Sessions.MemcachedSession')
self.getPage('/testStr')
self.assertBody('1')
self.getPage('/testGen', self.cookies)
self.assertBody('2')
self.getPage('/testStr', self.cookies)
self.assertBody('3')
self.getPage('/length', self.cookies)
self.assertErrorPage(500)
self.assertInBody('NotImplementedError')
self.getPage('/delkey?key=counter', self.cookies)
self.assertStatus(200)
# Wait for the session.timeout (1 second)
time.sleep(1.25)
self.getPage('/')
self.assertBody('1')
# Test session __contains__
self.getPage('/keyin?key=counter', self.cookies)
self.assertBody('True')
# Test session delete
self.getPage('/delete', self.cookies)
self.assertBody('done')
def test_1_Concurrency(self):
client_thread_count = 5
request_count = 30
# Get initial cookie
self.getPage('/')
self.assertBody('1')
cookies = self.cookies
data_dict = {}
def request(index):
for i in range(request_count):
self.getPage('/', cookies)
# Uncomment the following line to prove threads overlap.
# sys.stdout.write("%d " % index)
if not self.body.isdigit():
self.fail(self.body)
data_dict[index] = int(self.body)
# Start <request_count> concurrent requests from
# each of <client_thread_count> clients
ts = []
for c in range(client_thread_count):
data_dict[c] = 0
t = threading.Thread(target=request, args=(c,))
ts.append(t)
t.start()
for t in ts:
t.join()
hitcount = max(data_dict.values())
expected = 1 + (client_thread_count * request_count)
self.assertEqual(hitcount, expected)
def test_3_Redirect(self):
# Start a new session
self.getPage('/testStr')
self.getPage('/iredir', self.cookies)
self.assertBody('memcached')
def test_5_Error_paths(self):
self.getPage('/unknown/page')
self.assertErrorPage(
404, "The path '/unknown/page' was not found.")
# Note: this path is *not* the same as above. The above
# takes a normal route through the session code; this one
# skips the session code's before_handler and only calls
# before_finalize (save) and on_end (close). So the session
# code has to survive calling save/close without init.
self.getPage('/restricted', self.cookies, method='POST')
self.assertErrorPage(405, response_codes[405][1])

View file

@ -0,0 +1,61 @@
import cherrypy
from cherrypy.test import helper
class SessionAuthenticateTest(helper.CPWebCase):
@staticmethod
def setup_server():
def check(username, password):
# Dummy check_username_and_password function
if username != 'test' or password != 'password':
return 'Wrong login/password'
def augment_params():
# A simple tool to add some things to request.params
# This is to check to make sure that session_auth can handle
# request params (ticket #780)
cherrypy.request.params['test'] = 'test'
cherrypy.tools.augment_params = cherrypy.Tool(
'before_handler', augment_params, None, priority=30)
class Test:
_cp_config = {
'tools.sessions.on': True,
'tools.session_auth.on': True,
'tools.session_auth.check_username_and_password': check,
'tools.augment_params.on': True,
}
@cherrypy.expose
def index(self, **kwargs):
return 'Hi %s, you are logged in' % cherrypy.request.login
cherrypy.tree.mount(Test())
def testSessionAuthenticate(self):
# request a page and check for login form
self.getPage('/')
self.assertInBody('<form method="post" action="do_login">')
# setup credentials
login_body = 'username=test&password=password&from_page=/'
# attempt a login
self.getPage('/do_login', method='POST', body=login_body)
self.assertStatus((302, 303))
# get the page now that we are logged in
self.getPage('/', self.cookies)
self.assertBody('Hi test, you are logged in')
# do a logout
self.getPage('/do_logout', self.cookies, method='POST')
self.assertStatus((302, 303))
# verify we are logged out
self.getPage('/', self.cookies)
self.assertInBody('<form method="post" action="do_login">')

Some files were not shown because too many files have changed in this diff Show more