1
0
mirror of https://github.com/Tygs/0bin.git synced 2023-08-10 21:13:00 +03:00

Porting zerobin to python 3

This commit is contained in:
sametmax
2015-05-10 19:19:02 +02:00
parent 391df055f9
commit 9b84122414
137 changed files with 22928 additions and 4370 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
Copyright (c) 2004-2011, CherryPy Team (team@cherrypy.org)
Copyright (c) 2004-2015, CherryPy Team (team@cherrypy.org)
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,

View File

@ -53,11 +53,10 @@ with customized or extended components. The core API's are:
* Server API
* WSGI API
These API's are described in the CherryPy specification:
http://www.cherrypy.org/wiki/CherryPySpec
These API's are described in the `CherryPy specification <https://bitbucket.org/cherrypy/cherrypy/wiki/CherryPySpec>`_.
"""
__version__ = "3.2.2"
__version__ = "3.7.0"
from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
from cherrypy._cpcompat import basestring, unicodestr, set
@ -94,21 +93,22 @@ except ImportError:
engine.listeners['before_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:
@ -125,6 +125,24 @@ engine.thread_manager.subscribe()
engine.signal_handler = process.plugins.SignalHandler(engine)
class _HandleSignalsPlugin(object):
"""Handle signals from other processes based on the configured
platform handlers above."""
def __init__(self, bus):
self.bus = bus
def subscribe(self):
"""Add the handlers based on the platform"""
if hasattr(self.bus, "signal_handler"):
self.bus.signal_handler.subscribe()
if hasattr(self.bus, "console_control_handler"):
self.bus.console_control_handler.subscribe()
engine.signals = _HandleSignalsPlugin(engine)
from cherrypy import _cpserver
server = _cpserver.Server()
server.subscribe()
@ -132,7 +150,7 @@ server.subscribe()
def quickstart(root=None, script_name="", config=None):
"""Mount the given root, start the builtin server (and engine), then block.
root: an instance of a "controller class" (a collection of page handler
methods) which represents the root of the application.
script_name: a string containing the "mount point" of the application.
@ -140,7 +158,7 @@ def quickstart(root=None, script_name="", config=None):
at which to mount the given root. For example, if root.index() will
handle requests to "http://www.example.com:8080/dept/app1/", then
the script_name argument would be "/dept/app1".
It MUST NOT end in a slash. If the script_name refers to the root
of the URI, it MUST be an empty string (not "/").
config: a file or dict containing application config. If this contains
@ -149,23 +167,21 @@ def quickstart(root=None, script_name="", config=None):
"""
if config:
_global_conf_alias.update(config)
tree.mount(root, script_name, config)
if hasattr(engine, "signal_handler"):
engine.signal_handler.subscribe()
if hasattr(engine, "console_control_handler"):
engine.console_control_handler.subscribe()
engine.signals.subscribe()
engine.start()
engine.block()
from cherrypy._cpcompat import threadlocal as _local
class _Serving(_local):
"""An interface for registering request and response objects.
Rather than have a separate "thread local" object for the request and
the response, this class works as a single threadlocal container for
both objects (and any others which developers wish to define). In this
@ -173,22 +189,22 @@ class _Serving(_local):
conversation, yet still refer to them as module-level globals in a
thread-safe way.
"""
request = _cprequest.Request(_httputil.Host("127.0.0.1", 80),
_httputil.Host("127.0.0.1", 1111))
"""
The request object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None."""
response = _cprequest.Response()
"""
The response object for the current thread. In the main thread,
and any threads which are not receiving HTTP requests, this is None."""
def load(self, request, response):
self.request = request
self.response = response
def clear(self):
"""Remove all attributes of self."""
self.__dict__.clear()
@ -197,54 +213,54 @@ serving = _Serving()
class _ThreadLocalProxy(object):
__slots__ = ['__attrname__', '__dict__']
def __init__(self, attrname):
self.__attrname__ = attrname
def __getattr__(self, name):
child = getattr(serving, self.__attrname__)
return getattr(child, name)
def __setattr__(self, name, value):
if name in ("__attrname__", ):
object.__setattr__(self, name, value)
else:
child = getattr(serving, self.__attrname__)
setattr(child, name, value)
def __delattr__(self, name):
child = getattr(serving, self.__attrname__)
delattr(child, name)
def _get_dict(self):
child = getattr(serving, self.__attrname__)
d = child.__class__.__dict__.copy()
d.update(child.__dict__)
return d
__dict__ = property(_get_dict)
def __getitem__(self, key):
child = getattr(serving, self.__attrname__)
return child[key]
def __setitem__(self, key, value):
child = getattr(serving, self.__attrname__)
child[key] = value
def __delitem__(self, key):
child = getattr(serving, self.__attrname__)
del child[key]
def __contains__(self, key):
child = getattr(serving, self.__attrname__)
return key in child
def __len__(self):
child = getattr(serving, self.__attrname__)
return len(child)
def __nonzero__(self):
child = getattr(serving, self.__attrname__)
return bool(child)
@ -258,7 +274,10 @@ request = _ThreadLocalProxy('request')
response = _ThreadLocalProxy('response')
# Create thread_data object as a thread-specific all-purpose storage
class _ThreadData(_local):
"""A container for thread-specific data."""
thread_data = _ThreadData()
@ -283,27 +302,32 @@ except ImportError:
from cherrypy import _cplogging
class _GlobalLogManager(_cplogging.LogManager):
"""A site-wide LogManager; routes to app.log or global log as appropriate.
This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
cherrypy.log() and cherrypy.log.access(). If either
function is called during a request, the message will be sent to the
logger for the current Application. If they are called outside of a
request, the message will be sent to the site-wide logger.
"""
def __call__(self, *args, **kwargs):
"""Log the given message to the app.log or global log as appropriate."""
# Do NOT use try/except here. See http://www.cherrypy.org/ticket/945
"""Log the given message to the app.log or global log as appropriate.
"""
# Do NOT use try/except here. See
# https://bitbucket.org/cherrypy/cherrypy/issue/945
if hasattr(request, 'app') and hasattr(request.app, 'log'):
log = request.app.log
else:
log = self
return log.error(*args, **kwargs)
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 as appropriate.
"""
try:
return request.app.log.access()
except AttributeError:
@ -317,6 +341,7 @@ log.error_file = ''
# Using an access file makes CP about 10% slower. Leave off by default.
log.access_file = ''
def _buslog(msg, level):
log.error(msg, 'ENGINE', severity=level)
engine.subscribe('log', _buslog)
@ -335,8 +360,9 @@ def expose(func=None, alias=None):
for a in alias:
parents[a.replace(".", "_")] = func
return func
import sys, types
import sys
import types
if isinstance(func, (types.FunctionType, types.MethodType)):
if alias is None:
# @expose
@ -363,24 +389,25 @@ def expose(func=None, alias=None):
alias = func
return expose_
def popargs(*args, **kwargs):
"""A decorator for _cp_dispatch
"""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
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
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
@ -389,9 +416,9 @@ def popargs(*args, **kwargs):
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:
@ -399,122 +426,123 @@ def popargs(*args, **kwargs):
#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.
# 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():
for k, v in kwargs.items():
if k == 'handler':
handler = v
else:
raise TypeError(
"cherrypy.popargs() got an unexpected keyword argument '{0}'" \
"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)):
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
# 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
# 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 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).
@ -527,7 +555,7 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
qs = _urlencode(qs)
if qs:
qs = '?' + qs
if request.app:
if not path.startswith("/"):
# Append/remove trailing slash from path_info as needed
@ -540,17 +568,17 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
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).
@ -559,10 +587,10 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
# 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 = []
@ -574,12 +602,12 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
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
@ -599,7 +627,7 @@ def url(path="", qs="", script_name=None, base=None, relative=None):
new.pop(0)
new = (['..'] * len(old)) + new
newurl = '/'.join(new)
return newurl
@ -613,7 +641,7 @@ config.defaults = {
'tools.log_headers.on': True,
'tools.trailing_slash.on': True,
'tools.encode.on': True
}
}
config.namespaces["log"] = lambda k, v: setattr(log, k, v)
config.namespaces["checker"] = lambda k, v: setattr(checker, k, v)
# Must reset to get our defaults applied.

View File

@ -0,0 +1,4 @@
import cherrypy.daemon
if __name__ == '__main__':
cherrypy.daemon.run()

View File

@ -6,26 +6,26 @@ from cherrypy._cpcompat import iteritems, copykeys, builtins
class Checker(object):
"""A checker for CherryPy sites and their mounted applications.
When this object is called at engine startup, it executes each
of its own methods whose names start with ``check_``. If you wish
to disable selected checks, simply add a line in your global
config which sets the appropriate method to False::
[global]
checker.check_skipped_app_config = False
You may also dynamically add or replace ``check_*`` methods in this way.
"""
on = True
"""If True (the default), run all checks; if False, turn off all checks."""
def __init__(self):
self._populate_known_types()
def __call__(self):
"""Run all check_* methods."""
if self.on:
@ -39,16 +39,17 @@ class Checker(object):
method()
finally:
warnings.formatwarning = oldformatwarning
def formatwarning(self, message, category, filename, lineno, line=None):
"""Function to format a warning."""
return "CherryPy Checker:\n%s\n\n" % message
# This value should be set inside _cpconfig.
global_config_contained_paths = False
def check_app_config_entries_dont_start_with_script_name(self):
"""Check for Application config with sections that repeat script_name."""
"""Check for Application config with sections that repeat script_name.
"""
for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application):
continue
@ -61,30 +62,33 @@ class Checker(object):
key_atoms = key.strip("/").split("/")
if key_atoms[:len(sn_atoms)] == sn_atoms:
warnings.warn(
"The application mounted at %r has config " \
"entries that start with its script name: %r" % (sn, key))
"The application mounted at %r has config "
"entries that start with its script name: %r" % (sn,
key))
def check_site_config_entries_in_app_config(self):
"""Check for mounted Applications that have site-scoped config."""
for sn, app in iteritems(cherrypy.tree.apps):
if not isinstance(app, cherrypy.Application):
continue
msg = []
for section, entries in iteritems(app.config):
if section.startswith('/'):
for key, value in iteritems(entries):
for n in ("engine.", "server.", "tree.", "checker."):
if key.startswith(n):
msg.append("[%s] %s = %s" % (section, key, value))
msg.append("[%s] %s = %s" %
(section, key, value))
if msg:
msg.insert(0,
"The application mounted at %r contains the following "
"config entries, which are only allowed in site-wide "
"config. Move them to a [global] section and pass them "
"to cherrypy.config.update() instead of tree.mount()." % sn)
"The application mounted at %r contains the "
"following config entries, which are only allowed "
"in site-wide config. Move them to a [global] "
"section and pass them to cherrypy.config.update() "
"instead of tree.mount()." % sn)
warnings.warn(os.linesep.join(msg))
def check_skipped_app_config(self):
"""Check for mounted Applications that have no config."""
for sn, app in cherrypy.tree.apps.items():
@ -100,9 +104,11 @@ class Checker(object):
"cherrypy.tree.mount(..., config=app_config)")
warnings.warn(msg)
return
def check_app_config_brackets(self):
"""Check for Application config with extraneous brackets in section names."""
"""Check for Application config with extraneous brackets in section
names.
"""
for sn, app in cherrypy.tree.apps.items():
if not isinstance(app, cherrypy.Application):
continue
@ -111,11 +117,11 @@ class Checker(object):
for key in app.config.keys():
if key.startswith("[") or key.endswith("]"):
warnings.warn(
"The application mounted at %r has config " \
"The application mounted at %r has config "
"section names with extraneous brackets: %r. "
"Config *files* need brackets; config *dicts* "
"(e.g. passed to tree.mount) do not." % (sn, key))
def check_static_paths(self):
"""Check Application config for incorrect static paths."""
# Use the dummy Request object in the main thread.
@ -128,7 +134,7 @@ class Checker(object):
# get_resource will populate request.config
request.get_resource(section + "/dummy.html")
conf = request.config.get
if conf("tools.staticdir.on", False):
msg = ""
root = conf("tools.staticdir.root")
@ -144,30 +150,32 @@ class Checker(object):
"though a root is provided.")
testdir = os.path.join(root, dir[1:])
if os.path.exists(testdir):
msg += ("\nIf you meant to serve the "
"filesystem folder at %r, remove "
"the leading slash from dir." % testdir)
msg += (
"\nIf you meant to serve the "
"filesystem folder at %r, remove the "
"leading slash from dir." % (testdir,))
else:
if not root:
msg = "dir is a relative path and no root provided."
msg = (
"dir is a relative path and "
"no root provided.")
else:
fulldir = os.path.join(root, dir)
if not os.path.isabs(fulldir):
msg = "%r is not an absolute path." % fulldir
msg = ("%r is not an absolute path." % (
fulldir,))
if fulldir and not os.path.exists(fulldir):
if msg:
msg += "\n"
msg += ("%r (root + dir) is not an existing "
"filesystem path." % fulldir)
if msg:
warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r"
% (msg, section, root, dir))
# -------------------------- Compatibility -------------------------- #
obsolete = {
'server.default_content_type': 'tools.response_headers.headers',
'log_access_file': 'log.access_file',
@ -180,10 +188,10 @@ class Checker(object):
'throw_errors': 'request.throw_errors',
'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
'cherrypy.Application(Root())))'),
}
}
deprecated = {}
def _compat(self, config):
"""Process config and warn on each obsolete or deprecated entry."""
for section, conf in config.items():
@ -204,7 +212,7 @@ class Checker(object):
elif section in self.deprecated:
warnings.warn("%r is deprecated. Use %r instead."
% (section, self.deprecated[section]))
def check_compatibility(self):
"""Process config and warn on each obsolete or deprecated entry."""
self._compat(cherrypy.config)
@ -212,12 +220,10 @@ class Checker(object):
if not isinstance(app, cherrypy.Application):
continue
self._compat(app.config)
# ------------------------ Known Namespaces ------------------------ #
extra_config_namespaces = []
def _known_ns(self, app):
ns = ["wsgi"]
ns.extend(copykeys(app.toolboxes))
@ -225,7 +231,7 @@ class Checker(object):
ns.extend(copykeys(app.request_class.namespaces))
ns.extend(copykeys(cherrypy.config.namespaces))
ns += self.extra_config_namespaces
for section, conf in app.config.items():
is_path_section = section.startswith("/")
if is_path_section and isinstance(conf, dict):
@ -235,22 +241,26 @@ class Checker(object):
if atoms[0] not in ns:
# Spit out a special warning if a known
# namespace is preceded by "cherrypy."
if (atoms[0] == "cherrypy" and atoms[1] in ns):
msg = ("The config entry %r is invalid; "
"try %r instead.\nsection: [%s]"
% (k, ".".join(atoms[1:]), section))
if atoms[0] == "cherrypy" and atoms[1] in ns:
msg = (
"The config entry %r is invalid; "
"try %r instead.\nsection: [%s]"
% (k, ".".join(atoms[1:]), section))
else:
msg = ("The config entry %r is invalid, because "
"the %r config namespace is unknown.\n"
"section: [%s]" % (k, atoms[0], section))
msg = (
"The config entry %r is invalid, "
"because the %r config namespace "
"is unknown.\n"
"section: [%s]" % (k, atoms[0], section))
warnings.warn(msg)
elif atoms[0] == "tools":
if atoms[1] not in dir(cherrypy.tools):
msg = ("The config entry %r may be invalid, "
"because the %r tool was not found.\n"
"section: [%s]" % (k, atoms[1], section))
msg = (
"The config entry %r may be invalid, "
"because the %r tool was not found.\n"
"section: [%s]" % (k, atoms[1], section))
warnings.warn(msg)
def check_config_namespaces(self):
"""Process config and warn on each unknown config namespace."""
for sn, app in cherrypy.tree.apps.items():
@ -258,17 +268,13 @@ class Checker(object):
continue
self._known_ns(app)
# -------------------------- Config Types -------------------------- #
known_config_types = {}
def _populate_known_types(self):
b = [x for x in vars(builtins).values()
if type(x) is type(str)]
def traverse(obj, namespace):
for name in dir(obj):
# Hack for 3.2's warning about body_params
@ -277,17 +283,17 @@ class Checker(object):
vtype = type(getattr(obj, name, None))
if vtype in b:
self.known_config_types[namespace + "." + name] = vtype
traverse(cherrypy.request, "request")
traverse(cherrypy.response, "response")
traverse(cherrypy.server, "server")
traverse(cherrypy.engine, "engine")
traverse(cherrypy.log, "log")
def _known_types(self, config):
msg = ("The config entry %r in section %r is of type %r, "
"which does not match the expected type %r.")
for section, conf in config.items():
if isinstance(conf, dict):
for k, v in conf.items():
@ -305,7 +311,7 @@ class Checker(object):
if expected_type and vtype != expected_type:
warnings.warn(msg % (k, section, vtype.__name__,
expected_type.__name__))
def check_config_types(self):
"""Assert that config values are of the same type as default values."""
self._known_types(cherrypy.config)
@ -313,15 +319,14 @@ class Checker(object):
if not isinstance(app, cherrypy.Application):
continue
self._known_types(app.config)
# -------------------- Specific config warnings -------------------- #
def check_localhost(self):
"""Warn if any socket_host is 'localhost'. See #711."""
for k, v in cherrypy.config.items():
if k == 'server.socket_host' and v == 'localhost':
warnings.warn("The use of 'localhost' as a socket host can "
"cause problems on newer systems, since 'localhost' can "
"map to either an IPv4 or an IPv6 address. You should "
"use '127.0.0.1' or '[::1]' instead.")
"cause problems on newer systems, since "
"'localhost' can map to either an IPv4 or an "
"IPv6 address. You should use '127.0.0.1' "
"or '[::1]' instead.")

View File

@ -18,6 +18,7 @@ output.
import os
import re
import sys
import threading
if sys.version_info >= (3, 0):
py3k = True
@ -25,14 +26,23 @@ if sys.version_info >= (3, 0):
unicodestr = str
nativestr = unicodestr
basestring = (bytes, str)
def ntob(n, encoding='ISO-8859-1'):
"""Return the given native string as a byte string in the given encoding."""
"""Return the given native string as a byte string in the given
encoding.
"""
assert_native(n)
# In Python 3, the native string type is unicode
return n.encode(encoding)
def ntou(n, encoding='ISO-8859-1'):
"""Return the given native string as a unicode string with the given encoding."""
"""Return the given native string as a unicode string with the given
encoding.
"""
assert_native(n)
# In Python 3, the native string type is unicode
return n
def tonative(n, encoding='ISO-8859-1'):
"""Return the given string as a native string in the given encoding."""
# In Python 3, the native string type is unicode
@ -50,27 +60,36 @@ else:
unicodestr = unicode
nativestr = bytestr
basestring = basestring
def ntob(n, encoding='ISO-8859-1'):
"""Return the given native string as a byte string in the given encoding."""
"""Return the given native string as a byte string in the given
encoding.
"""
assert_native(n)
# In Python 2, the native string type is bytes. Assume it's already
# in the given encoding, which for ISO-8859-1 is almost always what
# was intended.
return n
def ntou(n, encoding='ISO-8859-1'):
"""Return the given native string as a unicode string with the given encoding."""
"""Return the given native string as a unicode string with the given
encoding.
"""
assert_native(n)
# In Python 2, the native string type is bytes.
# First, check for the special encoding 'escape'. The test suite uses this
# to signal that it wants to pass a string with embedded \uXXXX escapes,
# but without having to prefix it with u'' for Python 2, but no prefix
# for Python 3.
# First, check for the special encoding 'escape'. The test suite uses
# this to signal that it wants to pass a string with embedded \uXXXX
# escapes, but without having to prefix it with u'' for Python 2,
# but no prefix for Python 3.
if encoding == 'escape':
return unicode(
re.sub(r'\\u([0-9a-zA-Z]{4})',
lambda m: unichr(int(m.group(1), 16)),
n.decode('ISO-8859-1')))
# Assume it's already in the given encoding, which for ISO-8859-1 is almost
# always what was intended.
# Assume it's already in the given encoding, which for ISO-8859-1
# is almost always what was intended.
return n.decode(encoding)
def tonative(n, encoding='ISO-8859-1'):
"""Return the given string as a native string in the given encoding."""
# In Python 2, the native string type is bytes.
@ -86,6 +105,11 @@ else:
# bytes:
BytesIO = StringIO
def assert_native(n):
if not isinstance(n, nativestr):
raise TypeError("n must be a native str (got %s)" % type(n).__name__)
try:
set = set
except NameError:
@ -96,10 +120,11 @@ try:
from base64 import decodebytes as _base64_decodebytes
except ImportError:
# Python 3.0-
# since CherryPy claims compatibility with Python 2.3, we must use
# 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):
@ -198,28 +223,31 @@ except ImportError:
import __builtin__ as builtins
try:
# Python 2. We have to do it in this order so Python 2 builds
# 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, HTTPSConnection, IncompleteRead, NotConnected
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, HTTPSConnection, IncompleteRead, NotConnected
from http.client import BadStatusLine, HTTPConnection, IncompleteRead
from http.client import NotConnected
from http.server import BaseHTTPRequestHandler
try:
# Python 2. We have to do it in this order so Python 2 builds
# don't try to import the 'http' module from cherrypy.lib
from httplib import HTTPSConnection
except ImportError:
# Some platforms don't expose HTTPSConnection, so handle it separately
if py3k:
try:
# Python 3
from http.client import HTTPSConnection
except ImportError:
# Some platforms which don't have SSL don't expose HTTPSConnection
HTTPSConnection = None
else:
try:
from httplib import HTTPSConnection
except ImportError:
HTTPSConnection = None
try:
# Python 2
@ -233,16 +261,19 @@ 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:
@ -251,40 +282,49 @@ except ImportError:
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)
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.
# Prefer simplejson, which is usually more advanced than the builtin
# module.
import simplejson as json
json_decode = json.JSONDecoder().decode
json_encode = json.JSONEncoder().iterencode
_json_encode = json.JSONEncoder().iterencode
except ImportError:
if py3k:
# Python 3.0: json is part of the standard library,
# but outputs unicode. We need bytes.
if sys.version_info >= (2, 6):
# 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')
elif sys.version_info >= (2, 6):
# 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')
json_encode = _json_encode
try:
import cPickle as pickle
@ -296,11 +336,13 @@ except ImportError:
try:
os.urandom(20)
import binascii
def random20():
return binascii.hexlify(os.urandom(20)).decode('ascii')
except (AttributeError, NotImplementedError):
import random
# os.urandom not available until Python 2.4. Fall back to random.random.
def random20():
return sha('%s' % random.random()).hexdigest()
@ -316,3 +358,26 @@ except NameError:
# Python 2
def next(i):
return i.next()
if sys.version_info >= (3, 3):
Timer = threading.Timer
Event = threading.Event
else:
# Python 3.2 and earlier
Timer = threading._Timer
Event = threading._Event
# Prior to Python 2.6, the Thread class did not have a .daemon property.
# This mix-in adds that property.
class SetDaemonProperty:
def __get_daemon(self):
return self.isDaemon()
def __set_daemon(self, daemon):
self.setDaemon(daemon)
if sys.version_info < (2, 6):
daemon = property(__get_daemon, __set_daemon)

File diff suppressed because it is too large Load Diff

View File

@ -50,14 +50,14 @@ attribute. For example::
class Demo:
_cp_config = {'tools.gzip.on': True}
def index(self):
return "Hello world"
index.exposed = True
index._cp_config = {'request.show_tracebacks': False}
.. note::
This behavior is only guaranteed for the default dispatcher.
Other dispatchers may have different restrictions on where
you can attach _cp_config attributes.
@ -125,15 +125,16 @@ from cherrypy.lib import reprconf
# Deprecated in CherryPy 3.2--remove in 3.3
NamespaceSet = reprconf.NamespaceSet
def merge(base, other):
"""Merge one app config (from a dict, file, or filename) into another.
If the given config is a filename, it will be appended to
the list of files to monitor for "autoreload" changes.
"""
if isinstance(other, basestring):
cherrypy.engine.autoreload.files.add(other)
# Load other into base
for section, value_map in reprconf.as_dict(other).items():
if not isinstance(value_map, dict):
@ -146,6 +147,7 @@ def merge(base, other):
class Config(reprconf.Config):
"""The 'global' configuration data for the entire CherryPy process."""
def update(self, config):
@ -157,20 +159,21 @@ class Config(reprconf.Config):
def _apply(self, config):
"""Update self from a dict."""
if isinstance(config.get("global", None), dict):
if isinstance(config.get("global"), dict):
if len(config) > 1:
cherrypy.checker.global_config_contained_paths = True
config = config["global"]
if 'tools.staticdir.dir' in config:
config['tools.staticdir.section'] = "global"
reprconf.Config._apply(self, 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.")
def tool_decorator(f):
if not hasattr(f, "_cp_config"):
f._cp_config = {}
@ -180,25 +183,26 @@ class Config(reprconf.Config):
return tool_decorator
# Sphinx begin config.environments
Config.environments = environments = {
"staging": {
'engine.autoreload_on': False,
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': False,
'request.show_mismatched_params': False,
},
},
"production": {
'engine.autoreload_on': False,
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': False,
'request.show_mismatched_params': False,
'log.screen': False,
},
},
"embedded": {
# For use with CherryPy embedded in another deployment stack.
'engine.autoreload_on': False,
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': False,
@ -206,16 +210,17 @@ Config.environments = environments = {
'log.screen': False,
'engine.SIGHUP': None,
'engine.SIGTERM': None,
},
},
"test_suite": {
'engine.autoreload_on': False,
'engine.autoreload.on': False,
'checker.on': False,
'tools.log_headers.on': False,
'request.show_tracebacks': True,
'request.show_mismatched_params': True,
'log.screen': False,
},
}
},
}
# Sphinx end config.environments
def _server_namespace_handler(k, v):
@ -226,14 +231,14 @@ def _server_namespace_handler(k, v):
# to configure additional HTTP servers.
if not hasattr(cherrypy, "servers"):
cherrypy.servers = {}
servername, k = atoms
if servername not in cherrypy.servers:
from cherrypy import _cpserver
cherrypy.servers[servername] = _cpserver.Server()
# On by default, but 'on = False' can unsubscribe it (see below).
cherrypy.servers[servername].subscribe()
if k == 'on':
if v:
cherrypy.servers[servername].subscribe()
@ -245,9 +250,24 @@ def _server_namespace_handler(k, v):
setattr(cherrypy.server, k, v)
Config.namespaces["server"] = _server_namespace_handler
def _engine_namespace_handler(k, v):
"""Backward compatibility handler for the "engine" namespace."""
engine = cherrypy.engine
deprecated = {
'autoreload_on': 'autoreload.on',
'autoreload_frequency': 'autoreload.frequency',
'autoreload_match': 'autoreload.match',
'reload_files': 'autoreload.files',
'deadlock_poll_freq': 'timeout_monitor.frequency'
}
if k in deprecated:
engine.log(
'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()
@ -272,7 +292,10 @@ def _engine_namespace_handler(k, v):
if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'):
plugin.subscribe()
return
elif (not v) and hasattr(getattr(plugin, 'unsubscribe', None), '__call__'):
elif (
(not v) and
hasattr(getattr(plugin, 'unsubscribe', None), '__call__')
):
plugin.unsubscribe()
return
setattr(plugin, attrname, v)
@ -286,10 +309,9 @@ def _tree_namespace_handler(k, v):
if isinstance(v, dict):
for script_name, app in v.items():
cherrypy.tree.graft(app, script_name)
cherrypy.engine.log("Mounted: %s on %s" % (app, script_name or "/"))
cherrypy.engine.log("Mounted: %s on %s" %
(app, script_name or "/"))
else:
cherrypy.tree.graft(v, v.script_name)
cherrypy.engine.log("Mounted: %s on %s" % (v, v.script_name or "/"))
Config.namespaces["tree"] = _tree_namespace_handler

View File

@ -22,13 +22,40 @@ from cherrypy._cpcompat import set
class PageHandler(object):
"""Callable which sets response.body."""
def __init__(self, callable, *args, **kwargs):
self.callable = callable
self.args = args
self.kwargs = kwargs
def get_args(self):
return cherrypy.serving.request.args
def set_args(self, args):
cherrypy.serving.request.args = args
return cherrypy.serving.request.args
args = property(
get_args,
set_args,
doc="The ordered args should be accessible from post dispatch hooks"
)
def get_kwargs(self):
return cherrypy.serving.request.kwargs
def set_kwargs(self, kwargs):
cherrypy.serving.request.kwargs = 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):
try:
return self.callable(*self.args, **self.kwargs)
@ -54,7 +81,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
2. Too little parameters are passed to the function.
There are 3 sources of parameters to a cherrypy handler.
1. query string parameters are passed as keyword parameters to the handler.
1. query string parameters are passed as keyword parameters to the
handler.
2. body parameters are also passed as keyword parameters.
3. when partial matching occurs, the final path atoms are passed as
positional args.
@ -65,12 +93,13 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
show_mismatched_params = getattr(
cherrypy.serving.request, 'show_mismatched_params', False)
try:
(args, varargs, varkw, defaults) = inspect.getargspec(callable)
(args, varargs, varkw, defaults) = getargspec(callable)
except TypeError:
if isinstance(callable, object) and hasattr(callable, '__call__'):
(args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__)
(args, varargs, varkw,
defaults) = getargspec(callable.__call__)
else:
# If it wasn't one of our own types, re-raise
# If it wasn't one of our own types, re-raise
# the original error
raise
@ -117,15 +146,15 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
# 2. not enough body parameters -> 400
# 3. not enough path parts (partial matches) -> 404
#
# We can't actually tell which case it is,
# We can't actually tell which case it is,
# so I'm raising a 404 because that covers 2/3 of the
# possibilities
#
#
# In the case where the method does not allow body
# arguments it's definitely a 404.
message = None
if show_mismatched_params:
message="Missing parameters: %s" % ",".join(missing_args)
message = "Missing parameters: %s" % ",".join(missing_args)
raise cherrypy.HTTPError(404, message=message)
# the extra positional arguments come from the path - 404 Not Found
@ -147,8 +176,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
message = None
if show_mismatched_params:
message="Multiple values for parameters: "\
"%s" % ",".join(multiple_args)
message = "Multiple values for parameters: "\
"%s" % ",".join(multiple_args)
raise cherrypy.HTTPError(error, message=message)
if not varkw and varkw_usage > 0:
@ -158,8 +187,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
if extra_qs_params:
message = None
if show_mismatched_params:
message="Unexpected query string "\
"parameters: %s" % ", ".join(extra_qs_params)
message = "Unexpected query string "\
"parameters: %s" % ", ".join(extra_qs_params)
raise cherrypy.HTTPError(404, message=message)
# If there were any extra body parameters, it's a 400 Not Found
@ -167,8 +196,8 @@ def test_callable_spec(callable, callable_args, callable_kwargs):
if extra_body_params:
message = None
if show_mismatched_params:
message="Unexpected body parameters: "\
"%s" % ", ".join(extra_body_params)
message = "Unexpected body parameters: "\
"%s" % ", ".join(extra_body_params)
raise cherrypy.HTTPError(400, message=message)
@ -176,10 +205,16 @@ try:
import inspect
except ImportError:
test_callable_spec = lambda callable, args, kwargs: None
else:
getargspec = inspect.getargspec
# Python 3 requires using getfullargspec if keyword-only arguments are present
if hasattr(inspect, 'getfullargspec'):
def getargspec(callable):
return inspect.getfullargspec(callable)[:4]
class LateParamPageHandler(PageHandler):
"""When passing cherrypy.request.params to the page handler, we do not
want to capture that dict too early; we want to give tools like the
decoding tool a chance to modify the params dict in-between the lookup
@ -187,16 +222,17 @@ class LateParamPageHandler(PageHandler):
takes that into account, and allows request.params to be 'bound late'
(it's more complicated than that, but that's the effect).
"""
def _get_kwargs(self):
kwargs = cherrypy.serving.request.params.copy()
if self._kwargs:
kwargs.update(self._kwargs)
return kwargs
def _set_kwargs(self, kwargs):
cherrypy.serving.request.kwargs = kwargs
self._kwargs = kwargs
kwargs = property(_get_kwargs, _set_kwargs,
doc='page handler kwargs (with '
'cherrypy.request.params copied in)')
@ -205,19 +241,24 @@ class LateParamPageHandler(PageHandler):
if sys.version_info < (3, 0):
punctuation_to_underscores = string.maketrans(
string.punctuation, '_' * len(string.punctuation))
def validate_translator(t):
if not isinstance(t, str) or len(t) != 256:
raise ValueError("The translate argument must be a str of len 256.")
raise ValueError(
"The translate argument must be a str of len 256.")
else:
punctuation_to_underscores = str.maketrans(
string.punctuation, '_' * len(string.punctuation))
def validate_translator(t):
if not isinstance(t, dict):
raise ValueError("The translate argument must be a dict.")
class Dispatcher(object):
"""CherryPy Dispatcher which walks a tree of objects to find a handler.
The tree is rooted at cherrypy.request.app.root, and each hierarchical
component in the path_info argument is matched to a corresponding nested
attribute of the root object. Matching handlers must have an 'exposed'
@ -225,16 +266,16 @@ class Dispatcher(object):
matches a URI which ends in a slash ("/"). The special method name
"default" may match a portion of the path_info (but only when no longer
substring of the path_info matches some other object).
This is the default, built-in dispatcher for CherryPy.
"""
dispatch_method_name = '_cp_dispatch'
"""
The name of the dispatch method that nodes may optionally implement
to provide their own dynamic dispatch algorithm.
"""
def __init__(self, dispatch_method_name=None,
translate=punctuation_to_underscores):
validate_translator(translate)
@ -246,27 +287,27 @@ class Dispatcher(object):
"""Set handler and config for the current request."""
request = cherrypy.serving.request
func, vpath = self.find_handler(path_info)
if func:
# Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace("%2F", "/") for x in vpath]
request.handler = LateParamPageHandler(func, *vpath)
else:
request.handler = cherrypy.NotFound()
def find_handler(self, path):
"""Return the appropriate page handler, plus any virtual path.
This will return two objects. The first will be a callable,
which can be used to generate page output. Any parameters from
the query string or request body will be sent to that callable
as keyword arguments.
The callable is found by traversing the application's tree,
starting from cherrypy.request.app.root, and matching path
components to successive objects in the tree. For example, the
URL "/path/to/handler" might return root.path.to.handler.
The second object returned will be a list of names which are
'virtual path' components: parts of the URL which are dynamic,
and were not used when looking up the handler.
@ -277,7 +318,7 @@ class Dispatcher(object):
app = request.app
root = app.root
dispatch_name = self.dispatch_method_name
# Get config for the root object/path.
fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
fullpath_len = len(fullpath)
@ -288,14 +329,14 @@ class Dispatcher(object):
if "/" in app.config:
nodeconf.update(app.config["/"])
object_trail = [['root', root, nodeconf, segleft]]
node = root
iternames = fullpath[:]
while iternames:
name = iternames[0]
# map to legal Python identifiers (e.g. replace '.' with '_')
objname = name.translate(self.translate)
nodeconf = {}
subnode = getattr(node, objname, None)
pre_len = len(iternames)
@ -304,31 +345,31 @@ class Dispatcher(object):
if dispatch and hasattr(dispatch, '__call__') and not \
getattr(dispatch, 'exposed', False) and \
pre_len > 1:
#Don't expose the hidden 'index' token to _cp_dispatch
#We skip this if pre_len == 1 since it makes no sense
#to call a dispatcher when we have no tokens left.
# Don't expose the hidden 'index' token to _cp_dispatch
# We skip this if pre_len == 1 since it makes no sense
# to call a dispatcher when we have no tokens left.
index_name = iternames.pop()
subnode = dispatch(vpath=iternames)
iternames.append(index_name)
else:
#We didn't find a path, but keep processing in case there
#is a default() handler.
# We didn't find a path, but keep processing in case there
# is a default() handler.
iternames.pop(0)
else:
#We found the path, remove the vpath entry
# We found the path, remove the vpath entry
iternames.pop(0)
segleft = len(iternames)
if segleft > pre_len:
#No path segment was removed. Raise an error.
# No path segment was removed. Raise an error.
raise cherrypy.CherryPyException(
"A vpath segment was added. Custom dispatchers may only "
+ "remove elements. While trying to process "
+ "{0} in {1}".format(name, fullpath)
)
)
elif segleft == pre_len:
#Assume that the handler used the current path segment, but
#did not pop it. This allows things like
#return getattr(self, vpath[0], None)
# Assume that the handler used the current path segment, but
# did not pop it. This allows things like
# return getattr(self, vpath[0], None)
iternames.pop(0)
segleft -= 1
node = subnode
@ -337,7 +378,7 @@ class Dispatcher(object):
# Get _cp_config attached to this node.
if hasattr(node, "_cp_config"):
nodeconf.update(node._cp_config)
# Mix in values from app.config for this path.
existing_len = fullpath_len - pre_len
if existing_len != 0:
@ -349,43 +390,47 @@ class Dispatcher(object):
curpath += '/' + seg
if curpath in app.config:
nodeconf.update(app.config[curpath])
object_trail.append([name, node, nodeconf, segleft])
def set_conf():
"""Collapse all object_trail config into cherrypy.request.config."""
"""Collapse all object_trail config into cherrypy.request.config.
"""
base = cherrypy.config.copy()
# Note that we merge the config from each node
# even if that node was None.
for name, obj, conf, segleft in object_trail:
base.update(conf)
if 'tools.staticdir.dir' in conf:
base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft])
base['tools.staticdir.section'] = '/' + \
'/'.join(fullpath[0:fullpath_len - segleft])
return base
# Try successive objects (reverse order)
num_candidates = len(object_trail) - 1
for i in range(num_candidates, -1, -1):
name, candidate, nodeconf, segleft = object_trail[i]
if candidate is None:
continue
# Try a "default" method on the current leaf.
if hasattr(candidate, "default"):
defhandler = candidate.default
if getattr(defhandler, 'exposed', False):
# Insert any extra _cp_config from the default handler.
conf = getattr(defhandler, "_cp_config", {})
object_trail.insert(i+1, ["default", defhandler, conf, segleft])
object_trail.insert(
i + 1, ["default", defhandler, conf, segleft])
request.config = set_conf()
# See http://www.cherrypy.org/ticket/613
# See https://bitbucket.org/cherrypy/cherrypy/issue/613
request.is_index = path.endswith("/")
return defhandler, fullpath[fullpath_len - segleft:-1]
# Uncomment the next line to restrict positional params to "default".
# Uncomment the next line to restrict positional params to
# "default".
# if i < num_candidates - 2: continue
# Try the current leaf.
if getattr(candidate, 'exposed', False):
request.config = set_conf()
@ -400,28 +445,29 @@ class Dispatcher(object):
# positional parameters (virtual paths).
request.is_index = False
return candidate, fullpath[fullpath_len - segleft:-1]
# We didn't find anything
request.config = set_conf()
return None, []
class MethodDispatcher(Dispatcher):
"""Additional dispatch based on cherrypy.request.method.upper().
Methods named GET, POST, etc will be called on an exposed class.
The method names must be all caps; the appropriate Allow header
will be output showing all capitalized method names as allowable
HTTP verbs.
Note that the containing class must be exposed, not the methods.
"""
def __call__(self, path_info):
"""Set handler and config for the current request."""
request = cherrypy.serving.request
resource, vpath = self.find_handler(path_info)
if resource:
# Set Allow header
avail = [m for m in dir(resource) if m.isupper()]
@ -429,7 +475,7 @@ class MethodDispatcher(Dispatcher):
avail.append("HEAD")
avail.sort()
cherrypy.serving.response.headers['Allow'] = ", ".join(avail)
# Find the subhandler
meth = request.method.upper()
func = getattr(resource, meth, None)
@ -439,7 +485,7 @@ class MethodDispatcher(Dispatcher):
# Grab any _cp_config on the subhandler.
if hasattr(func, "_cp_config"):
request.config.update(func._cp_config)
# Decode any leftover %2F in the virtual_path atoms.
vpath = [x.replace("%2F", "/") for x in vpath]
request.handler = LateParamPageHandler(func, *vpath)
@ -450,9 +496,10 @@ class MethodDispatcher(Dispatcher):
class RoutesDispatcher(object):
"""A Routes based dispatcher for CherryPy."""
def __init__(self, full_result=False):
def __init__(self, full_result=False, **mapper_options):
"""
Routes dispatcher
@ -463,16 +510,16 @@ class RoutesDispatcher(object):
import routes
self.full_result = full_result
self.controllers = {}
self.mapper = routes.Mapper()
self.mapper = routes.Mapper(**mapper_options)
self.mapper.controller_scan = self.controllers.keys
def connect(self, name, route, controller, **kwargs):
self.controllers[name] = controller
self.mapper.connect(name, route, controller=name, **kwargs)
def redirect(self, url):
raise cherrypy.HTTPRedirect(url)
def __call__(self, path_info):
"""Set handler and config for the current request."""
func = self.find_handler(path_info)
@ -480,13 +527,13 @@ class RoutesDispatcher(object):
cherrypy.serving.request.handler = LateParamPageHandler(func)
else:
cherrypy.serving.request.handler = cherrypy.NotFound()
def find_handler(self, path_info):
"""Find the right page handler, and set request.config."""
import routes
request = cherrypy.serving.request
config = routes.request_config()
config.mapper = self.mapper
if hasattr(request, 'wsgi_environ'):
@ -494,9 +541,9 @@ class RoutesDispatcher(object):
config.host = request.headers.get('Host', None)
config.protocol = request.scheme
config.redirect = self.redirect
result = self.mapper.match(path_info)
config.mapper_dict = result
params = {}
if result:
@ -505,23 +552,23 @@ class RoutesDispatcher(object):
params.pop('controller', None)
params.pop('action', None)
request.params.update(params)
# Get config for the root object/path.
request.config = base = cherrypy.config.copy()
curpath = ""
def merge(nodeconf):
if 'tools.staticdir.dir' in nodeconf:
nodeconf['tools.staticdir.section'] = curpath or "/"
base.update(nodeconf)
app = request.app
root = app.root
if hasattr(root, "_cp_config"):
merge(root._cp_config)
if "/" in app.config:
merge(app.config["/"])
# Mix in values from app.config.
atoms = [x for x in path_info.split("/") if x]
if atoms:
@ -532,7 +579,7 @@ class RoutesDispatcher(object):
curpath = "/".join((curpath, atom))
if curpath in app.config:
merge(app.config[curpath])
handler = None
if result:
controller = result.get('controller')
@ -543,64 +590,66 @@ class RoutesDispatcher(object):
# Get config from the controller.
if hasattr(controller, "_cp_config"):
merge(controller._cp_config)
action = result.get('action')
if action is not None:
handler = getattr(controller, action, None)
# Get config from the handler
if hasattr(handler, "_cp_config"):
# Get config from the handler
if hasattr(handler, "_cp_config"):
merge(handler._cp_config)
else:
handler = controller
# Do the last path atom here so it can
# override the controller's _cp_config.
if last:
curpath = "/".join((curpath, last))
if curpath in app.config:
merge(app.config[curpath])
return handler
def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
from cherrypy.lib import xmlrpcutil
def xmlrpc_dispatch(path_info):
path_info = xmlrpcutil.patched_path(path_info)
return next_dispatcher(path_info)
return xmlrpc_dispatch
def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains):
def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
**domains):
"""
Select a different handler based on the Host header.
This can be useful when running multiple sites within one CP server.
It allows several domains to point to different parts of a single
website structure. For example::
http://www.domain.example -> root
http://www.domain2.example -> root/domain2/
http://www.domain2.example:443 -> root/secure
can be accomplished via the following config::
[/]
request.dispatch = cherrypy.dispatch.VirtualHost(
**{'www.domain2.example': '/domain2',
'www.domain2.example:443': '/secure',
})
next_dispatcher
The next dispatcher object in the dispatch chain.
The VirtualHost dispatcher adds a prefix to the URL and calls
another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
use_x_forwarded_host
If True (the default), any "X-Forwarded-Host"
request header will be used instead of the "Host" header. This
is commonly added by HTTP servers (such as Apache) when proxying.
``**domains``
A dict of {host header value: virtual prefix} pairs.
The incoming "Host" request header is looked up in this dict,
@ -611,26 +660,27 @@ def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domai
headers may contain the port number.
"""
from cherrypy.lib import httputil
def vhost_dispatch(path_info):
request = cherrypy.serving.request
header = request.headers.get
domain = header('Host', '')
if use_x_forwarded_host:
domain = header("X-Forwarded-Host", domain)
prefix = domains.get(domain, "")
if prefix:
path_info = httputil.urljoin(prefix, path_info)
result = next_dispatcher(path_info)
# Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
# Touch up staticdir config. See
# https://bitbucket.org/cherrypy/cherrypy/issue/614.
section = request.config.get('tools.staticdir.section')
if section:
section = section[len(prefix):]
request.config['tools.staticdir.section'] = section
return result
return vhost_dispatch

View File

@ -2,8 +2,9 @@
CherryPy provides (and uses) exceptions for declaring that the HTTP response
should be a status other than the default "200 OK". You can ``raise`` them like
normal Python exceptions. You can also call them and they will raise themselves;
this means you can set an :class:`HTTPError<cherrypy._cperror.HTTPError>`
normal Python exceptions. You can also call them and they will raise
themselves; this means you can set an
:class:`HTTPError<cherrypy._cperror.HTTPError>`
or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the
:attr:`request.handler<cherrypy._cprequest.Request.handler>`.
@ -21,7 +22,8 @@ POST, however, is neither safe nor idempotent--if you
charge a credit card, you don't want to be charged twice by a redirect!
For this reason, *none* of the 3xx responses permit a user-agent (browser) to
resubmit a POST on redirection without first confirming the action with the user:
resubmit a POST on redirection without first confirming the action with the
user:
===== ================================= ===========
300 Multiple Choices Confirm with the user
@ -53,14 +55,16 @@ Anticipated HTTP responses
--------------------------
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 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
`string formatting <http://www.python.org/doc/2.6.4/library/stdtypes.html#string-formatting-operations>`_.
expected responses (like 404 Not Found). Supply a filename from which the
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
`string formatting <http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_.
::
_cp_config = {'error_page.404': os.path.join(localDir, "static/index.html")}
_cp_config = {
'error_page.404': os.path.join(localDir, "static/index.html")
}
Beginning in version 3.1, you may also provide a function or other callable as
@ -72,7 +76,8 @@ version arguments that are interpolated into templates::
cherrypy.config.update({'error_page.402': error_page_402})
Also in 3.1, in addition to the numbered error codes, you may also supply
"error_page.default" to handle all codes which do not have their own error_page entry.
"error_page.default" to handle all codes which do not have their own error_page
entry.
@ -81,8 +86,9 @@ Unanticipated errors
CherryPy also has a generic error handling mechanism: whenever an unanticipated
error occurs in your code, it will call
:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to set
the response status, headers, and body. By default, this is the same output as
:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to
set the response status, headers, and body. By default, this is the same
output as
:class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide
some other behavior, you generally replace "request.error_response".
@ -93,67 +99,78 @@ send an e-mail containing the error::
def handle_error():
cherrypy.response.status = 500
cherrypy.response.body = ["<html><body>Sorry, an error occured</body></html>"]
sendMail('error@domain.com', 'Error in your web app', _cperror.format_exc())
cherrypy.response.body = [
"<html><body>Sorry, an error occured</body></html>"
]
sendMail('error@domain.com',
'Error in your web app',
_cperror.format_exc())
class Root:
_cp_config = {'request.error_response': handle_error}
Note that you have to explicitly set :attr:`response.body <cherrypy._cprequest.Response.body>`
Note that you have to explicitly set
:attr:`response.body <cherrypy._cprequest.Response.body>`
and not simply return an error message as a result.
"""
from cgi import escape as _escape
from sys import exc_info as _exc_info
from traceback import format_exception as _format_exception
from cherrypy._cpcompat import basestring, bytestr, iteritems, ntob, tonative, urljoin as _urljoin
from cherrypy._cpcompat import basestring, bytestr, iteritems, ntob
from cherrypy._cpcompat import tonative, urljoin as _urljoin
from cherrypy.lib import httputil as _httputil
class CherryPyException(Exception):
"""A base class for CherryPy exceptions."""
pass
class TimeoutError(CherryPyException):
"""Exception raised when Response.timed_out is detected."""
pass
class InternalRedirect(CherryPyException):
"""Exception raised to switch to the handler for a different URL.
This exception will redirect processing to another path within the site
(without informing the client). Provide the new path as an argument when
raising the exception. Provide any params in the querystring for the new URL.
raising the exception. Provide any params in the querystring for the new
URL.
"""
def __init__(self, path, query_string=""):
import cherrypy
self.request = cherrypy.serving.request
self.query_string = query_string
if "?" in path:
# Separate any params included in the path
path, self.query_string = path.split("?", 1)
# Note that urljoin will "do the right thing" whether url is:
# 1. a URL relative to root (e.g. "/dummy")
# 2. a URL relative to the current path
# Note that any query string will be discarded.
path = _urljoin(self.request.path_info, path)
# Set a 'path' member attribute so that code which traps this
# error can have access to it.
self.path = path
CherryPyException.__init__(self, path, self.query_string)
class HTTPRedirect(CherryPyException):
"""Exception raised when the request should be redirected.
This exception will force a HTTP redirect to the URL or URL's you give it.
The new URL must be passed as the first argument to the Exception,
e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list.
@ -162,40 +179,40 @@ class HTTPRedirect(CherryPyException):
If one of the provided URL is a unicode object, it will be encoded
using the default encoding or the one passed in parameter.
There are multiple types of redirect, from which you can select via the
``status`` argument. If you do not provide a ``status`` arg, it defaults to
303 (or 302 if responding with HTTP/1.0).
Examples::
raise cherrypy.HTTPRedirect("")
raise cherrypy.HTTPRedirect("/abs/path", 307)
raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301)
See :ref:`redirectingpost` for additional caveats.
"""
status = None
"""The integer HTTP status code to emit."""
urls = None
"""The list of URL's to emit."""
encoding = 'utf-8'
"""The encoding when passed urls are not native strings"""
def __init__(self, urls, status=None, encoding=None):
import cherrypy
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:
# 1. a complete URL with host (e.g. "http://www.example.com/test")
# 2. a URL relative to root (e.g. "/dummy")
@ -204,7 +221,7 @@ class HTTPRedirect(CherryPyException):
url = _urljoin(cherrypy.url(), url)
abs_urls.append(url)
self.urls = abs_urls
# RFC 2616 indicates a 301 response code fits our goal; however,
# browser support for 301 is quite messy. Do 302/303 instead. See
# http://www.alanflavell.org.uk/www/post-redirect.html
@ -217,36 +234,40 @@ class HTTPRedirect(CherryPyException):
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)
def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent self.
"""Modify cherrypy.response status, headers, and body to represent
self.
CherryPy uses this internally, but you can also use it to create an
HTTPRedirect object and set its output without *raising* the exception.
"""
import cherrypy
response = cherrypy.serving.response
response.status = status = self.status
if status in (300, 301, 302, 303, 307):
response.headers['Content-Type'] = "text/html;charset=utf-8"
# "The ... URI SHOULD be given by the Location field
# in the response."
response.headers['Location'] = self.urls[0]
# "Unless the request method was HEAD, the entity of the response
# SHOULD contain a short hypertext note with a hyperlink to the
# new URI(s)."
msg = {300: "This resource can be found at <a href='%s'>%s</a>.",
301: "This resource has permanently moved to <a href='%s'>%s</a>.",
302: "This resource resides temporarily at <a href='%s'>%s</a>.",
303: "This resource can be found at <a href='%s'>%s</a>.",
307: "This resource has moved temporarily to <a href='%s'>%s</a>.",
}[status]
msgs = [msg % (u, u) for u in self.urls]
msg = {
300: "This resource can be found at ",
301: "This resource has permanently moved to ",
302: "This resource resides temporarily at ",
303: "This resource can be found at ",
307: "This resource has moved temporarily to ",
}[status]
msg += '<a href=%s>%s</a>.'
from xml.sax import saxutils
msgs = [msg % (saxutils.quoteattr(u), u) 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
# (allow finalize to set it).
@ -256,7 +277,7 @@ class HTTPRedirect(CherryPyException):
# "The response MUST include the following header fields:
# Date, unless its omission is required by section 14.18.1"
# The "Date" header should have been set in Response.__init__
# "...the response SHOULD NOT include other entity-headers."
for key in ('Allow', 'Content-Encoding', 'Content-Language',
'Content-Length', 'Content-Location', 'Content-MD5',
@ -264,7 +285,7 @@ class HTTPRedirect(CherryPyException):
'Last-Modified'):
if key in response.headers:
del response.headers[key]
# "The 304 response MUST NOT contain a message-body."
response.body = None
# Previous code may have set C-L, so we have to reset it.
@ -278,7 +299,7 @@ class HTTPRedirect(CherryPyException):
response.headers.pop('Content-Length', None)
else:
raise ValueError("The %s status code is unknown." % status)
def __call__(self):
"""Use this exception as a request.handler (raise self)."""
raise self
@ -287,9 +308,9 @@ class HTTPRedirect(CherryPyException):
def clean_headers(status):
"""Remove any headers which should not apply to an error response."""
import cherrypy
response = cherrypy.serving.response
# Remove headers which applied to the original content,
# but do not apply to the error page.
respheaders = response.headers
@ -298,7 +319,7 @@ def clean_headers(status):
"Content-Location", "Content-MD5", "Last-Modified"]:
if key in respheaders:
del respheaders[key]
if status != 416:
# A server sending a response with status code 416 (Requested
# range not satisfiable) SHOULD include a Content-Range field
@ -311,88 +332,93 @@ def clean_headers(status):
class HTTPError(CherryPyException):
"""Exception used to return an HTTP error code (4xx-5xx) to the client.
This exception can be used to automatically send a response using a http status
code, with an appropriate error page. It takes an optional
This exception can be used to automatically send a response using a
http status code, with an appropriate error page. It takes an optional
``status`` argument (which must be between 400 and 599); it defaults to 500
("Internal Server Error"). It also takes an optional ``message`` argument,
which will be returned in the response body. See
`RFC 2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
`RFC2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
for a complete list of available error codes and when to use them.
Examples::
raise cherrypy.HTTPError(403)
raise cherrypy.HTTPError("403 Forbidden", "You are not allowed to access this resource.")
raise cherrypy.HTTPError(
"403 Forbidden", "You are not allowed to access this resource.")
"""
status = None
"""The HTTP status code. May be of type int or str (with a Reason-Phrase)."""
"""The HTTP status code. May be of type int or str (with a Reason-Phrase).
"""
code = None
"""The integer HTTP status code."""
reason = None
"""The HTTP Reason-Phrase string."""
def __init__(self, status=500, message=None):
self.status = status
try:
self.code, self.reason, defaultmsg = _httputil.valid_status(status)
except ValueError:
raise self.__class__(500, _exc_info()[1].args[0])
if self.code < 400 or self.code > 599:
raise ValueError("status must be between 400 and 599.")
# See http://www.python.org/dev/peps/pep-0352/
# self.message = message
self._message = message or defaultmsg
CherryPyException.__init__(self, status, message)
def set_response(self):
"""Modify cherrypy.response status, headers, and body to represent self.
"""Modify cherrypy.response status, headers, and body to represent
self.
CherryPy uses this internally, but you can also use it to create an
HTTPError object and set its output without *raising* the exception.
"""
import cherrypy
response = cherrypy.serving.response
clean_headers(self.code)
# In all cases, finalize will be called after this method,
# so don't bother cleaning up response values here.
response.status = self.status
tb = None
if cherrypy.serving.request.show_tracebacks:
tb = format_exc()
response.headers['Content-Type'] = "text/html;charset=utf-8"
response.headers.pop('Content-Length', None)
content = ntob(self.get_error_page(self.status, traceback=tb,
message=self._message), 'utf-8')
content = self.get_error_page(self.status, traceback=tb,
message=self._message)
response.body = content
_be_ie_unfriendly(self.code)
def get_error_page(self, *args, **kwargs):
return get_error_page(*args, **kwargs)
def __call__(self):
"""Use this exception as a request.handler (raise self)."""
raise self
class NotFound(HTTPError):
"""Exception raised when a URL could not be mapped to any handler (404).
This is equivalent to raising
:class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
"""
def __init__(self, path=None):
if path is None:
import cherrypy
@ -402,7 +428,8 @@ class NotFound(HTTPError):
HTTPError.__init__(self, 404, "The path '%s' was not found." % path)
_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC
"-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
@ -425,25 +452,28 @@ _HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitiona
<p>%(message)s</p>
<pre id="traceback">%(traceback)s</pre>
<div id="powered_by">
<span>Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a></span>
<span>
Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a>
</span>
</div>
</body>
</html>
'''
def get_error_page(status, **kwargs):
"""Return an HTML page, containing a pretty error response.
status should be an int or a str.
kwargs will be interpolated into the page template.
"""
import cherrypy
try:
code, reason, message = _httputil.valid_status(status)
except ValueError:
raise cherrypy.HTTPError(500, _exc_info()[1].args[0])
# We can't use setdefault here, because some
# callers send None for kwarg values.
if kwargs.get('status') is None:
@ -454,23 +484,43 @@ def get_error_page(status, **kwargs):
kwargs['traceback'] = ''
if kwargs.get('version') is None:
kwargs['version'] = cherrypy.__version__
for k, v in iteritems(kwargs):
if v is None:
kwargs[k] = ""
else:
kwargs[k] = _escape(kwargs[k])
# Use a custom template or callable for the error page?
pages = cherrypy.serving.request.error_page
error_page = pages.get(code) or pages.get('default')
# Default template, can be overridden below.
template = _HTTPErrorTemplate
if error_page:
try:
if hasattr(error_page, '__call__'):
return error_page(**kwargs)
# The caller function may be setting headers manually,
# so we delegate to it completely. We may be returning
# an iterator as well as a string here.
#
# We *must* make sure any content is not unicode.
result = error_page(**kwargs)
if cherrypy.lib.is_iterator(result):
from cherrypy.lib.encoding import UTF8StreamEncoder
return UTF8StreamEncoder(result)
elif isinstance(result, cherrypy._cpcompat.unicodestr):
return result.encode('utf-8')
else:
if not isinstance(result, cherrypy._cpcompat.bytestr):
raise ValueError('error page function did not '
'return a bytestring, unicodestring or an '
'iterator - returned object of type %s.'
% (type(result).__name__))
return result
else:
data = open(error_page, 'rb').read()
return tonative(data) % kwargs
# Load the template from this path.
template = tonative(open(error_page, 'rb').read())
except:
e = _format_exception(*_exc_info())[-1]
m = kwargs['message']
@ -478,21 +528,25 @@ def get_error_page(status, **kwargs):
m += "<br />"
m += "In addition, the custom error page failed:\n<br />%s" % e
kwargs['message'] = m
return _HTTPErrorTemplate % kwargs
response = cherrypy.serving.response
response.headers['Content-Type'] = "text/html;charset=utf-8"
result = template % kwargs
return result.encode('utf-8')
_ie_friendly_error_sizes = {
400: 512, 403: 256, 404: 512, 405: 256,
406: 512, 408: 512, 409: 512, 410: 256,
500: 512, 501: 512, 505: 512,
}
}
def _be_ie_unfriendly(status):
import cherrypy
response = cherrypy.serving.response
# For some statuses, Internet Explorer 5+ shows "friendly error
# messages" instead of our response.body if the body is smaller
# than a given size. Fix this by returning a body over that size
@ -525,32 +579,31 @@ def format_exc(exc=None):
finally:
del exc
def bare_error(extrabody=None):
"""Produce status, headers, body for a critical error.
Returns a triple without calling any other questionable functions,
so it should be as error-free as possible. Call it from an HTTP server
if you get errors outside of the request.
If extrabody is None, a friendly but rather unhelpful error message
is set in the body. If extrabody is a string, it will be appended
as-is to the body.
"""
# The whole point of this function is to be a last line-of-defense
# in handling errors. That is, it must not raise any errors itself;
# it cannot be allowed to fail. Therefore, don't add to it!
# In particular, don't call any other CP functions.
body = ntob("Unrecoverable error in the server.")
if extrabody is not None:
if not isinstance(extrabody, bytestr):
extrabody = extrabody.encode('utf-8')
body += ntob("\n") + extrabody
return (ntob("500 Internal Server Error"),
[(ntob('Content-Type'), ntob('text/plain')),
(ntob('Content-Length'), ntob(str(len(body)),'ISO-8859-1'))],
(ntob('Content-Length'), ntob(str(len(body)), 'ISO-8859-1'))],
[body])

View File

@ -34,10 +34,11 @@ and another set of rules specific to each application. The global log
manager is found at :func:`cherrypy.log`, and the log manager for each
application is found at :attr:`app.log<cherrypy._cptree.Application.log>`.
If you're inside a request, the latter is reachable from
``cherrypy.request.app.log``; if you're outside a request, you'll have to obtain
a reference to the ``app``: either the return value of
``cherrypy.request.app.log``; if you're outside a request, you'll have to
obtain a reference to the ``app``: either the return value of
:func:`tree.mount()<cherrypy._cptree.Tree.mount>` or, if you used
:func:`quickstart()<cherrypy.quickstart>` instead, via ``cherrypy.tree.apps['/']``.
:func:`quickstart()<cherrypy.quickstart>` instead, via
``cherrypy.tree.apps['/']``.
By default, the global logs are named "cherrypy.error" and "cherrypy.access",
and the application logs are named "cherrypy.error.2378745" and
@ -55,6 +56,13 @@ errors! The format of access messages is highly formalized, but the error log
isn't--it receives messages from a variety of sources (including full error
tracebacks, if enabled).
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
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()
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,
and attached as described below.
Custom Handlers
===============
@ -69,21 +77,21 @@ and uses a RotatingFileHandler instead:
#python
log = app.log
# Remove the default FileHandlers if present.
log.error_file = ""
log.access_file = ""
maxBytes = getattr(log, "rot_maxBytes", 10000000)
backupCount = getattr(log, "rot_backupCount", 1000)
# Make a new RotatingFileHandler for the error log.
fname = getattr(log, "rot_error_file", "error.log")
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
h.setLevel(DEBUG)
h.setFormatter(_cplogging.logfmt)
log.error_log.addHandler(h)
# Make a new RotatingFileHandler for the access log.
fname = getattr(log, "rot_access_file", "access.log")
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
@ -113,6 +121,7 @@ from cherrypy._cpcompat import ntob, py3k
class NullHandler(logging.Handler):
"""A no-op logging handler to silence the logging.lastResort handler."""
def handle(self, record):
@ -126,39 +135,40 @@ class NullHandler(logging.Handler):
class LogManager(object):
"""An object to assist both simple and advanced logging.
``cherrypy.log`` is an instance of this class.
"""
appid = None
"""The id() of the Application object which owns this log manager. If this
is a global log manager, appid is None."""
error_log = None
"""The actual :class:`logging.Logger` instance for error messages."""
access_log = None
"""The actual :class:`logging.Logger` instance for access messages."""
if py3k:
access_log_format = \
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
else:
access_log_format = \
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
logger_root = None
"""The "top-level" logger name.
This string will be used as the first segment in the Logger names.
The default is "cherrypy", for example, in which case the Logger names
will be of the form::
cherrypy.error.<appid>
cherrypy.access.<appid>
"""
def __init__(self, appid=None, logger_root="cherrypy"):
self.logger_root = logger_root
self.appid = appid
@ -166,8 +176,10 @@ class LogManager(object):
self.error_log = logging.getLogger("%s.error" % logger_root)
self.access_log = logging.getLogger("%s.access" % logger_root)
else:
self.error_log = logging.getLogger("%s.error.%s" % (logger_root, appid))
self.access_log = logging.getLogger("%s.access.%s" % (logger_root, appid))
self.error_log = logging.getLogger(
"%s.error.%s" % (logger_root, appid))
self.access_log = logging.getLogger(
"%s.access.%s" % (logger_root, appid))
self.error_log.setLevel(logging.INFO)
self.access_log.setLevel(logging.INFO)
@ -186,34 +198,36 @@ class LogManager(object):
h.stream.close()
h.stream = open(h.baseFilename, h.mode)
h.release()
def error(self, msg='', context='', severity=logging.INFO, traceback=False):
def error(self, msg='', context='', severity=logging.INFO,
traceback=False):
"""Write the given ``msg`` to the error log.
This is not just for errors! Applications may call this at any time
to log application-specific information.
If ``traceback`` is True, the traceback of the current exception
(if any) will be appended to ``msg``.
"""
if traceback:
msg += _cperror.format_exc()
self.error_log.log(severity, ' '.join((self.time(), context, msg)))
def __call__(self, *args, **kwargs):
"""An alias for ``error``."""
return self.error(*args, **kwargs)
def access(self):
"""Write to the access log (in Apache/NCSA Combined Log format).
See http://httpd.apache.org/docs/2.0/logs.html#combined for format
details.
See the
`apache documentation <http://httpd.apache.org/docs/current/logs.html#combined>`_
for format details.
CherryPy calls this automatically for you. Note there are no arguments;
it collects the data itself from
:class:`cherrypy.request<cherrypy._cprequest.Request>`.
Like Apache started doing in 2.0.46, non-printable and other special
characters in %r (and we expand that to all parts) are escaped using
\\xhh sequences, where hh stands for the hexadecimal representation
@ -232,7 +246,7 @@ class LogManager(object):
status = response.output_status.split(ntob(" "), 1)[0]
if py3k:
status = status.decode('ISO-8859-1')
atoms = {'h': remote.name or remote.ip,
'l': '-',
'u': getattr(request, "login", None) or "-",
@ -242,6 +256,7 @@ class LogManager(object):
'b': dict.get(outheaders, 'Content-Length', '') or "-",
'f': dict.get(inheaders, 'Referer', ''),
'a': dict.get(inheaders, 'User-Agent', ''),
'o': dict.get(inheaders, 'Host', '-'),
}
if py3k:
for k, v in atoms.items():
@ -251,17 +266,18 @@ class LogManager(object):
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
# and backslash for us. All we have to do is strip the quotes.
v = repr(v)[2:-1]
# in python 3.0 the repr of bytes (as returned by encode)
# in python 3.0 the repr of bytes (as returned by encode)
# uses double \'s. But then the logger escapes them yet, again
# resulting in quadruple slashes. Remove the extra one here.
v = v.replace('\\\\', '\\')
# Escape double-quote.
atoms[k] = v
try:
self.access_log.log(logging.INFO, self.access_log_format.format(**atoms))
self.access_log.log(
logging.INFO, self.access_log_format.format(**atoms))
except:
self(traceback=True)
else:
@ -275,12 +291,13 @@ class LogManager(object):
v = repr(v)[1:-1]
# Escape double-quote.
atoms[k] = v.replace('"', '\\"')
try:
self.access_log.log(logging.INFO, self.access_log_format % atoms)
self.access_log.log(
logging.INFO, self.access_log_format % atoms)
except:
self(traceback=True)
def time(self):
"""Return now() in Apache Common Log Format (no timezone)."""
now = datetime.datetime.now()
@ -289,51 +306,49 @@ class LogManager(object):
month = monthnames[now.month - 1].capitalize()
return ('[%02d/%s/%04d:%02d:%02d:%02d]' %
(now.day, month, now.year, now.hour, now.minute, now.second))
def _get_builtin_handler(self, log, key):
for h in log.handlers:
if getattr(h, "_cpbuiltin", None) == key:
return h
# ------------------------- Screen handlers ------------------------- #
def _set_screen_handler(self, log, enable, stream=None):
h = self._get_builtin_handler(log, "screen")
if enable:
if not h:
if stream is None:
stream=sys.stderr
stream = sys.stderr
h = logging.StreamHandler(stream)
h.setFormatter(logfmt)
h._cpbuiltin = "screen"
log.addHandler(h)
elif h:
log.handlers.remove(h)
def _get_screen(self):
h = self._get_builtin_handler
has_h = h(self.error_log, "screen") or h(self.access_log, "screen")
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.
doc="""Turn stderr/stdout logging on or off.
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.
""")
# -------------------------- File handlers -------------------------- #
def _add_builtin_file_handler(self, log, fname):
h = logging.FileHandler(fname)
h.setFormatter(logfmt)
h._cpbuiltin = "file"
log.addHandler(h)
def _set_file_handler(self, log, filename):
h = self._get_builtin_handler(log, "file")
if filename:
@ -348,37 +363,39 @@ class LogManager(object):
if h:
h.close()
log.handlers.remove(h)
def _get_error_file(self):
h = self._get_builtin_handler(self.error_log, "file")
if h:
return h.baseFilename
return ''
def _set_error_file(self, 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.
doc="""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.
""")
def _get_access_file(self):
h = self._get_builtin_handler(self.access_log, "file")
if h:
return h.baseFilename
return ''
def _set_access_file(self, 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.
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 ------------------------- #
def _set_wsgi_handler(self, log, enable):
h = self._get_builtin_handler(log, "wsgi")
if enable:
@ -389,15 +406,15 @@ class LogManager(object):
log.addHandler(h)
elif h:
log.handlers.remove(h)
def _get_wsgi(self):
return bool(self._get_builtin_handler(self.error_log, "wsgi"))
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.
doc="""Write errors to wsgi.errors.
If you set this to True, it'll add the appropriate
:class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you
(which writes errors to ``wsgi.errors``).
@ -406,8 +423,9 @@ class LogManager(object):
class WSGIErrorHandler(logging.Handler):
"A handler class which writes logging records to environ['wsgi.errors']."
def flush(self):
"""Flushes the stream."""
try:
@ -416,7 +434,7 @@ class WSGIErrorHandler(logging.Handler):
pass
else:
stream.flush()
def emit(self, record):
"""Emit a record."""
try:
@ -428,7 +446,8 @@ class WSGIErrorHandler(logging.Handler):
msg = self.format(record)
fs = "%s\n"
import types
if not hasattr(types, "UnicodeType"): #if no unicode support...
# if no unicode support...
if not hasattr(types, "UnicodeType"):
stream.write(fs % msg)
else:
try:

View File

@ -35,12 +35,12 @@ Listen 8080
LoadModule python_module /usr/lib/apache2/modules/mod_python.so
<Location "/">
PythonPath "sys.path+['/path/to/my/application']"
SetHandler python-program
PythonHandler cherrypy._cpmodpy::handler
PythonOption cherrypy.setup myapp::setup_server
PythonDebug On
</Location>
PythonPath "sys.path+['/path/to/my/application']"
SetHandler python-program
PythonHandler cherrypy._cpmodpy::handler
PythonOption cherrypy.setup myapp::setup_server
PythonDebug On
</Location>
# End
The actual path to your mod_python.so is dependent on your
@ -67,11 +67,11 @@ from cherrypy.lib import httputil
# ------------------------------ Request-handling
def setup(req):
from mod_python import apache
# Run any setup functions defined by a "PythonOption cherrypy.setup" directive.
# Run any setup functions defined by a "PythonOption cherrypy.setup"
# directive.
options = req.get_options()
if 'cherrypy.setup' in options:
for function in options['cherrypy.setup'].split():
@ -83,12 +83,12 @@ def setup(req):
mod = __import__(modname, globals(), locals(), [fname])
func = getattr(mod, fname)
func()
cherrypy.config.update({'log.screen': False,
"tools.ignore_headers.on": True,
"tools.ignore_headers.headers": ['Range'],
})
engine = cherrypy.engine
if hasattr(engine, "signal_handler"):
engine.signal_handler.unsubscribe()
@ -96,7 +96,7 @@ def setup(req):
engine.console_control_handler.unsubscribe()
engine.autoreload.unsubscribe()
cherrypy.server.unsubscribe()
def _log(msg, level):
newlevel = apache.APLOG_ERR
if logging.DEBUG >= level:
@ -106,13 +106,13 @@ def setup(req):
elif logging.WARNING >= level:
newlevel = apache.APLOG_WARNING
# On Windows, req.server is required or the msg will vanish. See
# 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..."
apache.log_error(msg, newlevel, req.server)
engine.subscribe('log', _log)
engine.start()
def cherrypy_cleanup(data):
engine.exit()
try:
@ -124,6 +124,7 @@ def setup(req):
class _ReadOnlyRequest:
expose = ('read', 'readline', 'readlines')
def __init__(self, req):
for method in self.expose:
self.__dict__[method] = getattr(req, method)
@ -132,6 +133,8 @@ class _ReadOnlyRequest:
recursive = False
_isSetUp = False
def handler(req):
from mod_python import apache
try:
@ -139,16 +142,18 @@ def handler(req):
if not _isSetUp:
setup(req)
_isSetUp = True
# Obtain a Request object from CherryPy
local = req.connection.local_addr
local = httputil.Host(local[0], local[1], req.connection.local_host or "")
local = httputil.Host(
local[0], local[1], req.connection.local_host or "")
remote = req.connection.remote_addr
remote = httputil.Host(remote[0], remote[1], req.connection.remote_host or "")
remote = httputil.Host(
remote[0], remote[1], req.connection.remote_host or "")
scheme = req.parsed_uri[0] or 'http'
req.get_basic_auth_pw()
try:
# apache.mpm_query only became available in mod_python 3.1
q = apache.mpm_query
@ -158,7 +163,7 @@ def handler(req):
bad_value = ("You must provide a PythonOption '%s', "
"either 'on' or 'off', when running a version "
"of mod_python < 3.1")
threaded = options.get('multithread', '').lower()
if threaded == 'on':
threaded = True
@ -166,7 +171,7 @@ def handler(req):
threaded = False
else:
raise ValueError(bad_value % "multithread")
forked = options.get('multiprocess', '').lower()
if forked == 'on':
forked = True
@ -174,7 +179,7 @@ def handler(req):
forked = False
else:
raise ValueError(bad_value % "multiprocess")
sn = cherrypy.tree.script_name(req.uri or "/")
if sn is None:
send_response(req, '404 Not Found', [], '')
@ -187,7 +192,7 @@ def handler(req):
headers = copyitems(req.headers_in)
rfile = _ReadOnlyRequest(req)
prev = None
try:
redirections = []
while True:
@ -198,7 +203,7 @@ def handler(req):
request.multiprocess = bool(forked)
request.app = app
request.prev = prev
# Run the CherryPy Request object and obtain the response
try:
request.run(method, path, qs, reqproto, headers, rfile)
@ -207,25 +212,28 @@ def handler(req):
ir = sys.exc_info()[1]
app.release_serving()
prev = request
if not recursive:
if ir.path in redirections:
raise RuntimeError("InternalRedirector visited the "
"same URL twice: %r" % ir.path)
raise RuntimeError(
"InternalRedirector visited the same URL "
"twice: %r" % ir.path)
else:
# Add the *previous* path_info + qs to redirections.
# Add the *previous* path_info + qs to
# redirections.
if qs:
qs = "?" + qs
redirections.append(sn + path + qs)
# Munge environment and try again.
method = "GET"
path = ir.path
qs = ir.query_string
rfile = BytesIO()
send_response(req, response.output_status, response.header_list,
response.body, response.stream)
send_response(
req, response.output_status, response.header_list,
response.body, response.stream)
finally:
app.release_serving()
except:
@ -239,7 +247,7 @@ def handler(req):
def send_response(req, status, headers, body, stream=False):
# Set response status
req.status = int(status[:3])
# Set response headers
req.content_type = "text/plain"
for header, value in headers:
@ -247,11 +255,11 @@ def send_response(req, status, headers, body, stream=False):
req.content_type = value
continue
req.headers_out.add(header, value)
if stream:
# Flush now so the status and headers are sent immediately.
req.flush()
# Set response body
if isinstance(body, basestring):
req.write(body)
@ -260,14 +268,12 @@ def send_response(req, status, headers, body, stream=False):
req.write(seg)
# --------------- Startup tools for CherryPy + mod_python --------------- #
import os
import re
try:
import subprocess
def popen(fullcmd):
p = subprocess.Popen(fullcmd, shell=True,
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
@ -284,8 +290,12 @@ def read_process(cmd, args=""):
pipeout = popen(fullcmd)
try:
firstline = pipeout.readline()
if (re.search(ntob("(not recognized|No such file|not found)"), firstline,
re.IGNORECASE)):
cmd_not_found = re.search(
ntob("(not recognized|No such file|not found)"),
firstline,
re.IGNORECASE
)
if cmd_not_found:
raise IOError('%s must be on your system path.' % cmd)
output = firstline + pipeout.read()
finally:
@ -294,7 +304,7 @@ def read_process(cmd, args=""):
class ModPythonServer(object):
template = """
# Apache2 server configuration file for running CherryPy with mod_python.
@ -309,7 +319,7 @@ LoadModule python_module modules/mod_python.so
%(opts)s
</Location>
"""
def __init__(self, loc="/", port=80, opts=None, apache_path="apache",
handler="cherrypy._cpmodpy::handler"):
self.loc = loc
@ -317,7 +327,7 @@ LoadModule python_module modules/mod_python.so
self.opts = opts
self.apache_path = apache_path
self.handler = handler
def start(self):
opts = "".join([" PythonOption %s %s\n" % (k, v)
for k, v in self.opts])
@ -326,19 +336,18 @@ LoadModule python_module modules/mod_python.so
"opts": opts,
"handler": self.handler,
}
mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf")
f = open(mpconf, 'wb')
try:
f.write(conf_data)
finally:
f.close()
response = read_process(self.apache_path, "-k start -f %s" % mpconf)
self.ready = True
return response
def stop(self):
os.popen("apache -k stop")
self.ready = False

View File

@ -11,9 +11,9 @@ from cherrypy import wsgiserver
class NativeGateway(wsgiserver.Gateway):
recursive = False
def respond(self):
req = self.req
try:
@ -22,7 +22,7 @@ class NativeGateway(wsgiserver.Gateway):
local = httputil.Host(local[0], local[1], "")
remote = req.conn.remote_addr, req.conn.remote_port
remote = httputil.Host(remote[0], remote[1], "")
scheme = req.scheme
sn = cherrypy.tree.script_name(req.uri or "/")
if sn is None:
@ -35,7 +35,7 @@ class NativeGateway(wsgiserver.Gateway):
headers = req.inheaders.items()
rfile = req.rfile
prev = None
try:
redirections = []
while True:
@ -45,32 +45,36 @@ class NativeGateway(wsgiserver.Gateway):
request.multiprocess = False
request.app = app
request.prev = prev
# Run the CherryPy Request object and obtain the response
# Run the CherryPy Request object and obtain the
# response
try:
request.run(method, path, qs, req.request_protocol, headers, rfile)
request.run(method, path, qs,
req.request_protocol, headers, rfile)
break
except cherrypy.InternalRedirect:
ir = sys.exc_info()[1]
app.release_serving()
prev = request
if not self.recursive:
if ir.path in redirections:
raise RuntimeError("InternalRedirector visited the "
"same URL twice: %r" % ir.path)
raise RuntimeError(
"InternalRedirector visited the same "
"URL twice: %r" % ir.path)
else:
# Add the *previous* path_info + qs to redirections.
# Add the *previous* path_info + qs to
# redirections.
if qs:
qs = "?" + qs
redirections.append(sn + path + qs)
# Munge environment and try again.
method = "GET"
path = ir.path
qs = ir.query_string
rfile = BytesIO()
self.send_response(
response.output_status, response.header_list,
response.body)
@ -78,59 +82,62 @@ class NativeGateway(wsgiserver.Gateway):
app.release_serving()
except:
tb = format_exc()
#print tb
# print tb
cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
s, h, b = bare_error()
self.send_response(s, h, b)
def send_response(self, status, headers, body):
req = self.req
# Set response status
req.status = str(status or "500 Server Error")
# Set response headers
for header, value in headers:
req.outheaders.append((header, value))
if (req.ready and not req.sent_headers):
req.sent_headers = True
req.send_headers()
# Set response body
for seg in body:
req.write(seg)
class CPHTTPServer(wsgiserver.HTTPServer):
"""Wrapper for wsgiserver.HTTPServer.
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 apply some attributes
from config -> cherrypy.server -> HTTPServer.
"""
def __init__(self, server_adapter=cherrypy.server):
self.server_adapter = server_adapter
server_name = (self.server_adapter.socket_host or
self.server_adapter.socket_file or
None)
wsgiserver.HTTPServer.__init__(
self, server_adapter.bind_addr, NativeGateway,
minthreads=server_adapter.thread_pool,
maxthreads=server_adapter.thread_pool_max,
server_name=server_name)
self.max_request_header_size = self.server_adapter.max_request_header_size or 0
self.max_request_body_size = self.server_adapter.max_request_body_size or 0
self.max_request_header_size = (
self.server_adapter.max_request_header_size or 0)
self.max_request_body_size = (
self.server_adapter.max_request_body_size or 0)
self.request_queue_size = self.server_adapter.socket_queue_size
self.timeout = self.server_adapter.socket_timeout
self.shutdown_timeout = self.server_adapter.shutdown_timeout
self.protocol = self.server_adapter.protocol_version
self.nodelay = self.server_adapter.nodelay
ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
if self.server_adapter.ssl_context:
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
@ -145,5 +152,3 @@ class CPHTTPServer(wsgiserver.HTTPServer):
self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain)

View File

@ -3,8 +3,10 @@
.. versionadded:: 3.2
Application authors have complete control over the parsing of HTTP request
entities. In short, :attr:`cherrypy.request.body<cherrypy._cprequest.Request.body>`
is now always set to an instance of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>`,
entities. In short,
:attr:`cherrypy.request.body<cherrypy._cprequest.Request.body>`
is now always set to an instance of
:class:`RequestBody<cherrypy._cpreqbody.RequestBody>`,
and *that* class is a subclass of :class:`Entity<cherrypy._cpreqbody.Entity>`.
When an HTTP request includes an entity body, it is often desirable to
@ -21,9 +23,9 @@ key to look up a value in the
:attr:`request.body.processors<cherrypy._cpreqbody.Entity.processors>` dict.
If the full media
type is not found, then the major type is tried; for example, if no processor
is found for the 'image/jpeg' type, then we look for a processor for the 'image'
types altogether. If neither the full type nor the major type has a matching
processor, then a default processor is used
is found for the 'image/jpeg' type, then we look for a processor for the
'image' types altogether. If neither the full type nor the major type has a
matching processor, then a default processor is used
(:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>`). For most
types, this means no processing is done, and the body is left unread as a
raw byte stream. Processors are configurable in an 'on_start_resource' hook.
@ -53,7 +55,7 @@ Custom Processors
You can add your own processors for any specific or major MIME type. Simply add
it to the :attr:`processors<cherrypy._cprequest.Entity.processors>` dict in a
hook/tool that runs at ``on_start_resource`` or ``before_request_body``.
hook/tool that runs at ``on_start_resource`` or ``before_request_body``.
Here's the built-in JSON tool for an example::
def json_in(force=True, debug=False):
@ -62,7 +64,7 @@ Here's the built-in JSON tool for an example::
\"""Read application/json data into request.json.\"""
if not entity.headers.get("Content-Length", ""):
raise cherrypy.HTTPError(411)
body = entity.fp.read()
try:
request.json = json_decode(body)
@ -74,31 +76,36 @@ Here's the built-in JSON tool for an example::
415, 'Expected an application/json content type')
request.body.processors['application/json'] = json_processor
We begin by defining a new ``json_processor`` function to stick in the ``processors``
dictionary. All processor functions take a single argument, the ``Entity`` instance
they are to process. It will be called whenever a request is received (for those
URI's where the tool is turned on) which has a ``Content-Type`` of
"application/json".
We begin by defining a new ``json_processor`` function to stick in the
``processors`` dictionary. All processor functions take a single argument,
the ``Entity`` instance they are to process. It will be called whenever a
request is received (for those URI's where the tool is turned on) which
has a ``Content-Type`` of "application/json".
First, it checks for a valid ``Content-Length`` (raising 411 if not valid), then
reads the remaining bytes on the socket. The ``fp`` object knows its own length, so
it won't hang waiting for data that never arrives. It will return when all data
has been read. Then, we decode those bytes using Python's built-in ``json`` module,
and stick the decoded result onto ``request.json`` . If it cannot be decoded, we
raise 400.
First, it checks for a valid ``Content-Length`` (raising 411 if not valid),
then reads the remaining bytes on the socket. The ``fp`` object knows its
own length, so it won't hang waiting for data that never arrives. It will
return when all data has been read. Then, we decode those bytes using
Python's built-in ``json`` module, and stick the decoded result onto
``request.json`` . If it cannot be decoded, we raise 400.
If the "force" argument is True (the default), the ``Tool`` clears the ``processors``
dict so that request entities of other ``Content-Types`` aren't parsed at all. Since
there's no entry for those invalid MIME types, the ``default_proc`` method of ``cherrypy.request.body``
is called. But this does nothing by default (usually to provide the page handler an opportunity to handle it.)
But in our case, we want to raise 415, so we replace ``request.body.default_proc``
If the "force" argument is True (the default), the ``Tool`` clears the
``processors`` dict so that request entities of other ``Content-Types``
aren't parsed at all. Since there's no entry for those invalid MIME
types, the ``default_proc`` method of ``cherrypy.request.body`` is
called. But this does nothing by default (usually to provide the page
handler an opportunity to handle it.)
But in our case, we want to raise 415, so we replace
``request.body.default_proc``
with the error (``HTTPError`` instances, when called, raise themselves).
If we were defining a custom processor, we can do so without making a ``Tool``. Just add the config entry::
If we were defining a custom processor, we can do so without making a ``Tool``.
Just add the config entry::
request.body.processors = {'application/json': json_processor}
Note that you can only replace the ``processors`` dict wholesale this way, not update the existing one.
Note that you can only replace the ``processors`` dict wholesale this way,
not update the existing one.
"""
try:
@ -129,7 +136,7 @@ from cherrypy._cpcompat import basestring, ntob, ntou
from cherrypy.lib import httputil
# -------------------------------- Processors -------------------------------- #
# ------------------------------- Processors -------------------------------- #
def process_urlencoded(entity):
"""Read application/x-www-form-urlencoded data into entity.params."""
@ -141,14 +148,14 @@ def process_urlencoded(entity):
for pair in aparam.split(ntob(';')):
if not pair:
continue
atoms = pair.split(ntob('='), 1)
if len(atoms) == 1:
atoms.append(ntob(''))
key = unquote_plus(atoms[0]).decode(charset)
value = unquote_plus(atoms[1]).decode(charset)
if key in params:
if not isinstance(params[key], list):
params[key] = [params[key]]
@ -164,7 +171,7 @@ def process_urlencoded(entity):
raise cherrypy.HTTPError(
400, "The request entity could not be decoded. The following "
"charsets were attempted: %s" % repr(entity.attempt_charsets))
# Now that all values have been successfully parsed and decoded,
# apply them to the entity.params dict.
for key, value in params.items():
@ -185,22 +192,22 @@ def process_multipart(entity):
# is often necessary to enclose the boundary parameter values in quotes
# on the Content-type line"
ib = entity.content_type.params['boundary'].strip('"')
if not re.match("^[ -~]{0,200}[!-~]$", ib):
raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
ib = ('--' + ib).encode('ascii')
# Find the first marker
while True:
b = entity.readline()
if not b:
return
b = b.strip()
if b == ib:
break
# Read all parts
while True:
part = entity.part_class.from_fp(entity.fp, ib)
@ -209,10 +216,12 @@ def process_multipart(entity):
if part.fp.done:
break
def process_multipart_form_data(entity):
"""Read all multipart/form-data parts into entity.parts or entity.params."""
"""Read all multipart/form-data parts into entity.parts or entity.params.
"""
process_multipart(entity)
kept_parts = []
for part in entity.parts:
if part.name is None:
@ -225,28 +234,29 @@ def process_multipart_form_data(entity):
# It's a file upload. Retain the whole part so consumer code
# has access to its .file and .filename attributes.
value = part
if part.name in entity.params:
if not isinstance(entity.params[part.name], list):
entity.params[part.name] = [entity.params[part.name]]
entity.params[part.name].append(value)
else:
entity.params[part.name] = value
entity.parts = kept_parts
def _old_process_multipart(entity):
"""The behavior of 3.2 and lower. Deprecated and will be changed in 3.3."""
process_multipart(entity)
params = entity.params
for part in entity.parts:
if part.name is None:
key = ntou('parts')
else:
key = part.name
if part.filename is None:
# It's a regular field
value = part.fullvalue()
@ -254,7 +264,7 @@ def _old_process_multipart(entity):
# It's a file upload. Retain the whole part so consumer code
# has access to its .file and .filename attributes.
value = part
if key in params:
if not isinstance(params[key], list):
params[key] = [params[key]]
@ -263,31 +273,32 @@ def _old_process_multipart(entity):
params[key] = value
# --------------------------------- Entities --------------------------------- #
# -------------------------------- Entities --------------------------------- #
class Entity(object):
"""An HTTP request body, or MIME multipart body.
This class collects information about the HTTP request entity. When a
given entity is of MIME type "multipart", each part is parsed into its own
Entity instance, and the set of parts stored in
:attr:`entity.parts<cherrypy._cpreqbody.Entity.parts>`.
Between the ``before_request_body`` and ``before_handler`` tools, CherryPy
tries to process the request body (if any) by calling
:func:`request.body.process<cherrypy._cpreqbody.RequestBody.process`.
This uses the ``content_type`` of the Entity to look up a suitable processor
in :attr:`Entity.processors<cherrypy._cpreqbody.Entity.processors>`, a dict.
:func:`request.body.process<cherrypy._cpreqbody.RequestBody.process>`.
This uses the ``content_type`` of the Entity to look up a suitable
processor in
:attr:`Entity.processors<cherrypy._cpreqbody.Entity.processors>`,
a dict.
If a matching processor cannot be found for the complete Content-Type,
it tries again using the major type. For example, if a request with an
entity of type "image/jpeg" arrives, but no processor can be found for
that complete type, then one is sought for the major type "image". If a
processor is still not found, then the
:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method of the
Entity is called (which does nothing by default; you can override this too).
:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method
of the Entity is called (which does nothing by default; you can
override this too).
CherryPy includes processors for the "application/x-www-form-urlencoded"
type, the "multipart/form-data" type, and the "multipart" major type.
CherryPy 3.2 processes these types almost exactly as older versions.
@ -298,43 +309,43 @@ class Entity(object):
case it will have ``file`` and ``filename`` attributes, or possibly a
``value`` attribute). Each Part is itself a subclass of
Entity, and has its own ``process`` method and ``processors`` dict.
There is a separate processor for the "multipart" major type which is more
flexible, and simply stores all multipart parts in
:attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can
enable it with::
cherrypy.request.body.processors['multipart'] = _cpreqbody.process_multipart
in an ``on_start_resource`` tool.
"""
# http://tools.ietf.org/html/rfc2046#section-4.1.2:
# "The default character set, which must be assumed in the
# absence of a charset parameter, is US-ASCII."
# However, many browsers send data in utf-8 with no charset.
attempt_charsets = ['utf-8']
"""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
encodings will be tried in order. The first one to successfully decode the
entity without raising an error is stored as
:attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
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>`_),
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>`_),
but ``['us-ascii', 'utf-8']`` for multipart parts.
"""
charset = None
"""The successful decoding; see "attempt_charsets" above."""
content_type = None
"""The value of the Content-Type request header.
If the Entity is part of a multipart payload, this will be the Content-Type
given in the MIME headers for this part.
"""
default_content_type = 'application/x-www-form-urlencoded'
"""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
@ -344,26 +355,26 @@ class Entity(object):
declares that a part with no Content-Type defaults to "text/plain"
(see :class:`Part<cherrypy._cpreqbody.Part>`).
"""
filename = None
"""The ``Content-Disposition.filename`` header, if available."""
fp = None
"""The readable socket file object."""
headers = None
"""A dict of request/multipart header names and values.
This is a copy of the ``request.headers`` for the ``request.body``;
for multipart parts, it is the set of headers for that part.
"""
length = None
"""The value of the ``Content-Length`` header, if provided."""
name = None
"""The "name" parameter of the ``Content-Disposition`` header, if any."""
params = None
"""
If the request Content-Type is 'application/x-www-form-urlencoded' or
@ -373,39 +384,40 @@ class Entity(object):
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)."""
processors = {'application/x-www-form-urlencoded': process_urlencoded,
'multipart/form-data': process_multipart_form_data,
'multipart': process_multipart,
}
"""A dict of Content-Type names to processor methods."""
parts = None
"""A list of Part instances if ``Content-Type`` is of major type "multipart"."""
"""A list of Part instances if ``Content-Type`` is of major type
"multipart"."""
part_class = None
"""The class used for multipart parts.
You can replace this with custom subclasses to alter the processing of
multipart parts.
"""
def __init__(self, fp, headers, params=None, parts=None):
# Make an instance-specific copy of the class processors
# so Tools, etc. can replace them per-request.
self.processors = self.processors.copy()
self.fp = fp
self.headers = headers
if params is None:
params = {}
self.params = params
if parts is None:
parts = []
self.parts = parts
# Content-Type
self.content_type = headers.elements('Content-Type')
if self.content_type:
@ -413,25 +425,29 @@ class Entity(object):
else:
self.content_type = httputil.HeaderElement.from_str(
self.default_content_type)
# Copy the class 'attempt_charsets', prepending any Content-Type charset
# Copy the class 'attempt_charsets', prepending any Content-Type
# charset
dec = self.content_type.params.get("charset", None)
if dec:
self.attempt_charsets = [dec] + [c for c in self.attempt_charsets
if c != dec]
else:
self.attempt_charsets = self.attempt_charsets[:]
# Length
self.length = None
clen = headers.get('Content-Length', None)
# If Transfer-Encoding is 'chunked', ignore any Content-Length.
if clen is not None and 'chunked' not in headers.get('Transfer-Encoding', ''):
if (
clen is not None and
'chunked' not in headers.get('Transfer-Encoding', '')
):
try:
self.length = int(clen)
except ValueError:
pass
# Content-Disposition
self.name = None
self.filename = None
@ -444,25 +460,31 @@ class Entity(object):
self.name = self.name[1:-1]
if 'filename' in disp.params:
self.filename = disp.params['filename']
if self.filename.startswith('"') and self.filename.endswith('"'):
if (
self.filename.startswith('"') and
self.filename.endswith('"')
):
self.filename = self.filename[1:-1]
# The 'type' attribute is deprecated in 3.2; remove it in 3.3.
type = property(lambda self: self.content_type,
doc="""A deprecated alias for :attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`.""")
type = property(
lambda self: self.content_type,
doc="A deprecated alias for "
":attr:`content_type<cherrypy._cpreqbody.Entity.content_type>`."
)
def read(self, size=None, fp_out=None):
return self.fp.read(size, fp_out)
def readline(self, size=None):
return self.fp.readline(size)
def readlines(self, sizehint=None):
return self.fp.readlines(sizehint)
def __iter__(self):
return self
def __next__(self):
line = self.readline()
if not line:
@ -471,21 +493,24 @@ class Entity(object):
def next(self):
return self.__next__()
def read_into_file(self, fp_out=None):
"""Read the request body into fp_out (or make_file() if None). Return fp_out."""
"""Read the request body into fp_out (or make_file() if None).
Return fp_out.
"""
if fp_out is None:
fp_out = self.make_file()
self.read(fp_out=fp_out)
return fp_out
def make_file(self):
"""Return a file-like object into which the request body will be read.
By default, this will return a TemporaryFile. Override as needed.
See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`."""
return tempfile.TemporaryFile()
def fullvalue(self):
"""Return this entity as a string, whether stored in a file or not."""
if self.file:
@ -496,7 +521,7 @@ class Entity(object):
else:
value = self.value
return value
def process(self):
"""Execute the best-match processor for the given media type."""
proc = None
@ -513,9 +538,11 @@ class Entity(object):
self.default_proc()
else:
proc(self)
def default_proc(self):
"""Called if a more-specific processor is not found for the ``Content-Type``."""
"""Called if a more-specific processor is not found for the
``Content-Type``.
"""
# Leave the fp alone for someone else to read. This works fine
# for request.body, but the Part subclasses need to override this
# so they can move on to the next part.
@ -523,25 +550,26 @@ class Entity(object):
class Part(Entity):
"""A MIME part entity, part of a multipart entity."""
# "The default character set, which must be assumed in the absence of a
# charset parameter, is US-ASCII."
attempt_charsets = ['us-ascii', 'utf-8']
"""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
encodings will be tried in order. The first one to successfully decode the
entity without raising an error is stored as
:attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
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>`_),
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>`_),
but ``['us-ascii', 'utf-8']`` for multipart parts.
"""
boundary = None
"""The MIME multipart boundary."""
default_content_type = 'text/plain'
"""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
@ -551,26 +579,27 @@ class Part(Entity):
the MIME spec declares that a part with no Content-Type defaults to
"text/plain".
"""
# This is the default in stdlib cgi. We may want to increase it.
maxrambytes = 1000
"""The threshold of bytes after which point the ``Part`` will store its data
in a file (generated by :func:`make_file<cherrypy._cprequest.Entity.make_file>`)
instead of a string. Defaults to 1000, just like the :mod:`cgi` module in
Python's standard library.
"""The threshold of bytes after which point the ``Part`` will store
its data in a file (generated by
:func:`make_file<cherrypy._cprequest.Entity.make_file>`)
instead of a string. Defaults to 1000, just like the :mod:`cgi`
module in Python's standard library.
"""
def __init__(self, fp, headers, boundary):
Entity.__init__(self, fp, headers)
self.boundary = boundary
self.file = None
self.value = None
def from_fp(cls, fp, boundary):
headers = cls.read_headers(fp)
return cls(fp, headers, boundary)
from_fp = classmethod(from_fp)
def read_headers(cls, fp):
headers = httputil.HeaderMap()
while True:
@ -578,13 +607,13 @@ class Part(Entity):
if not line:
# No more data--illegal end of headers
raise EOFError("Illegal end of headers.")
if line == ntob('\r\n'):
# Normal end of headers
break
if not line.endswith(ntob('\r\n')):
raise ValueError("MIME requires CRLF terminators: %r" % line)
if line[0] in ntob(' \t'):
# It's a continuation line.
v = line.strip().decode('ISO-8859-1')
@ -592,24 +621,24 @@ class Part(Entity):
k, v = line.split(ntob(":"), 1)
k = k.strip().decode('ISO-8859-1')
v = v.strip().decode('ISO-8859-1')
existing = headers.get(k)
if existing:
v = ", ".join((existing, v))
headers[k] = v
return headers
read_headers = classmethod(read_headers)
def read_lines_to_boundary(self, fp_out=None):
"""Read bytes from self.fp and return or write them to a file.
If the 'fp_out' argument is None (the default), all bytes read are
returned in a single byte string.
If the 'fp_out' argument is not None, it must be a file-like object that
supports the 'write' method; all bytes read will be written to the fp,
and that fp is returned.
If the 'fp_out' argument is not None, it must be a file-like
object that supports the 'write' method; all bytes read will be
written to the fp, and that fp is returned.
"""
endmarker = self.boundary + ntob("--")
delim = ntob("")
@ -617,7 +646,7 @@ class Part(Entity):
lines = []
seen = 0
while True:
line = self.fp.readline(1<<16)
line = self.fp.readline(1 << 16)
if not line:
raise EOFError("Illegal end of multipart body.")
if line.startswith(ntob("--")) and prev_lf:
@ -627,9 +656,9 @@ class Part(Entity):
if strippedline == endmarker:
self.fp.finish()
break
line = delim + line
if line.endswith(ntob("\r\n")):
delim = ntob("\r\n")
line = line[:-2]
@ -641,7 +670,7 @@ class Part(Entity):
else:
delim = ntob("")
prev_lf = False
if fp_out is None:
lines.append(line)
seen += len(line)
@ -651,7 +680,7 @@ class Part(Entity):
fp_out.write(line)
else:
fp_out.write(line)
if fp_out is None:
result = ntob('').join(lines)
for charset in self.attempt_charsets:
@ -664,14 +693,18 @@ class Part(Entity):
return result
else:
raise cherrypy.HTTPError(
400, "The request entity could not be decoded. The following "
"charsets were attempted: %s" % repr(self.attempt_charsets))
400,
"The request entity could not be decoded. The following "
"charsets were attempted: %s" % repr(self.attempt_charsets)
)
else:
fp_out.seek(0)
return fp_out
def default_proc(self):
"""Called if a more-specific processor is not found for the ``Content-Type``."""
"""Called if a more-specific processor is not found for the
``Content-Type``.
"""
if self.filename:
# Always read into a file if a .filename was given.
self.file = self.read_into_file()
@ -681,9 +714,12 @@ class Part(Entity):
self.value = result
else:
self.file = result
def read_into_file(self, fp_out=None):
"""Read the request body into fp_out (or make_file() if None). Return fp_out."""
"""Read the request body into fp_out (or make_file() if None).
Return fp_out.
"""
if fp_out is None:
fp_out = self.make_file()
self.read_lines_to_boundary(fp_out=fp_out)
@ -696,23 +732,30 @@ try:
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']
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:
def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, has_trailers=False):
def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE,
has_trailers=False):
# Wrap our fp in a buffer so peek() works
self.fp = fp
self.length = length
@ -722,25 +765,25 @@ class SizedReader:
self.bytes_read = 0
self.done = False
self.has_trailers = has_trailers
def read(self, size=None, fp_out=None):
"""Read bytes from the request body and return or write them to a file.
A number of bytes less than or equal to the 'size' argument are read
off the socket. The actual number of bytes read are tracked in
self.bytes_read. The number may be smaller than 'size' when 1) the
client sends fewer bytes, 2) the 'Content-Length' request header
specifies fewer bytes than requested, or 3) the number of bytes read
exceeds self.maxbytes (in which case, 413 is raised).
If the 'fp_out' argument is None (the default), all bytes read are
returned in a single byte string.
If the 'fp_out' argument is not None, it must be a file-like object that
supports the 'write' method; all bytes read will be written to the fp,
and None is returned.
If the 'fp_out' argument is not None, it must be a file-like
object that supports the 'write' method; all bytes read will be
written to the fp, and None is returned.
"""
if self.length is None:
if size is None:
remaining = inf
@ -756,9 +799,9 @@ class SizedReader:
return ntob('')
else:
return None
chunks = []
# Read bytes from the buffer.
if self.buffer:
if remaining is inf:
@ -769,18 +812,18 @@ class SizedReader:
self.buffer = self.buffer[remaining:]
datalen = len(data)
remaining -= datalen
# Check lengths.
self.bytes_read += datalen
if self.maxbytes and self.bytes_read > self.maxbytes:
raise cherrypy.HTTPError(413)
# Store the data.
if fp_out is None:
chunks.append(data)
else:
fp_out.write(data)
# Read bytes from the socket.
while remaining > 0:
chunksize = min(remaining, self.bufsize)
@ -799,21 +842,21 @@ class SizedReader:
break
datalen = len(data)
remaining -= datalen
# Check lengths.
self.bytes_read += datalen
if self.maxbytes and self.bytes_read > self.maxbytes:
raise cherrypy.HTTPError(413)
# Store the data.
if fp_out is None:
chunks.append(data)
else:
fp_out.write(data)
if fp_out is None:
return ntob('').join(chunks)
def readline(self, size=None):
"""Read a line from the request body and return it."""
chunks = []
@ -834,7 +877,7 @@ class SizedReader:
else:
chunks.append(data)
return ntob('').join(chunks)
def readlines(self, sizehint=None):
"""Read lines from the request body and return them."""
if self.length is not None:
@ -842,7 +885,7 @@ class SizedReader:
sizehint = self.length - self.bytes_read
else:
sizehint = min(sizehint, self.length - self.bytes_read)
lines = []
seen = 0
while True:
@ -854,12 +897,12 @@ class SizedReader:
if seen >= sizehint:
break
return lines
def finish(self):
self.done = True
if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
self.trailers = {}
try:
for line in self.fp.read_trailer_lines():
if line[0] in ntob(' \t'):
@ -872,7 +915,7 @@ class SizedReader:
raise ValueError("Illegal header line.")
k = k.strip().title()
v = v.strip()
if k in comma_separated_headers:
existing = self.trailers.get(envname)
if existing:
@ -889,13 +932,15 @@ class SizedReader:
class RequestBody(Entity):
"""The entity of the HTTP request."""
bufsize = 8 * 1024
"""The buffer size used when reading the socket."""
# Don't parse the request body at all if the client didn't provide
# a Content-Type header. See http://www.cherrypy.org/ticket/790
# a Content-Type header. See
# https://bitbucket.org/cherrypy/cherrypy/issue/790
default_content_type = ''
"""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
@ -905,13 +950,15 @@ class RequestBody(Entity):
declares that a part with no Content-Type defaults to "text/plain"
(see :class:`Part<cherrypy._cpreqbody.Part>`).
"""
maxbytes = None
"""Raise ``MaxSizeExceeded`` if more bytes than this are read from the socket."""
"""Raise ``MaxSizeExceeded`` if more bytes than this are read from
the socket.
"""
def __init__(self, fp, headers, params=None, request_params=None):
Entity.__init__(self, fp, headers, params)
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
# When no explicit charset parameter is provided by the
# sender, media subtypes of the "text" type are defined
@ -923,14 +970,14 @@ class RequestBody(Entity):
break
else:
self.attempt_charsets.append('ISO-8859-1')
# Temporary fix while deprecating passing .parts as .params.
self.processors['multipart'] = _old_process_multipart
if request_params is None:
request_params = {}
self.request_params = request_params
def process(self):
"""Process the request entity based on its Content-Type."""
# "The presence of a message-body in a request is signaled by the
@ -942,21 +989,22 @@ class RequestBody(Entity):
h = cherrypy.serving.request.headers
if 'Content-Length' not in h and 'Transfer-Encoding' not in h:
raise cherrypy.HTTPError(411)
self.fp = SizedReader(self.fp, self.length,
self.maxbytes, bufsize=self.bufsize,
has_trailers='Trailer' in h)
super(RequestBody, self).process()
# Body params should also be a part of the request_params
# add them in here.
request_params = self.request_params
for key, value in self.params.items():
# Python 2 only: keyword arguments must be byte strings (type 'str').
# Python 2 only: keyword arguments must be byte strings (type
# 'str').
if sys.version_info < (3, 0):
if isinstance(key, unicode):
key = key.encode('ISO-8859-1')
if key in request_params:
if not isinstance(request_params[key], list):
request_params[key] = [request_params[key]]

View File

@ -13,42 +13,43 @@ from cherrypy.lib import httputil, file_generator
class Hook(object):
"""A callback and its metadata: failsafe, priority, and kwargs."""
callback = None
"""
The bare callable that this Hook object is wrapping, which will
be called when the Hook is called."""
failsafe = False
"""
If True, the callback is guaranteed to run even if other callbacks
from the same call point raise exceptions."""
priority = 50
"""
Defines the order of execution for a list of Hooks. Priority numbers
should be limited to the closed interval [0, 100], but values outside
this range are acceptable, as are fractional values."""
kwargs = {}
"""
A set of keyword arguments that will be passed to the
callable on each call."""
def __init__(self, callback, failsafe=None, priority=None, **kwargs):
self.callback = callback
if failsafe is None:
failsafe = getattr(callback, "failsafe", False)
self.failsafe = failsafe
if priority is None:
priority = getattr(callback, "priority", 50)
self.priority = priority
self.kwargs = kwargs
def __lt__(self, other):
# Python 3
return self.priority < other.priority
@ -56,11 +57,11 @@ class Hook(object):
def __cmp__(self, other):
# Python 2
return cmp(self.priority, other.priority)
def __call__(self):
"""Run self.callback(**self.kwargs)."""
return self.callback(**self.kwargs)
def __repr__(self):
cls = self.__class__
return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)"
@ -71,21 +72,22 @@ class Hook(object):
class HookMap(dict):
"""A map of call points to lists of callbacks (Hook objects)."""
def __new__(cls, points=None):
d = dict.__new__(cls)
for p in points or []:
d[p] = []
return d
def __init__(self, *a, **kw):
pass
def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
"""Append a new Hook made from the supplied arguments."""
self[point].append(Hook(callback, failsafe, priority, **kwargs))
def run(self, point):
"""Execute all registered Hooks (callbacks) for the given point."""
exc = None
@ -110,7 +112,7 @@ class HookMap(dict):
cherrypy.log(traceback=True, severity=40)
if exc:
raise exc
def __copy__(self):
newmap = self.__class__()
# We can't just use 'update' because we want copies of the
@ -119,10 +121,14 @@ class HookMap(dict):
newmap[k] = v[:]
return newmap
copy = __copy__
def __repr__(self):
cls = self.__class__
return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, copykeys(self))
return "%s.%s(points=%r)" % (
cls.__module__,
cls.__name__,
copykeys(self)
)
# Config namespace handlers
@ -139,14 +145,17 @@ def hooks_namespace(k, v):
v = Hook(v)
cherrypy.serving.request.hooks[hookpoint].append(v)
def request_namespace(k, v):
"""Attach request attributes declared in config."""
# Provides config entries to set request.body attrs (like attempt_charsets).
# Provides config entries to set request.body attrs (like
# attempt_charsets).
if k[:5] == 'body.':
setattr(cherrypy.serving.request.body, k[5:], v)
else:
setattr(cherrypy.serving.request, k, v)
def response_namespace(k, v):
"""Attach response attributes declared in config."""
# Provides config entries to set default response headers
@ -156,6 +165,7 @@ def response_namespace(k, v):
else:
setattr(cherrypy.serving.response, k, v)
def error_page_namespace(k, v):
"""Attach error pages declared in config."""
if k != 'default':
@ -170,8 +180,9 @@ hookpoints = ['on_start_resource', 'before_request_body',
class Request(object):
"""An HTTP request.
This object represents the metadata of an HTTP request message;
that is, it contains attributes which describe the environment
in which the request URL, headers, and body were sent (if you
@ -181,43 +192,43 @@ class Request(object):
also contains data regarding the configuration in effect for
the given URL, and the execution plan for generating a response.
"""
prev = None
"""
The previous Request object (if any). This should be None
unless we are processing an InternalRedirect."""
# Conversation/connection attributes
local = httputil.Host("127.0.0.1", 80)
"An httputil.Host(ip, port, hostname) object for the server socket."
remote = httputil.Host("127.0.0.1", 1111)
"An httputil.Host(ip, port, hostname) object for the client socket."
scheme = "http"
"""
The protocol used between client and server. In most cases,
this will be either 'http' or 'https'."""
server_protocol = "HTTP/1.1"
"""
The HTTP version for which the HTTP server is at least
conditionally compliant."""
base = ""
"""The (scheme://host) portion of the requested URL.
In some cases (e.g. when proxying via mod_rewrite), this may contain
path segments which cherrypy.url uses when constructing url's, but
which otherwise are ignored by CherryPy. Regardless, this value
MUST NOT end in a slash."""
# Request-Line attributes
request_line = ""
"""
The complete Request-Line received from the client. This is a
single string consisting of the request method, URI, and protocol
version (joined by spaces). Any final CRLF is removed."""
method = "GET"
"""
Indicates the HTTP method to be performed on the resource identified
@ -225,7 +236,7 @@ class Request(object):
DELETE. CherryPy allows any extension method; however, various HTTP
servers and gateways may restrict the set of allowable methods.
CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
query_string = ""
"""
The query component of the Request-URI, a string of information to be
@ -233,7 +244,7 @@ class Request(object):
path component, and is separated by a '?'. For example, the URI
'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
'a=3&b=4'."""
query_string_encoding = 'utf8'
"""
The encoding expected for query string arguments after % HEX HEX decoding).
@ -242,7 +253,7 @@ class Request(object):
arbitrary encodings to not error, set this to 'Latin-1'; you can then
encode back to bytes and re-decode to whatever encoding you like later.
"""
protocol = (1, 1)
"""The HTTP protocol version corresponding to the set
of features which should be allowed in the response. If BOTH
@ -250,20 +261,20 @@ class Request(object):
compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
If either is 1.0, this attribute will be the tuple (1, 0).
Lower HTTP protocol versions are not explicitly supported."""
params = {}
"""
A dict which combines query string (GET) and request entity (POST)
variables. This is populated in two stages: GET params are added
before the 'on_start_resource' hook, and POST params are added
between the 'before_request_body' and 'before_handler' hooks."""
# Message attributes
header_list = []
"""
A list of the HTTP request headers as (name, value) tuples.
In general, you should use request.headers (a dict) instead."""
headers = httputil.HeaderMap()
"""
A dict-like object containing the request headers. Keys are header
@ -272,10 +283,10 @@ class Request(object):
headers['content-type'] refer to the same value. Values are header
values (decoded according to :rfc:`2047` if necessary). See also:
httputil.HeaderMap, httputil.HeaderElement."""
cookie = SimpleCookie()
"""See help(Cookie)."""
rfile = None
"""
If the request included an entity (body), it will be available
@ -283,11 +294,11 @@ class Request(object):
be read for you between the 'before_request_body' hook and the
'before_handler' hook, and the resulting string is placed into
either request.params or the request.body attribute.
You may disable the automatic consumption of the rfile by setting
request.process_request_body to False, either in config for the desired
path, or in an 'on_start_resource' or 'before_request_body' hook.
WARNING: In almost every case, you should not attempt to read from the
rfile stream after CherryPy's automatic mechanism has read it. If you
turn off the automatic parsing of rfile, you should read exactly the
@ -295,17 +306,20 @@ class Request(object):
Ignoring either of these warnings may result in a hung request thread
or in corruption of the next (pipelined) request.
"""
process_request_body = True
"""
If True, the rfile (if any) is automatically read and parsed,
and the result placed into request.params or request.body."""
methods_with_bodies = ("POST", "PUT")
"""
A sequence of HTTP methods for which CherryPy will automatically
attempt to read a body from the rfile."""
attempt to read a body from the rfile. If you are going to change
this property, modify it on the configuration (recommended)
or on the "hook point" `on_start_resource`.
"""
body = None
"""
If the request Content-Type is 'application/x-www-form-urlencoded'
@ -313,7 +327,7 @@ class Request(object):
of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you
can .read()); this value is set between the 'before_request_body' and
'before_handler' hooks (assuming that process_request_body is True)."""
# Dispatch attributes
dispatch = cherrypy.dispatch.Dispatcher()
"""
@ -322,19 +336,19 @@ class Request(object):
request attributes, and the application architecture. The core
calls the dispatcher as early as possible, passing it a 'path_info'
argument.
The default dispatcher discovers the page handler by matching path_info
to a hierarchical arrangement of objects, starting at request.app.root.
See help(cherrypy.dispatch) for more information."""
script_name = ""
"""
The 'mount point' of the application which is handling this request.
This attribute MUST NOT end in a slash. If the script_name refers to
the root of the URI, it MUST be an empty string (not "/").
"""
path_info = "/"
"""
The 'relative path' portion of the Request-URI. This is relative
@ -346,12 +360,12 @@ class Request(object):
When authentication is used during the request processing this is
set to 'False' if it failed and to the 'username' value if it succeeded.
The default 'None' implies that no authentication happened."""
# Note that cherrypy.url uses "if request.app:" to determine whether
# the call is during a real HTTP request or not. So leave this None.
app = None
"""The cherrypy.Application object which is handling this request."""
handler = None
"""
The function, method, or other callable which CherryPy will call to
@ -360,12 +374,12 @@ class Request(object):
By default, the handler is discovered by walking a tree of objects
starting at request.app.root, and is then passed all HTTP params
(from the query string and POST body) as keyword arguments."""
toolmaps = {}
"""
A nested dict of all Toolboxes and Tools in effect for this request,
of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
config = None
"""
A flat dict of all configuration entries which apply to the
@ -375,7 +389,7 @@ class Request(object):
effect for this request; by default, handler config can be attached
anywhere in the tree between request.app.root and the final handler,
and inherits downward)."""
is_index = None
"""
This will be True if the current request is mapped to an 'index'
@ -383,7 +397,7 @@ class Request(object):
a slash). The value may be used to automatically redirect the
user-agent to a 'more canonical' URL which either adds or removes
the trailing slash. See cherrypy.tools.trailing_slash."""
hooks = HookMap(hookpoints)
"""
A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
@ -392,7 +406,7 @@ class Request(object):
The list of hooks is generally populated as early as possible (mostly
from Tools specified in config), but may be extended at any time.
See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
error_response = cherrypy.HTTPError(500).set_response
"""
The no-arg callable which will handle unexpected, untrapped errors
@ -402,31 +416,31 @@ class Request(object):
via request.error_page or by overriding HTTPError.set_response).
By default, error_response uses HTTPError(500) to return a generic
error response to the user-agent."""
error_page = {}
"""
A dict of {error code: response filename or callable} pairs.
The error code must be an int representing a given HTTP error code,
or the string 'default', which will be used if no matching entry
is found for a given numeric code.
If a filename is provided, the file should contain a Python string-
formatting template, and can expect by default to receive format
formatting template, and can expect by default to receive format
values with the mapping keys %(status)s, %(message)s, %(traceback)s,
and %(version)s. The set of format mappings can be extended by
overriding HTTPError.set_response.
If a callable is provided, it will be called by default with keyword
arguments 'status', 'message', 'traceback', and 'version', as for a
string-formatting template. The callable must return a string or iterable of
strings which will be set to response.body. It may also override headers or
perform any other processing.
string-formatting template. The callable must return a string or
iterable of strings which will be set to response.body. It may also
override headers or perform any other processing.
If no entry is given for an error code, and no 'default' entry exists,
a default template will be used.
"""
show_tracebacks = True
"""
If True, unexpected errors encountered during request processing will
@ -436,23 +450,23 @@ class Request(object):
"""
If True, mismatched parameters encountered during PageHandler invocation
processing will be included in the response body."""
throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
"""The sequence of exceptions which Request.run does not trap."""
throw_errors = False
"""
If True, Request.run will not trap any errors (except HTTPRedirect and
HTTPError, which are more properly called 'exceptions', not errors)."""
closed = False
"""True once the close method has been called, False otherwise."""
stage = None
"""
A string containing the stage reached in the request-handling process.
This is useful when debugging a live server with hung requests."""
namespaces = _cpconfig.NamespaceSet(
**{"hooks": hooks_namespace,
"request": request_namespace,
@ -460,11 +474,11 @@ class Request(object):
"error_page": error_page_namespace,
"tools": cherrypy.tools,
})
def __init__(self, local_host, remote_host, scheme="http",
server_protocol="HTTP/1.1"):
"""Populate a new Request object.
local_host should be an httputil.Host object with the server info.
remote_host should be an httputil.Host object with the client info.
scheme should be a string, either "http" or "https".
@ -473,17 +487,17 @@ class Request(object):
self.remote = remote_host
self.scheme = scheme
self.server_protocol = server_protocol
self.closed = False
# Put a *copy* of the class error_page into self.
self.error_page = self.error_page.copy()
# Put a *copy* of the class namespaces into self.
self.namespaces = self.namespaces.copy()
self.stage = None
def close(self):
"""Run cleanup code. (Core)"""
if not self.closed:
@ -491,49 +505,49 @@ class Request(object):
self.stage = 'on_end_request'
self.hooks.run('on_end_request')
self.stage = 'close'
def run(self, method, path, query_string, req_protocol, headers, rfile):
r"""Process the Request. (Core)
method, path, query_string, and req_protocol should be pulled directly
from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
path
This should be %XX-unquoted, but query_string should not be.
When using Python 2, they both MUST be byte strings,
not unicode strings.
When using Python 3, they both MUST be unicode strings,
not byte strings, and preferably not bytes \x00-\xFF
disguised as unicode.
headers
A list of (name, value) tuples.
rfile
A file-like object containing the HTTP request entity.
When run() is done, the returned object should have 3 attributes:
* status, e.g. "200 OK"
* header_list, a list of (name, value) tuples
* body, an iterable yielding strings
Consumer code (HTTP servers) should then access these response
attributes to build the outbound stream.
"""
response = cherrypy.serving.response
self.stage = 'run'
try:
self.error_response = cherrypy.HTTPError(500).set_response
self.method = method
path = path or "/"
self.query_string = query_string or ''
self.params = {}
# Compare request and server HTTP protocol versions, in case our
# server does not support the requested protocol. Limit our output
# to min(req, server). We want the following output:
@ -550,30 +564,30 @@ class Request(object):
sp = int(self.server_protocol[5]), int(self.server_protocol[7])
self.protocol = min(rp, sp)
response.headers.protocol = self.protocol
# Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
url = path
if query_string:
url += '?' + query_string
self.request_line = '%s %s %s' % (method, url, req_protocol)
self.header_list = list(headers)
self.headers = httputil.HeaderMap()
self.rfile = rfile
self.body = None
self.cookie = SimpleCookie()
self.handler = None
# path_info should be the path from the
# app root (script_name) to the handler.
self.script_name = self.app.script_name
self.path_info = pi = path[len(self.script_name):]
self.stage = 'respond'
self.respond(pi)
except self.throws:
raise
except:
@ -589,24 +603,24 @@ class Request(object):
body = ""
r = bare_error(body)
response.output_status, response.header_list, response.body = r
if self.method == "HEAD":
# HEAD requests MUST NOT return a message-body in the response.
response.body = []
try:
cherrypy.log.access()
except:
cherrypy.log.error(traceback=True)
if response.timed_out:
raise cherrypy.TimeoutError()
return response
# Uncomment for stage debugging
# stage = property(lambda self: self._stage, lambda self, v: print(v))
def respond(self, path_info):
"""Generate a response for the resource at self.path_info. (Core)"""
response = cherrypy.serving.response
@ -615,30 +629,30 @@ class Request(object):
try:
if self.app is None:
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:
@ -647,14 +661,14 @@ class Request(object):
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')
@ -674,7 +688,7 @@ class Request(object):
if self.throw_errors:
raise
self.handle_error()
def process_query_string(self):
"""Parse the query string into Python structures. (Core)"""
try:
@ -685,7 +699,7 @@ class Request(object):
404, "The given query string could not be processed. Query "
"strings for this resource must be encoded with %r." %
self.query_string_encoding)
# Python 2 only: keyword arguments must be byte strings (type 'str').
if not py3k:
for key, value in p.items():
@ -693,7 +707,7 @@ class Request(object):
del p[key]
p[key.encode(self.query_string_encoding)] = value
self.params.update(p)
def process_headers(self):
"""Parse HTTP header data into Python structures. (Core)"""
# Process the headers into self.headers
@ -703,15 +717,16 @@ class Request(object):
# so title doesn't have to be called twice.
name = name.title()
value = value.strip()
# Warning: if there is more than one header entry for cookies (AFAIK,
# only Konqueror does that), only the last one will remain in headers
# (but they will be correctly stored in request.cookie).
# Warning: if there is more than one header entry for cookies
# (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
# cookies come on different lines with the same key
if name == 'Cookie':
@ -720,7 +735,7 @@ class Request(object):
except CookieError:
msg = "Illegal cookie name %s" % value.split('=')[0]
raise cherrypy.HTTPError(400, msg)
if not dict.__contains__(headers, 'Host'):
# All Internet-based HTTP/1.1 servers MUST respond with a 400
# (Bad Request) status code to any HTTP/1.1 request message
@ -732,17 +747,18 @@ class Request(object):
if not host:
host = self.local.name or self.local.ip
self.base = "%s://%s" % (self.scheme, host)
def get_resource(self, path):
"""Call a dispatcher (which sets self.handler and .config). (Core)"""
# First, see if there is a custom dispatch at this URI. Custom
# dispatchers can only be specified in app.config, not in _cp_config
# (since custom dispatchers may not even have an app.root).
dispatch = self.app.find_config(path, "request.dispatch", self.dispatch)
dispatch = self.app.find_config(
path, "request.dispatch", self.dispatch)
# dispatch() should set self.handler and self.config
dispatch(path)
def handle_error(self):
"""Handle the last unanticipated exception. (Core)"""
try:
@ -755,18 +771,18 @@ class Request(object):
inst = sys.exc_info()[1]
inst.set_response()
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
)
"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= """
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
@ -774,30 +790,31 @@ class Request(object):
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):
"""The body of the HTTP response (the response entity)."""
if py3k:
unicode_err = ("Page handlers MUST return bytes. Use tools.encode "
"if you wish to return unicode.")
def __get__(self, obj, objclass=None):
if obj is None:
# When calling on the class instead of an instance...
return self
else:
return obj._body
def __set__(self, obj, value):
# Convert the given value to an iterable object.
if py3k and isinstance(value, str):
raise ValueError(self.unicode_err)
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
@ -808,7 +825,7 @@ class ResponseBody(object):
# [''] 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 isinstance(item, str):
raise ValueError(self.unicode_err)
@ -822,18 +839,19 @@ class ResponseBody(object):
class Response(object):
"""An HTTP Response, including status, headers, and body."""
status = ""
"""The HTTP Status-Code and Reason-Phrase."""
header_list = []
"""
A list of the HTTP response headers as (name, value) tuples.
In general, you should use response.headers (a dict) instead. This
attribute is generated from response.headers and is not valid until
after the finalize phase."""
headers = httputil.HeaderMap()
"""
A dict-like object containing the response headers. Keys are header
@ -841,36 +859,36 @@ class Response(object):
a case-insensitive manner. That is, headers['Content-Type'] and
headers['content-type'] refer to the same value. Values are header
values (decoded according to :rfc:`2047` if necessary).
.. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement`
"""
cookie = SimpleCookie()
"""See help(Cookie)."""
body = ResponseBody()
"""The body (entity) of the HTTP response."""
time = None
"""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
"""If False, buffer the response body."""
def __init__(self):
self.status = None
self.header_list = None
self._body = []
self.time = time.time()
self.headers = httputil.HeaderMap()
# Since we know all our keys are titled strings, we can
# bypass HeaderMap.update and get a big speed boost.
@ -880,34 +898,36 @@ class Response(object):
"Date": httputil.HTTPDate(self.time),
})
self.cookie = SimpleCookie()
def collapse_body(self):
"""Collapse self.body to a single string; replace it and return it."""
if isinstance(self.body, basestring):
return self.body
newbody = []
for chunk in self.body:
if py3k and not isinstance(chunk, bytes):
raise TypeError("Chunk %s is not of type 'bytes'." % repr(chunk))
raise TypeError("Chunk %s is not of type 'bytes'." %
repr(chunk))
newbody.append(chunk)
newbody = ntob('').join(newbody)
self.body = newbody
return newbody
def finalize(self):
"""Transform headers (and cookies) into self.header_list. (Core)"""
try:
code, reason, _ = httputil.valid_status(self.status)
except ValueError:
raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
headers = self.headers
self.status = "%s %s" % (code, reason)
self.output_status = ntob(str(code), 'ascii') + ntob(" ") + headers.encode(reason)
self.output_status = ntob(str(code), 'ascii') + \
ntob(" ") + headers.encode(reason)
if self.stream:
# The upshot: wsgiserver will chunk the response if
# you pop Content-Length (or set it explicitly to None).
@ -926,10 +946,10 @@ class Response(object):
if dict.get(headers, 'Content-Length') is None:
content = self.collapse_body()
dict.__setitem__(headers, 'Content-Length', len(content))
# Transform our header dict into a list of tuples.
self.header_list = h = headers.output()
cookie = self.cookie.output()
if cookie:
for line in cookie.split("\n"):
@ -942,15 +962,12 @@ class Response(object):
if isinstance(value, unicodestr):
value = headers.encode(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,
so that a monitor thread can interrupt the Response thread.
"""
if time.time() > self.time + self.timeout:
self.timed_out = True

View File

@ -12,112 +12,129 @@ from cherrypy.process.servers import *
class Server(ServerAdapter):
"""An adapter for an HTTP server.
You can set attributes (like socket_host and socket_port)
on *this* object (which is probably cherrypy.server), and call
quickstart. For example::
cherrypy.server.socket_port = 80
cherrypy.quickstart()
"""
socket_port = 8080
"""The TCP port on which to listen for connections."""
_socket_host = '127.0.0.1'
def _get_socket_host(self):
return self._socket_host
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,
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.
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
IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
is the similar IN6ADDR_ANY for IPv6. The empty string or None are
not allowed.""")
socket_file = None
"""If given, the name of the UNIX socket to use instead of TCP/IP.
When this option is not None, the `socket_host` and `socket_port` options
are ignored."""
socket_queue_size = 5
"""The 'backlog' argument to socket.listen(); specifies the maximum number
of queued connections (default 5)."""
socket_timeout = 10
"""The timeout in seconds for accepted connections (default 10)."""
accepted_queue_size = -1
"""The maximum number of requests which will be queued up before
the server refuses to accept it (default -1, meaning no limit)."""
accepted_queue_timeout = 10
"""The timeout in seconds for attempting to add a request to the
queue when the queue is full (default 10)."""
shutdown_timeout = 5
"""The time to wait for HTTP worker threads to clean up."""
protocol_version = 'HTTP/1.1'
"""The version string to write in the Status-Line of all HTTP responses,
for example, "HTTP/1.1" (the default). Depending on the HTTP server used,
this should also limit the supported features used in the response."""
thread_pool = 10
"""The number of worker threads to start up in the pool."""
thread_pool_max = -1
"""The maximum size of the worker-thread pool. Use -1 to indicate no limit."""
"""The maximum size of the worker-thread pool. Use -1 to indicate no limit.
"""
max_request_header_size = 500 * 1024
"""The maximum number of bytes allowable in the request headers. If exceeded,
the HTTP server should return "413 Request Entity Too Large"."""
"""The maximum number of bytes allowable in the request headers.
If exceeded, the HTTP server should return "413 Request Entity Too Large".
"""
max_request_body_size = 100 * 1024 * 1024
"""The maximum number of bytes allowable in the request body. If exceeded,
the HTTP server should return "413 Request Entity Too Large"."""
instance = None
"""If not None, this should be an HTTP server instance (such as
CPWSGIServer) which cherrypy.server will control. Use this when you need
more control over object instantiation than is available in the various
configuration options."""
ssl_context = None
"""When using PyOpenSSL, an instance of SSL.Context."""
ssl_certificate = None
"""The filename of the SSL certificate to use."""
ssl_certificate_chain = None
"""When using PyOpenSSL, the certificate chain to pass to
Context.load_verify_locations."""
ssl_private_key = None
"""The filename of the private key to use with SSL."""
if py3k:
ssl_module = 'builtin'
"""The name of a registered SSL adaptation module to use with the builtin
WSGI server. Builtin options are: 'builtin' (to use the SSL library built
into recent versions of Python). You may also register your
own classes in the wsgiserver.ssl_adapters dict."""
"""The name of a registered SSL adaptation module to use with
the builtin WSGI server. Builtin options are: 'builtin' (to
use the SSL library built into recent versions of Python).
You may also register your own classes in the
wsgiserver.ssl_adapters dict."""
else:
ssl_module = 'pyopenssl'
"""The name of a registered SSL adaptation module to use with the builtin
WSGI server. Builtin options are 'builtin' (to use the SSL library built
into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL
project, which you must install separately). You may also register your
own classes in the wsgiserver.ssl_adapters dict."""
"""The name of a registered SSL adaptation module to use with the
builtin WSGI server. Builtin options are 'builtin' (to use the SSL
library built into recent versions of Python) and 'pyopenssl' (to
use the PyOpenSSL project, which you must install separately). You
may also register your own classes in the wsgiserver.ssl_adapters
dict."""
statistics = False
"""Turns statistics-gathering on or off for aware HTTP servers."""
nodelay = True
"""If True (the default since 3.1), sets the TCP_NODELAY socket option."""
wsgi_version = (1, 0)
"""The WSGI version tuple to use with the builtin WSGI server.
The provided options are (1, 0) [which includes support for PEP 3333,
@ -125,13 +142,13 @@ class Server(ServerAdapter):
wsgi.version (1, 0)] and ('u', 0), an experimental unicode version.
You may create and register your own experimental versions of the WSGI
protocol by adding custom classes to the wsgiserver.wsgi_gateways dict."""
def __init__(self):
self.bus = cherrypy.engine
self.httpserver = None
self.interrupt = None
self.running = False
def httpserver_from_self(self, httpserver=None):
"""Return a (httpserver, bind_addr) pair based on self attributes."""
if httpserver is None:
@ -143,20 +160,21 @@ class Server(ServerAdapter):
# Is anyone using this? Can I add an arg?
httpserver = attributes(httpserver)(self)
return httpserver, self.bind_addr
def start(self):
"""Start the HTTP server."""
if not self.httpserver:
self.httpserver, self.bind_addr = self.httpserver_from_self()
ServerAdapter.start(self)
start.priority = 75
def _get_bind_addr(self):
if self.socket_file:
return self.socket_file
if self.socket_host is None and self.socket_port is None:
return None
return (self.socket_host, self.socket_port)
def _set_bind_addr(self, value):
if value is None:
self.socket_file = None
@ -174,14 +192,18 @@ class Server(ServerAdapter):
raise ValueError("bind_addr must be a (host, port) tuple "
"(for TCP sockets) or a string (for Unix "
"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.')
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):
"""Return the base (scheme://host[:port] or sock file) for this server."""
"""Return the base (scheme://host[:port] or sock file) for this server.
"""
if self.socket_file:
return self.socket_file
host = self.socket_host
if host in ('0.0.0.0', '::'):
# 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY.
@ -189,9 +211,9 @@ class Server(ServerAdapter):
# safest thing to spit out in a URL.
import socket
host = socket.gethostname()
port = self.socket_port
if self.ssl_certificate:
scheme = "https"
if port != 443:
@ -200,6 +222,5 @@ class Server(ServerAdapter):
scheme = "http"
if port != 80:
host += ":%s" % port
return "%s://%s" % (scheme, host)
return "%s://%s" % (scheme, host)

View File

@ -137,6 +137,7 @@ affects what we see:
# Threading import is at end
class _localbase(object):
__slots__ = '_local__key', '_local__args', '_local__lock'
@ -158,6 +159,7 @@ class _localbase(object):
return self
def _patch(self):
key = object.__getattribute__(self, '_local__key')
d = currentThread().__dict__.get(key)
@ -175,6 +177,7 @@ def _patch(self):
else:
object.__setattr__(self, '__dict__', d)
class local(_localbase):
def __getattribute__(self, name):
@ -204,7 +207,6 @@ class local(_localbase):
finally:
lock.release()
def __del__():
threading_enumerate = enumerate
__getattribute__ = object.__getattribute__
@ -231,7 +233,7 @@ class local(_localbase):
try:
del __dict__[key]
except KeyError:
pass # didn't have anything in this thread
pass # didn't have anything in this thread
return __del__
__del__ = __del__()

View File

@ -2,18 +2,18 @@
Tools are usually designed to be used in a variety of ways (although some
may only offer one if they choose):
Library calls
All tools are callables that can be used wherever needed.
The arguments are straightforward and should be detailed within the
docstring.
Function decorators
All tools, when called, may be used as decorators which configure
individual CherryPy page handlers (methods on the CherryPy tree).
That is, "@tools.anytool()" should "turn on" the tool via the
decorated function's _cp_config attribute.
CherryPy config
If a tool exposes a "_setup" callable, it will be called
once per Request (if the feature is "turned on" via config).
@ -43,17 +43,21 @@ def _getargs(func):
return co.co_varnames[:co.co_argcount]
_attr_error = ("CherryPy Tools cannot be turned on directly. Instead, turn them "
"on via config, or use them as decorators on your page handlers.")
_attr_error = (
"CherryPy Tools cannot be turned on directly. Instead, turn them "
"on via config, or use them as decorators on your page handlers."
)
class Tool(object):
"""A registered function for use with CherryPy request-processing hooks.
help(tool.callable) should give you more information about this Tool.
"""
namespace = "tools"
def __init__(self, point, callable, name=None, priority=50):
self._point = point
self.callable = callable
@ -61,13 +65,14 @@ class Tool(object):
self._priority = priority
self.__doc__ = self.callable.__doc__
self._setargs()
def _get_on(self):
raise AttributeError(_attr_error)
def _set_on(self, value):
raise AttributeError(_attr_error)
on = property(_get_on, _set_on)
def _setargs(self):
"""Copy func parameter names to obj attributes."""
try:
@ -86,28 +91,28 @@ class Tool(object):
# but if we trap it here it doesn't prevent CP from working.
except IndexError:
pass
def _merged_args(self, d=None):
"""Return a dict of configuration entries for this Tool."""
if d:
conf = d.copy()
else:
conf = {}
tm = cherrypy.serving.request.toolmaps[self.namespace]
if self._name in tm:
conf.update(tm[self._name])
if "on" in conf:
del conf["on"]
return conf
def __call__(self, *args, **kwargs):
"""Compile-time decorator (turn on the tool in config).
For example::
@tools.proxy()
def whats_my_base(self):
return cherrypy.request.base
@ -117,6 +122,7 @@ class Tool(object):
raise TypeError("The %r Tool does not accept positional "
"arguments; you must use keyword arguments."
% self._name)
def tool_decorator(f):
if not hasattr(f, "_cp_config"):
f._cp_config = {}
@ -126,10 +132,10 @@ class Tool(object):
f._cp_config[subspace + k] = v
return f
return tool_decorator
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
@ -142,8 +148,9 @@ class Tool(object):
class HandlerTool(Tool):
"""Tool which is called 'before main', that may skip normal handlers.
If the tool successfully handles the request (by setting response.body),
if should return True. This will cause CherryPy to skip any 'normal' page
handler. If the tool did not handle the request, it should return False
@ -151,15 +158,15 @@ class HandlerTool(Tool):
tool is declared AS a page handler (see the 'handler' method), returning
False will raise NotFound.
"""
def __init__(self, callable, name=None):
Tool.__init__(self, 'before_handler', callable, name)
def handler(self, *args, **kwargs):
"""Use this tool as a CherryPy page handler.
For example::
class Root:
nav = tools.staticdir.handler(section="/nav", dir="nav",
root=absDir)
@ -171,14 +178,14 @@ class HandlerTool(Tool):
return cherrypy.serving.response.body
handle_func.exposed = True
return handle_func
def _wrapper(self, **kwargs):
if self.callable(**kwargs):
cherrypy.serving.request.handler = None
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
@ -191,16 +198,17 @@ class HandlerTool(Tool):
class HandlerWrapperTool(Tool):
"""Tool which wraps request.handler in a provided wrapper function.
The 'newhandler' arg must be a handler wrapper function that takes a
'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all
page handler
functions, it must return an iterable for use as cherrypy.response.body.
For example, to allow your 'inner' page handlers to return dicts
which then get interpolated into a template::
def interpolator(next_handler, *args, **kwargs):
filename = cherrypy.request.config.get('template')
cherrypy.response.template = env.get_template(filename)
@ -208,32 +216,35 @@ class HandlerWrapperTool(Tool):
return cherrypy.response.template.render(**response_dict)
cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
"""
def __init__(self, newhandler, point='before_handler', name=None, priority=50):
def __init__(self, newhandler, point='before_handler', name=None,
priority=50):
self.newhandler = newhandler
self._point = point
self._name = name
self._priority = priority
def callable(self, debug=False):
def callable(self, *args, **kwargs):
innerfunc = cherrypy.serving.request.handler
def wrap(*args, **kwargs):
return self.newhandler(innerfunc, *args, **kwargs)
cherrypy.serving.request.handler = wrap
class ErrorTool(Tool):
"""Tool which is used to replace the default request.error_response."""
def __init__(self, callable, name=None):
Tool.__init__(self, None, callable, name)
def _wrapper(self):
self.callable(**self._merged_args())
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
@ -249,45 +260,47 @@ from cherrypy.lib import auth_basic, auth_digest
class SessionTool(Tool):
"""Session Tool for CherryPy.
sessions.locking
When 'implicit' (the default), the session will be locked for you,
just before running the page handler.
When 'early', the session will be locked before reading the request
body. This is off by default for safety reasons; for example,
a large upload would block the session, denying an AJAX
progress meter (see http://www.cherrypy.org/ticket/630).
progress meter
(`issue <https://bitbucket.org/cherrypy/cherrypy/issue/630>`_).
When 'explicit' (or any other value), you need to call
cherrypy.session.acquire_lock() yourself before using
session data.
"""
def __init__(self):
# _sessions.init must be bound after headers are read
Tool.__init__(self, 'before_request_body', _sessions.init)
def _lock_session(self):
cherrypy.serving.session.acquire_lock()
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
hooks = cherrypy.serving.request.hooks
conf = self._merged_args()
p = conf.pop("priority", None)
if p is None:
p = getattr(self.callable, "priority", self._priority)
hooks.attach(self._point, self.callable, priority=p, **conf)
locking = conf.pop('locking', 'implicit')
if locking == 'implicit':
hooks.attach('before_handler', self._lock_session)
@ -298,15 +311,15 @@ class SessionTool(Tool):
else:
# Don't lock
pass
hooks.attach('before_finalize', _sessions.save)
hooks.attach('on_end_request', _sessions.close)
def regenerate(self):
"""Drop the current session and make a new one (with a new id)."""
sess = cherrypy.serving.session
sess.regenerate()
# Grab cookie-relevant tool args
conf = dict([(k, v) for k, v in self._merged_args().items()
if k in ('path', 'path_header', 'name', 'timeout',
@ -314,19 +327,18 @@ class SessionTool(Tool):
_sessions.set_response_cookie(**conf)
class XMLRPCController(object):
"""A Controller (page handler collection) for XML-RPC.
To use it, have your controllers subclass this base class (it will
turn on the tool for you).
You can also supply the following optional config entries::
tools.xmlrpc.encoding: 'utf-8'
tools.xmlrpc.allow_none: 0
XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
appropriate handler must first be performed according to the URL, and
then a second dispatch step must take place according to the RPC method
@ -334,42 +346,42 @@ class XMLRPCController(object):
prefix in the URL, supplies its own handler args in the body, and
requires a 200 OK "Fault" response instead of 404 when the desired
method is not found.
Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
This Controller acts as the dispatch target for the first half (based
on the URL); it then reads the RPC method from the request body and
does its own second dispatch step based on that method. It also reads
body params, and returns a Fault on error.
The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
in your URL's, you can safely skip turning on the XMLRPCDispatcher.
Otherwise, you need to use declare it in config::
request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
"""
# Note we're hard-coding this into the 'tools' namespace. We could do
# a huge amount of work to make it relocatable, but the only reason why
# would be if someone actually disabled the default_toolbox. Meh.
_cp_config = {'tools.xmlrpc.on': True}
def default(self, *vpath, **params):
rpcparams, rpcmethod = _xmlrpc.process_body()
subhandler = self
for attr in str(rpcmethod).split('.'):
subhandler = getattr(subhandler, attr, None)
if subhandler and getattr(subhandler, "exposed", False):
body = subhandler(*(vpath + rpcparams), **params)
else:
# http://www.cherrypy.org/ticket/533
# https://bitbucket.org/cherrypy/cherrypy/issue/533
# if a method is not found, an xmlrpclib.Fault should be returned
# raising an exception here will do that; see
# cherrypy.lib.xmlrpcutil.on_error
raise Exception('method "%s" is not supported' % attr)
conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {})
_xmlrpc.respond(body,
conf.get('encoding', 'utf-8'),
@ -379,7 +391,7 @@ class XMLRPCController(object):
class SessionAuthTool(HandlerTool):
def _setargs(self):
for name in dir(cptools.SessionAuth):
if not name.startswith("__"):
@ -387,8 +399,9 @@ class SessionAuthTool(HandlerTool):
class CachingTool(Tool):
"""Caching Tool for CherryPy."""
def _wrapper(self, **kwargs):
request = cherrypy.serving.request
if _caching.get(**kwargs):
@ -397,29 +410,29 @@ class CachingTool(Tool):
if request.cacheable:
# Note the devious technique here of adding hooks on the fly
request.hooks.attach('before_finalize', _caching.tee_output,
priority = 90)
priority=90)
_wrapper.priority = 20
def _setup(self):
"""Hook caching into cherrypy.request."""
conf = self._merged_args()
p = conf.pop("priority", None)
cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
priority=p, **conf)
class Toolbox(object):
"""A collection of Tools.
This object also functions as a config namespace handler for itself.
Custom toolboxes should be added to each Application's toolboxes dict.
"""
def __init__(self, namespace):
self.namespace = namespace
def __setattr__(self, name, value):
# If the Tool._name is None, supply it from the attribute name.
if isinstance(value, Tool):
@ -427,16 +440,17 @@ class Toolbox(object):
value._name = name
value.namespace = self.namespace
object.__setattr__(self, name, value)
def __enter__(self):
"""Populate request.toolmaps from tools specified in config."""
cherrypy.serving.request.toolmaps[self.namespace] = map = {}
def populate(k, v):
toolname, arg = k.split(".", 1)
bucket = map.setdefault(toolname, {})
bucket[arg] = v
return populate
def __exit__(self, exc_type, exc_val, exc_tb):
"""Run tool._setup() for each tool in our toolmap."""
map = cherrypy.serving.request.toolmaps.get(self.namespace)
@ -448,21 +462,22 @@ class Toolbox(object):
class DeprecatedTool(Tool):
_name = None
warnmsg = "This Tool is deprecated."
def __init__(self, point, warnmsg=None):
self.point = point
if warnmsg is not None:
self.warnmsg = warnmsg
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)
@ -487,12 +502,16 @@ _d.sessions = SessionTool()
_d.xmlrpc = ErrorTool(_xmlrpc.on_error)
_d.caching = CachingTool('before_handler', _caching.get, 'caching')
_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.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.referer = Tool('before_request_body', cptools.referer)
_d.basic_auth = Tool('on_start_resource', auth.basic_auth)

View File

@ -1,7 +1,6 @@
"""CherryPy Application and Tree objects."""
import os
import sys
import cherrypy
from cherrypy._cpcompat import ntou, py3k
@ -10,101 +9,106 @@ from cherrypy.lib import httputil
class Application(object):
"""A CherryPy Application.
Servers and gateways should not instantiate Request objects directly.
Instead, they should ask an Application object for a request object.
An instance of this class may also be used as a WSGI callable
(WSGI application object) for itself.
"""
root = None
"""The top-most container of page handlers for this app. Handlers should
be arranged in a hierarchy of attributes, matching the expected URI
hierarchy; the default dispatcher then searches this hierarchy for a
matching handler. When using a dispatcher other than the default,
this value may be None."""
config = {}
"""A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
of {key: value} pairs."""
namespaces = _cpconfig.NamespaceSet()
toolboxes = {'tools': cherrypy.tools}
log = None
"""A LogManager instance. See _cplogging."""
wsgiapp = None
"""A CPWSGIApp instance. See _cpwsgi."""
request_class = _cprequest.Request
response_class = _cprequest.Response
relative_urls = False
def __init__(self, root, script_name="", config=None):
self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
self.root = root
self.script_name = script_name
self.wsgiapp = _cpwsgi.CPWSGIApp(self)
self.namespaces = self.namespaces.copy()
self.namespaces["log"] = lambda k, v: setattr(self.log, k, v)
self.namespaces["wsgi"] = self.wsgiapp.namespace_handler
self.config = self.__class__.config.copy()
if config:
self.merge(config)
def __repr__(self):
return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__,
self.root, self.script_name)
script_name_doc = """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.
script_name_doc = """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'].
"""
def _get_script_name(self):
if self._script_name is None:
# None signals that the script name should be pulled from WSGI environ.
return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/")
return self._script_name
if self._script_name is not None:
return self._script_name
# A `_script_name` with a value of None signals that the script name
# should be pulled from WSGI environ.
return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/")
def _set_script_name(self, value):
if value:
value = value.rstrip("/")
self._script_name = value
script_name = property(fget=_get_script_name, fset=_set_script_name,
doc=script_name_doc)
def merge(self, config):
"""Merge the given config into self.config."""
_cpconfig.merge(self.config, config)
# Handle namespaces specified in config.
self.namespaces(self.config.get("/", {}))
def find_config(self, path, key, default=None):
"""Return the most-specific value for key along path, or default."""
trail = path or "/"
while trail:
nodeconf = self.config.get(trail, {})
if key in nodeconf:
return nodeconf[key]
lastslash = trail.rfind("/")
if lastslash == -1:
break
@ -112,78 +116,79 @@ class Application(object):
trail = "/"
else:
trail = trail[:lastslash]
return default
def get_serving(self, local, remote, scheme, sproto):
"""Create and return a Request and Response object."""
req = self.request_class(local, remote, scheme, sproto)
req.app = self
for name, toolbox in self.toolboxes.items():
req.namespaces[name] = toolbox
resp = self.response_class()
cherrypy.serving.load(req, resp)
cherrypy.engine.publish('acquire_thread')
cherrypy.engine.publish('before_request')
return req, resp
def release_serving(self):
"""Release the current serving (request and response)."""
req = cherrypy.serving.request
cherrypy.engine.publish('after_request')
try:
req.close()
except:
cherrypy.log(traceback=True, severity=40)
cherrypy.serving.clear()
def __call__(self, environ, start_response):
return self.wsgiapp(environ, start_response)
class Tree(object):
"""A registry of CherryPy applications, mounted at diverse points.
An instance of this class may also be used as a WSGI callable
(WSGI application object), in which case it dispatches to all
mounted apps.
"""
apps = {}
"""
A dict of the form {script name: application}, where "script name"
is a string declaring the URI mount point (no trailing slash), and
"application" is an instance of cherrypy.Application (or an arbitrary
WSGI callable if you happen to be using a WSGI server)."""
def __init__(self):
self.apps = {}
def mount(self, root, script_name="", config=None):
"""Mount a new app from a root object, script_name, and config.
root
An instance of a "controller class" (a collection of page
handler methods) which represents the root of the application.
This may also be an Application instance, or None if using
a dispatcher other than the default.
script_name
A string containing the "mount point" of the application.
This should start with a slash, and be the path portion of the
URL at which to mount the given root. For example, if root.index()
will handle requests to "http://www.example.com:8080/dept/app1/",
then the script_name argument would be "/dept/app1".
It MUST NOT end in a slash. If the script_name refers to the
root of the URI, it MUST be an empty string (not "/").
config
A file or dict containing application config.
"""
@ -194,42 +199,43 @@ class Tree(object):
"order to inpect the WSGI environ for SCRIPT_NAME upon each "
"request). You cannot mount such Applications on this Tree; "
"you must pass them to a WSGI server interface directly.")
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip("/")
if isinstance(root, Application):
app = root
if script_name != "" and script_name != app.script_name:
raise ValueError("Cannot specify a different script name and "
"pass an Application instance to cherrypy.mount")
raise ValueError(
"Cannot specify a different script name and pass an "
"Application instance to cherrypy.mount")
script_name = app.script_name
else:
app = Application(root, script_name)
# If mounted at "", add favicon.ico
if (script_name == "" and root is not None
and not hasattr(root, "favicon_ico")):
favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
"favicon.ico")
root.favicon_ico = tools.staticfile.handler(favicon)
if config:
app.merge(config)
self.apps[script_name] = app
return app
def graft(self, wsgi_callable, script_name=""):
"""Mount a wsgi callable at the given script_name."""
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
script_name = script_name.rstrip("/")
self.apps[script_name] = wsgi_callable
def script_name(self, path=None):
"""The script_name of the app at the given path, or None.
If path is None, cherrypy.request is used.
"""
if path is None:
@ -239,17 +245,17 @@ class Tree(object):
request.path_info)
except AttributeError:
return None
while True:
if path in self.apps:
return path
if path == "":
return None
# Move one node up the tree and try again.
path = path[:path.rfind("/")]
def __call__(self, environ, start_response):
# If you're calling this, then you're probably setting SCRIPT_NAME
# to '' (some WSGI servers always set SCRIPT_NAME to '').
@ -263,9 +269,9 @@ class Tree(object):
if sn is None:
start_response('404 Not Found', [])
return []
app = self.apps[sn]
# Correct the SCRIPT_NAME and PATH_INFO environ entries.
environ = environ.copy()
if not py3k:
@ -273,7 +279,8 @@ class Tree(object):
# Python 2/WSGI u.0: all strings MUST be of type unicode
enc = environ[ntou('wsgi.url_encoding')]
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
@ -285,6 +292,8 @@ class Tree(object):
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')
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)

View File

@ -13,12 +13,13 @@ import cherrypy as _cherrypy
from cherrypy._cpcompat import BytesIO, bytestr, ntob, ntou, py3k, unicodestr
from cherrypy import _cperror
from cherrypy.lib import httputil
from cherrypy.lib import is_closable_iterator
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.
"""
env1x = {}
url_encoding = environ[ntou('wsgi.url_encoding')]
for k, v in list(environ.items()):
if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
@ -26,36 +27,37 @@ def downgrade_wsgi_ux_to_1x(environ):
elif isinstance(v, unicodestr):
v = v.encode('ISO-8859-1')
env1x[k.encode('ISO-8859-1')] = v
return env1x
class VirtualHost(object):
"""Select a different WSGI application based on the Host header.
This can be useful when running multiple sites within one CP server.
It allows several domains to point to different applications. For example::
root = Root()
RootApp = cherrypy.Application(root)
Domain2App = cherrypy.Application(root)
SecureApp = cherrypy.Application(Secure())
vhost = cherrypy._cpwsgi.VirtualHost(RootApp,
domains={'www.domain2.example': Domain2App,
'www.domain2.example:443': SecureApp,
})
cherrypy.tree.graft(vhost)
"""
default = None
"""Required. The default WSGI application."""
use_x_forwarded_host = True
"""If True (the default), any "X-Forwarded-Host"
request header will be used instead of the "Host" header. This
is commonly added by HTTP servers (such as Apache) when proxying."""
domains = {}
"""A dict of {host header value: application} pairs.
The incoming "Host" request header is looked up in this dict,
@ -64,17 +66,17 @@ class VirtualHost(object):
separate entries for "example.com" and "www.example.com".
In addition, "Host" headers may contain the port number.
"""
def __init__(self, default, domains=None, use_x_forwarded_host=True):
self.default = default
self.domains = domains or {}
self.use_x_forwarded_host = use_x_forwarded_host
def __call__(self, environ, start_response):
domain = environ.get('HTTP_HOST', '')
if self.use_x_forwarded_host:
domain = environ.get("HTTP_X_FORWARDED_HOST", domain)
nextapp = self.domains.get(domain)
if nextapp is None:
nextapp = self.default
@ -82,12 +84,13 @@ class VirtualHost(object):
class InternalRedirector(object):
"""WSGI middleware that handles raised cherrypy.InternalRedirect."""
def __init__(self, nextapp, recursive=False):
self.nextapp = nextapp
self.recursive = recursive
def __call__(self, environ, start_response):
redirections = []
while True:
@ -99,15 +102,16 @@ class InternalRedirector(object):
sn = environ.get('SCRIPT_NAME', '')
path = environ.get('PATH_INFO', '')
qs = environ.get('QUERY_STRING', '')
# Add the *previous* path_info + qs to redirections.
old_uri = sn + path
if qs:
old_uri += "?" + qs
redirections.append(old_uri)
if not self.recursive:
# Check to see if the new URI has been redirected to already
# Check to see if the new URI has been redirected to
# already
new_uri = sn + ir.path
if ir.query_string:
new_uri += "?" + ir.query_string
@ -115,7 +119,7 @@ class InternalRedirector(object):
ir.request.close()
raise RuntimeError("InternalRedirector visited the "
"same URL twice: %r" % new_uri)
# Munge the environment and try again.
environ['REQUEST_METHOD'] = "GET"
environ['PATH_INFO'] = ir.path
@ -126,44 +130,51 @@ class InternalRedirector(object):
class ExceptionTrapper(object):
"""WSGI middleware that traps exceptions."""
def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
self.nextapp = nextapp
self.throws = throws
def __call__(self, environ, start_response):
return _TrappedResponse(self.nextapp, environ, start_response, self.throws)
return _TrappedResponse(
self.nextapp,
environ,
start_response,
self.throws
)
class _TrappedResponse(object):
response = iter([])
def __init__(self, nextapp, environ, start_response, throws):
self.nextapp = nextapp
self.environ = environ
self.start_response = start_response
self.throws = throws
self.started_response = False
self.response = self.trap(self.nextapp, self.environ, self.start_response)
self.response = self.trap(
self.nextapp, self.environ, self.start_response)
self.iter_response = iter(self.response)
def __iter__(self):
self.started_response = True
return self
if py3k:
def __next__(self):
return self.trap(next, self.iter_response)
else:
def next(self):
return self.trap(self.iter_response.next)
def close(self):
if hasattr(self.response, 'close'):
self.response.close()
def trap(self, func, *args, **kwargs):
try:
return func(*args, **kwargs)
@ -188,7 +199,7 @@ class _TrappedResponse(object):
self.iter_response = iter([])
else:
self.iter_response = iter(b)
try:
self.start_response(s, h, _sys.exc_info())
except:
@ -199,7 +210,7 @@ class _TrappedResponse(object):
# But we still log and call close() to clean up ourselves.
_cherrypy.log(traceback=True, severity=40)
raise
if self.started_response:
return ntob("").join(b)
else:
@ -210,8 +221,9 @@ class _TrappedResponse(object):
class AppResponse(object):
"""WSGI response iterable for CherryPy applications."""
def __init__(self, environ, start_response, cpapp):
self.cpapp = cpapp
try:
@ -226,20 +238,24 @@ class AppResponse(object):
outstatus = r.output_status
if not isinstance(outstatus, bytestr):
raise TypeError("response.output_status is not a byte string.")
outheaders = []
for k, v in r.header_list:
if not isinstance(k, bytestr):
raise TypeError("response.header_list key %r is not a byte string." % k)
raise TypeError(
"response.header_list key %r is not a byte string." %
k)
if not isinstance(v, bytestr):
raise TypeError("response.header_list value %r is not a byte string." % v)
raise TypeError(
"response.header_list value %r is not a byte string." %
v)
outheaders.append((k, v))
if py3k:
# According to PEP 3333, when using Python 3, the response status
# and headers must be bytes masquerading as unicode; that is, they
# must be of type "str" but are restricted to code points in the
# "latin-1" set.
# According to PEP 3333, when using Python 3, the response
# status and headers must be bytes masquerading as unicode;
# that is, they must be of type "str" but are restricted to
# code points in the "latin-1" set.
outstatus = outstatus.decode('ISO-8859-1')
outheaders = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
for k, v in outheaders]
@ -249,34 +265,46 @@ class AppResponse(object):
except:
self.close()
raise
def __iter__(self):
return self
if py3k:
def __next__(self):
return next(self.iter_response)
else:
def next(self):
return self.iter_response.next()
def close(self):
"""Close and de-reference the current request and response. (Core)"""
streaming = _cherrypy.serving.response.stream
self.cpapp.release_serving()
# We avoid the expense of examining the iterator to see if it's
# closable unless we are streaming the response, as that's the
# only situation where we are going to have an iterator which
# may not have been exhausted yet.
if streaming and is_closable_iterator(self.iter_response):
iter_close = self.iter_response.close
try:
iter_close()
except Exception:
_cherrypy.log(traceback=True, severity=40)
def run(self):
"""Create a Request object using environ."""
env = self.environ.get
local = httputil.Host('', int(env('SERVER_PORT', 80)),
env('SERVER_NAME', ''))
env('SERVER_NAME', ''))
remote = httputil.Host(env('REMOTE_ADDR', ''),
int(env('REMOTE_PORT', -1) or -1),
env('REMOTE_HOST', ''))
scheme = env('wsgi.url_scheme')
sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1")
request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
# LOGON_USER is served by IIS, and is the name of the
# user after having been mapped to a local account.
# Both IIS and Apache set REMOTE_USER, when possible.
@ -285,24 +313,25 @@ class AppResponse(object):
request.multiprocess = self.environ['wsgi.multiprocess']
request.wsgi_environ = self.environ
request.prev = env('cherrypy.previous_request', None)
meth = self.environ['REQUEST_METHOD']
path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''),
self.environ.get('PATH_INFO', ''))
qs = self.environ.get('QUERY_STRING', '')
if py3k:
# 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.
# 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.
# 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)
@ -313,19 +342,19 @@ class AppResponse(object):
# Only set transcoded values if they both succeed.
path = u_path
qs = u_qs
rproto = self.environ.get('SERVER_PROTOCOL')
headers = self.translate_headers(self.environ)
rfile = self.environ['wsgi.input']
request.run(meth, path, qs, rproto, headers, rfile)
headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization',
'CONTENT_LENGTH': 'Content-Length',
'CONTENT_TYPE': 'Content-Type',
'REMOTE_HOST': 'Remote-Host',
'REMOTE_ADDR': 'Remote-Addr',
}
def translate_headers(self, environ):
"""Translate CGI-environ header names to HTTP header names."""
for cgiName in environ:
@ -339,8 +368,9 @@ class AppResponse(object):
class CPWSGIApp(object):
"""A WSGI application object for a CherryPy Application."""
pipeline = [('ExceptionTrapper', ExceptionTrapper),
('InternalRedirector', InternalRedirector),
]
@ -349,35 +379,36 @@ class CPWSGIApp(object):
plus optional keyword arguments, and returns a WSGI application
(that takes environ and start_response arguments). The 'name' can
be any you choose, and will correspond to keys in self.config."""
head = None
"""Rather than nest all apps in the pipeline on each call, it's only
done the first time, and the result is memoized into self.head. Set
this to None again if you change self.pipeline after calling self."""
config = {}
"""A dict whose keys match names listed in the pipeline. Each
value is a further dict which will be passed to the corresponding
named WSGI callable (from the pipeline) as keyword arguments."""
response_class = AppResponse
"""The class to instantiate and return as the next app in the WSGI chain."""
"""The class to instantiate and return as the next app in the WSGI chain.
"""
def __init__(self, cpapp, pipeline=None):
self.cpapp = cpapp
self.pipeline = self.pipeline[:]
if pipeline:
self.pipeline.extend(pipeline)
self.config = self.config.copy()
def tail(self, environ, start_response):
"""WSGI application callable for the actual CherryPy application.
You probably shouldn't call this; call self.__call__ instead,
so that any WSGI middleware in self.pipeline can run first.
"""
return self.response_class(environ, start_response, self.cpapp)
def __call__(self, environ, start_response):
head = self.head
if head is None:
@ -389,7 +420,7 @@ class CPWSGIApp(object):
head = callable(head, **conf)
self.head = head
return head(environ, start_response)
def namespace_handler(self, k, v):
"""Config handler for the 'wsgi' namespace."""
if k == "pipeline":
@ -405,4 +436,3 @@ class CPWSGIApp(object):
name, arg = k.split(".", 1)
bucket = self.config.setdefault(name, {})
bucket[arg] = v

View File

@ -8,32 +8,39 @@ from cherrypy import wsgiserver
class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
"""Wrapper for wsgiserver.CherryPyWSGIServer.
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_adapter=cherrypy.server):
self.server_adapter = server_adapter
self.max_request_header_size = self.server_adapter.max_request_header_size or 0
self.max_request_body_size = self.server_adapter.max_request_body_size or 0
self.max_request_header_size = (
self.server_adapter.max_request_header_size or 0
)
self.max_request_body_size = (
self.server_adapter.max_request_body_size or 0
)
server_name = (self.server_adapter.socket_host or
self.server_adapter.socket_file or
None)
self.wsgi_version = self.server_adapter.wsgi_version
s = wsgiserver.CherryPyWSGIServer
s.__init__(self, server_adapter.bind_addr, cherrypy.tree,
self.server_adapter.thread_pool,
server_name,
max = self.server_adapter.thread_pool_max,
request_queue_size = self.server_adapter.socket_queue_size,
timeout = self.server_adapter.socket_timeout,
shutdown_timeout = self.server_adapter.shutdown_timeout,
max=self.server_adapter.thread_pool_max,
request_queue_size=self.server_adapter.socket_queue_size,
timeout=self.server_adapter.socket_timeout,
shutdown_timeout=self.server_adapter.shutdown_timeout,
accepted_queue_size=self.server_adapter.accepted_queue_size,
accepted_queue_timeout=self.server_adapter.accepted_queue_timeout,
)
self.protocol = self.server_adapter.protocol_version
self.nodelay = self.server_adapter.nodelay
@ -55,9 +62,9 @@ class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
self.server_adapter.ssl_certificate,
self.server_adapter.ssl_private_key,
self.server_adapter.ssl_certificate_chain)
self.stats['Enabled'] = getattr(self.server_adapter, 'statistics', False)
self.stats['Enabled'] = getattr(
self.server_adapter, 'statistics', False)
def error_log(self, msg="", level=20, traceback=False):
cherrypy.engine.log(msg, level, traceback)

View File

@ -1,109 +1,6 @@
#! /usr/bin/env python
"""The CherryPy daemon."""
import sys
import cherrypy
from cherrypy.process import plugins, servers
from cherrypy import Application
def start(configfiles=None, daemonize=False, environment=None,
fastcgi=False, scgi=False, pidfile=None, imports=None,
cgi=False):
"""Subscribe all engine plugins and start the engine."""
sys.path = [''] + sys.path
for i in imports or []:
exec("import %s" % i)
for c in configfiles or []:
cherrypy.config.update(c)
# If there's only one app mounted, merge config into it.
if len(cherrypy.tree.apps) == 1:
for app in cherrypy.tree.apps.values():
if isinstance(app, Application):
app.merge(c)
engine = cherrypy.engine
if environment is not None:
cherrypy.config.update({'environment': environment})
# Only daemonize if asked to.
if daemonize:
# Don't print anything to stdout/sterr.
cherrypy.config.update({'log.screen': False})
plugins.Daemonizer(engine).subscribe()
if pidfile:
plugins.PIDFile(engine, pidfile).subscribe()
if hasattr(engine, "signal_handler"):
engine.signal_handler.subscribe()
if hasattr(engine, "console_control_handler"):
engine.console_control_handler.subscribe()
if (fastcgi and (scgi or cgi)) or (scgi and cgi):
cherrypy.log.error("You may only specify one of the cgi, fastcgi, and "
"scgi options.", 'ENGINE')
sys.exit(1)
elif fastcgi or scgi or cgi:
# Turn off autoreload when using *cgi.
cherrypy.config.update({'engine.autoreload_on': False})
# Turn off the default HTTP server (which is subscribed by default).
cherrypy.server.unsubscribe()
addr = cherrypy.server.bind_addr
if fastcgi:
f = servers.FlupFCGIServer(application=cherrypy.tree,
bindAddress=addr)
elif scgi:
f = servers.FlupSCGIServer(application=cherrypy.tree,
bindAddress=addr)
else:
f = servers.FlupCGIServer(application=cherrypy.tree,
bindAddress=addr)
s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
s.subscribe()
# Always start the engine; this will start all other services
try:
engine.start()
except:
# Assume the error has been logged already via bus.log.
sys.exit(1)
else:
engine.block()
import cherrypy.daemon
if __name__ == '__main__':
from optparse import OptionParser
p = OptionParser()
p.add_option('-c', '--config', action="append", dest='config',
help="specify config file(s)")
p.add_option('-d', action="store_true", dest='daemonize',
help="run the server as a daemon")
p.add_option('-e', '--environment', dest='environment', default=None,
help="apply the given config environment")
p.add_option('-f', action="store_true", dest='fastcgi',
help="start a fastcgi server instead of the default HTTP server")
p.add_option('-s', action="store_true", dest='scgi',
help="start a scgi server instead of the default HTTP server")
p.add_option('-x', action="store_true", dest='cgi',
help="start a cgi server instead of the default HTTP server")
p.add_option('-i', '--import', action="append", dest='imports',
help="specify modules to import")
p.add_option('-p', '--pidfile', dest='pidfile', default=None,
help="store the process id in the given file")
p.add_option('-P', '--Path', action="append", dest='Path',
help="add the given paths to sys.path")
options, args = p.parse_args()
if options.Path:
for p in options.Path:
sys.path.insert(0, p)
start(options.config, options.daemonize,
options.environment, options.fastcgi, options.scgi,
options.pidfile, options.imports, options.cgi)
cherrypy.daemon.run()

109
libs/cherrypy/daemon.py Normal file
View File

@ -0,0 +1,109 @@
"""The CherryPy daemon."""
import sys
import cherrypy
from cherrypy.process import plugins, servers
from cherrypy import Application
def start(configfiles=None, daemonize=False, environment=None,
fastcgi=False, scgi=False, pidfile=None, imports=None,
cgi=False):
"""Subscribe all engine plugins and start the engine."""
sys.path = [''] + sys.path
for i in imports or []:
exec("import %s" % i)
for c in configfiles or []:
cherrypy.config.update(c)
# If there's only one app mounted, merge config into it.
if len(cherrypy.tree.apps) == 1:
for app in cherrypy.tree.apps.values():
if isinstance(app, Application):
app.merge(c)
engine = cherrypy.engine
if environment is not None:
cherrypy.config.update({'environment': environment})
# Only daemonize if asked to.
if daemonize:
# Don't print anything to stdout/sterr.
cherrypy.config.update({'log.screen': False})
plugins.Daemonizer(engine).subscribe()
if pidfile:
plugins.PIDFile(engine, pidfile).subscribe()
if hasattr(engine, "signal_handler"):
engine.signal_handler.subscribe()
if hasattr(engine, "console_control_handler"):
engine.console_control_handler.subscribe()
if (fastcgi and (scgi or cgi)) or (scgi and cgi):
cherrypy.log.error("You may only specify one of the cgi, fastcgi, and "
"scgi options.", 'ENGINE')
sys.exit(1)
elif fastcgi or scgi or cgi:
# Turn off autoreload when using *cgi.
cherrypy.config.update({'engine.autoreload_on': False})
# Turn off the default HTTP server (which is subscribed by default).
cherrypy.server.unsubscribe()
addr = cherrypy.server.bind_addr
if fastcgi:
f = servers.FlupFCGIServer(application=cherrypy.tree,
bindAddress=addr)
elif scgi:
f = servers.FlupSCGIServer(application=cherrypy.tree,
bindAddress=addr)
else:
f = servers.FlupCGIServer(application=cherrypy.tree,
bindAddress=addr)
s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
s.subscribe()
# Always start the engine; this will start all other services
try:
engine.start()
except:
# Assume the error has been logged already via bus.log.
sys.exit(1)
else:
engine.block()
def run():
from optparse import OptionParser
p = OptionParser()
p.add_option('-c', '--config', action="append", dest='config',
help="specify config file(s)")
p.add_option('-d', action="store_true", dest='daemonize',
help="run the server as a daemon")
p.add_option('-e', '--environment', dest='environment', default=None,
help="apply the given config environment")
p.add_option('-f', action="store_true", dest='fastcgi',
help="start a fastcgi server instead of the default HTTP "
"server")
p.add_option('-s', action="store_true", dest='scgi',
help="start a scgi server instead of the default HTTP server")
p.add_option('-x', action="store_true", dest='cgi',
help="start a cgi server instead of the default HTTP server")
p.add_option('-i', '--import', action="append", dest='imports',
help="specify modules to import")
p.add_option('-p', '--pidfile', dest='pidfile', default=None,
help="store the process id in the given file")
p.add_option('-P', '--Path', action="append", dest='Path',
help="add the given paths to sys.path")
options, args = p.parse_args()
if options.Path:
for p in options.Path:
sys.path.insert(0, p)
start(options.config, options.daemonize,
options.environment, options.fastcgi, options.scgi,
options.pidfile, options.imports, options.cgi)

View File

@ -3,16 +3,54 @@
# Deprecated in CherryPy 3.2 -- remove in CherryPy 3.3
from cherrypy.lib.reprconf import unrepr, modules, attributes
class file_generator(object):
"""Yield the given input (a file object) in chunks (default 64k). (Core)"""
def is_iterator(obj):
'''Returns a boolean indicating if the object provided implements
the iterator protocol (i.e. like a generator). This will return
false for objects which iterable, but not iterators themselves.'''
from types import GeneratorType
if isinstance(obj, GeneratorType):
return True
elif not hasattr(obj, '__iter__'):
return False
else:
# Types which implement the protocol must return themselves when
# invoking 'iter' upon them.
return iter(obj) is obj
def is_closable_iterator(obj):
# Not an iterator.
if not is_iterator(obj):
return False
# A generator - the easiest thing to deal with.
import inspect
if inspect.isgenerator(obj):
return True
# A custom iterator. Look for a close method...
if not (hasattr(obj, 'close') and callable(obj.close)):
return False
# ... which doesn't require any arguments.
try:
inspect.getcallargs(obj.close)
except TypeError:
return False
else:
return True
class file_generator(object):
"""Yield the given input (a file object) in chunks (default 64k). (Core)"""
def __init__(self, input, chunkSize=65536):
self.input = input
self.chunkSize = chunkSize
def __iter__(self):
return self
def __next__(self):
chunk = self.input.read(self.chunkSize)
if chunk:
@ -23,6 +61,7 @@ class file_generator(object):
raise StopIteration()
next = __next__
def file_generator_limited(fileobj, count, chunk_size=65536):
"""Yield the given file object in chunks, stopping after `count`
bytes has been emitted. Default chunk size is 64kB. (Core)
@ -36,6 +75,7 @@ def file_generator_limited(fileobj, count, chunk_size=65536):
remaining -= chunklen
yield chunk
def set_vary_header(response, header_name):
"Add a Vary header to a response"
varies = response.headers.get("Vary", "")

View File

@ -3,25 +3,27 @@ from cherrypy.lib import httpauth
def check_auth(users, encrypt=None, realm=None):
"""If an authorization header contains credentials, return True, else False."""
"""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
users = users() # expect it to return a dictionary
if not isinstance(users, dict):
raise ValueError("Authentication users must be a dictionary")
raise ValueError(
"Authentication users must be a dictionary")
# fetch the user password
password = users.get(ah["username"], None)
except TypeError:
@ -30,58 +32,66 @@ def check_auth(users, encrypt=None, realm=None):
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.
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")
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.
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")
cherrypy.serving.response.headers[
'www-authenticate'] = httpauth.digestAuth(realm)
raise cherrypy.HTTPError(
401, "You are not authorized to access that resource")

View File

@ -3,7 +3,8 @@
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
__doc__ = """This module provides a CherryPy 3.x tool which implements
the server-side of HTTP Basic Access Authentication, as described in :rfc:`2617`.
the server-side of HTTP Basic Access Authentication, as described in
:rfc:`2617`.
Example usage, using the built-in checkpassword_dict function which uses a dict
as the credentials store::
@ -60,13 +61,13 @@ def basic_auth(realm, checkpassword, debug=False):
username and password are the values obtained from the request's
'authorization' header. If authentication succeeds, checkpassword
returns True, else it returns False.
"""
if '"' in realm:
raise ValueError('Realm cannot contain the " (quote) character.')
request = cherrypy.serving.request
auth_header = request.headers.get('authorization')
if auth_header is not None:
try:
@ -77,11 +78,13 @@ def basic_auth(realm, checkpassword, debug=False):
if debug:
cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')
request.login = username
return # successful authentication
except (ValueError, binascii.Error): # split() error, base64.decodestring() error
return # successful authentication
# split() error, base64.decodestring() error
except (ValueError, binascii.Error):
raise cherrypy.HTTPError(400, 'Bad Request')
# Respond with 401 status and a WWW-Authenticate header
cherrypy.serving.response.headers['www-authenticate'] = 'Basic realm="%s"' % realm
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
# Respond with 401 status and a WWW-Authenticate header
cherrypy.serving.response.headers[
'www-authenticate'] = 'Basic realm="%s"' % realm
raise cherrypy.HTTPError(
401, "You are not authorized to access that resource")

View File

@ -41,6 +41,8 @@ def TRACE(msg):
# Three helper functions for users of the tool, providing three variants
# of get_ha1() functions for three different kinds of credential stores.
def get_ha1_dict_plain(user_password_dict):
"""Returns a get_ha1 function which obtains a plaintext password from a
dictionary of the form: {username : password}.
@ -57,6 +59,7 @@ def get_ha1_dict_plain(user_password_dict):
return get_ha1
def get_ha1_dict(user_ha1_dict):
"""Returns a get_ha1 function which obtains a HA1 password hash from a
dictionary of the form: {username : HA1}.
@ -67,10 +70,11 @@ def get_ha1_dict(user_ha1_dict):
argument to digest_auth().
"""
def get_ha1(realm, username):
return user_ha1_dict.get(user)
return user_ha1_dict.get(username)
return get_ha1
def get_ha1_file_htdigest(filename):
"""Returns a get_ha1 function which obtains a HA1 password hash from a
flat file with lines of the same format as that produced by the Apache
@ -99,18 +103,19 @@ def get_ha1_file_htdigest(filename):
def synthesize_nonce(s, key, timestamp=None):
"""Synthesize a nonce value which resists spoofing and can be checked for staleness.
Returns a string suitable as the value for 'nonce' in the www-authenticate header.
"""Synthesize a nonce value which resists spoofing and can be checked
for staleness. Returns a string suitable as the value for 'nonce' in
the www-authenticate header.
s
A string related to the resource, such as the hostname of the server.
key
A secret string known only to the server.
timestamp
An integer seconds-since-the-epoch timestamp
"""
if timestamp is None:
timestamp = int(time.time())
@ -125,6 +130,7 @@ def H(s):
class HttpDigestAuthorization (object):
"""Class to parse a Digest Authorization header and perform re-calculation
of the digest.
"""
@ -135,7 +141,7 @@ class HttpDigestAuthorization (object):
def __init__(self, auth_header, http_method, debug=False):
self.http_method = http_method
self.debug = debug
scheme, params = auth_header.split(" ", 1)
scheme, params = auth_header.split(" ", 1)
self.scheme = scheme.lower()
if self.scheme != 'digest':
raise ValueError('Authorization scheme is not "Digest"')
@ -151,84 +157,95 @@ class HttpDigestAuthorization (object):
self.nonce = paramsd.get('nonce')
self.uri = paramsd.get('uri')
self.method = paramsd.get('method')
self.response = paramsd.get('response') # the response digest
self.algorithm = paramsd.get('algorithm', 'MD5')
self.response = paramsd.get('response') # the response digest
self.algorithm = paramsd.get('algorithm', 'MD5').upper()
self.cnonce = paramsd.get('cnonce')
self.opaque = paramsd.get('opaque')
self.qop = paramsd.get('qop') # qop
self.nc = paramsd.get('nc') # nonce count
self.qop = paramsd.get('qop') # qop
self.nc = paramsd.get('nc') # nonce count
# perform some correctness checks
if self.algorithm not in valid_algorithms:
raise ValueError(self.errmsg("Unsupported value for algorithm: '%s'" % self.algorithm))
raise ValueError(
self.errmsg("Unsupported value for algorithm: '%s'" %
self.algorithm))
has_reqd = self.username and \
self.realm and \
self.nonce and \
self.uri and \
self.response
has_reqd = (
self.username and
self.realm and
self.nonce and
self.uri and
self.response
)
if not has_reqd:
raise ValueError(self.errmsg("Not all required parameters are present."))
raise ValueError(
self.errmsg("Not all required parameters are present."))
if self.qop:
if self.qop not in valid_qops:
raise ValueError(self.errmsg("Unsupported value for qop: '%s'" % self.qop))
raise ValueError(
self.errmsg("Unsupported value for qop: '%s'" % self.qop))
if not (self.cnonce and self.nc):
raise ValueError(self.errmsg("If qop is sent then cnonce and nc MUST be present"))
raise ValueError(
self.errmsg("If qop is sent then "
"cnonce and nc MUST be present"))
else:
if self.cnonce or self.nc:
raise ValueError(self.errmsg("If qop is not sent, neither cnonce nor nc can be present"))
raise ValueError(
self.errmsg("If qop is not sent, "
"neither cnonce nor nc can be present"))
def __str__(self):
return 'authorization : %s' % self.auth_header
def validate_nonce(self, s, key):
"""Validate the nonce.
Returns True if nonce was generated by synthesize_nonce() and the timestamp
is not spoofed, else returns False.
Returns True if nonce was generated by synthesize_nonce() and the
timestamp is not spoofed, else returns False.
s
A string related to the resource, such as the hostname of the server.
A string related to the resource, such as the hostname of
the server.
key
A secret string known only to the server.
Both s and key must be the same values which were used to synthesize the nonce
we are trying to validate.
Both s and key must be the same values which were used to synthesize
the nonce we are trying to validate.
"""
try:
timestamp, hashpart = self.nonce.split(':', 1)
s_timestamp, s_hashpart = synthesize_nonce(s, key, timestamp).split(':', 1)
s_timestamp, s_hashpart = synthesize_nonce(
s, key, timestamp).split(':', 1)
is_valid = s_hashpart == hashpart
if self.debug:
TRACE('validate_nonce: %s' % is_valid)
return is_valid
except ValueError: # split() error
except ValueError: # split() error
pass
return False
def is_nonce_stale(self, max_age_seconds=600):
"""Returns True if a validated nonce is stale. The nonce contains a
timestamp in plaintext and also a secure hash of the timestamp. You should
first validate the nonce to ensure the plaintext timestamp is not spoofed.
timestamp in plaintext and also a secure hash of the timestamp.
You should first validate the nonce to ensure the plaintext
timestamp is not spoofed.
"""
try:
timestamp, hashpart = self.nonce.split(':', 1)
if int(timestamp) + max_age_seconds > int(time.time()):
return False
except ValueError: # int() error
except ValueError: # int() error
pass
if self.debug:
TRACE("nonce is stale")
return True
def HA2(self, entity_body=''):
"""Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3."""
# RFC 2617 3.2.2.3
# If the "qop" directive's value is "auth" or is unspecified, then A2 is:
# If the "qop" directive's value is "auth" or is unspecified,
# then A2 is:
# A2 = method ":" digest-uri-value
#
# If the "qop" value is "auth-int", then A2 is:
@ -238,11 +255,11 @@ class HttpDigestAuthorization (object):
elif self.qop == "auth-int":
a2 = "%s:%s:%s" % (self.http_method, self.uri, H(entity_body))
else:
# in theory, this should never happen, since I validate qop in __init__()
# in theory, this should never happen, since I validate qop in
# __init__()
raise ValueError(self.errmsg("Unrecognized value for qop!"))
return H(a2)
def request_digest(self, ha1, entity_body=''):
"""Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1.
@ -253,22 +270,24 @@ class HttpDigestAuthorization (object):
If 'qop' is set to 'auth-int', then A2 includes a hash
of the "entity body". The entity body is the part of the
message which follows the HTTP headers. See :rfc:`2617` section
4.3. This refers to the entity the user agent sent in the request which
has the Authorization header. Typically GET requests don't have an entity,
and POST requests do.
4.3. This refers to the entity the user agent sent in the
request which has the Authorization header. Typically GET
requests don't have an entity, and POST requests do.
"""
ha2 = self.HA2(entity_body)
# Request-Digest -- RFC 2617 3.2.2.1
if self.qop:
req = "%s:%s:%s:%s:%s" % (self.nonce, self.nc, self.cnonce, self.qop, ha2)
req = "%s:%s:%s:%s:%s" % (
self.nonce, self.nc, self.cnonce, self.qop, ha2)
else:
req = "%s:%s" % (self.nonce, ha2)
# RFC 2617 3.2.2.2
#
# If the "algorithm" directive's value is "MD5" or is unspecified, then A1 is:
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
# If the "algorithm" directive's value is "MD5" or is unspecified,
# then A1 is:
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
#
# If the "algorithm" directive's value is "MD5-sess", then A1 is
# calculated only once - on the first request by the client following
@ -282,8 +301,8 @@ class HttpDigestAuthorization (object):
return digest
def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stale=False):
def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth,
stale=False):
"""Constructs a WWW-Authenticate header for Digest authentication."""
if qop not in valid_qops:
raise ValueError("Unsupported value for qop: '%s'" % qop)
@ -293,7 +312,7 @@ def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stal
if nonce is None:
nonce = synthesize_nonce(realm, key)
s = 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
realm, nonce, algorithm, qop)
realm, nonce, algorithm, qop)
if stale:
s += ', stale="true"'
return s
@ -302,16 +321,16 @@ def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stal
def digest_auth(realm, get_ha1, key, debug=False):
"""A CherryPy tool which hooks at before_handler to perform
HTTP Digest Access Authentication, as specified in :rfc:`2617`.
If the request has an 'authorization' header with a 'Digest' scheme, this
tool authenticates the credentials supplied in that header. If
the request has no 'authorization' header, or if it does but the scheme is
not "Digest", or if authentication fails, the tool sends a 401 response with
a 'WWW-Authenticate' Digest header.
If the request has an 'authorization' header with a 'Digest' scheme,
this tool authenticates the credentials supplied in that header.
If the request has no 'authorization' header, or if it does but the
scheme is not "Digest", or if authentication fails, the tool sends
a 401 response with a 'WWW-Authenticate' Digest header.
realm
A string containing the authentication realm.
get_ha1
A callable which looks up a username in a credentials store
and returns the HA1 string, which is defined in the RFC to be
@ -320,46 +339,52 @@ def digest_auth(realm, get_ha1, key, debug=False):
where username is obtained from the request's 'authorization' header.
If username is not found in the credentials store, get_ha1() returns
None.
key
A secret string known only to the server, used in the synthesis of nonces.
A secret string known only to the server, used in the synthesis
of nonces.
"""
request = cherrypy.serving.request
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)
auth = HttpDigestAuthorization(
auth_header, request.method, debug=debug)
except ValueError:
raise cherrypy.HTTPError(400, "The Authorization header could not be parsed.")
raise cherrypy.HTTPError(
400, "The Authorization header could not be parsed.")
if debug:
TRACE(str(auth))
if auth.validate_nonce(realm, key):
ha1 = get_ha1(realm, auth.username)
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.
# 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 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
# 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)
TRACE("authentication of %s successful" %
auth.username)
return
# Respond with 401 status and a WWW-Authenticate header
header = www_authenticate(realm, key, stale=nonce_is_stale)
if debug:
TRACE(header)
cherrypy.serving.response.headers['WWW-Authenticate'] = header
raise cherrypy.HTTPError(401, "You are not authorized to access that resource")
raise cherrypy.HTTPError(
401, "You are not authorized to access that resource")

View File

@ -1,7 +1,7 @@
"""
CherryPy implements a simple caching system as a pluggable Tool. This tool tries
to be an (in-process) HTTP/1.1-compliant cache. It's not quite there yet, but
it's probably good enough for most sites.
CherryPy implements a simple caching system as a pluggable Tool. This tool
tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there
yet, but it's probably good enough for most sites.
In general, GET responses are cached (along with selecting headers) and, if
another request arrives for the same resource, the caching Tool will return 304
@ -9,8 +9,8 @@ Not Modified if possible, or serve the cached response otherwise. It also sets
request.cached to True if serving a cached representation, and sets
request.cacheable to False (so it doesn't get cached again).
If POST, PUT, or DELETE requests are made for a cached resource, they invalidate
(delete) any cached response.
If POST, PUT, or DELETE requests are made for a cached resource, they
invalidate (delete) any cached response.
Usage
=====
@ -39,58 +39,58 @@ import time
import cherrypy
from cherrypy.lib import cptools, httputil
from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted
from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted, Event
class Cache(object):
"""Base class for Cache implementations."""
def get(self):
"""Return the current variant if in the cache, else None."""
raise NotImplemented
def put(self, obj, size):
"""Store the current variant in the cache."""
raise NotImplemented
def delete(self):
"""Remove ALL cached variants of the current resource."""
raise NotImplemented
def clear(self):
"""Reset the cache to its initial, empty state."""
raise NotImplemented
# ------------------------------- Memory Cache ------------------------------- #
# ------------------------------ Memory Cache ------------------------------- #
class AntiStampedeCache(dict):
"""A storage system for cached items which reduces stampede collisions."""
def wait(self, key, timeout=5, debug=False):
"""Return the cached value for the given key, or None.
If timeout is not None, and the value is already
being calculated by another thread, wait until the given timeout has
elapsed. If the value is available before the timeout expires, it is
returned. If not, None is returned, and a sentinel placed in the cache
to signal other threads to wait.
If timeout is None, no waiting is performed nor sentinels used.
"""
value = self.get(key)
if isinstance(value, threading._Event):
if isinstance(value, Event):
if timeout is None:
# Ignore the other thread and recalc it ourselves.
if debug:
cherrypy.log('No timeout', 'TOOLS.CACHING')
return None
# Wait until it's done or times out.
if debug:
cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING')
cherrypy.log('Waiting up to %s seconds' %
timeout, 'TOOLS.CACHING')
value.wait(timeout)
if value.result is not None:
# The other thread finished its calculation. Use it.
@ -104,7 +104,7 @@ class AntiStampedeCache(dict):
e = threading.Event()
e.result = None
dict.__setitem__(self, key, e)
return None
elif value is None:
# Stick an Event in the slot so other threads wait
@ -115,12 +115,12 @@ class AntiStampedeCache(dict):
e.result = None
dict.__setitem__(self, key, e)
return value
def __setitem__(self, key, value):
"""Set the cached value for the given key."""
existing = self.get(key)
dict.__setitem__(self, key, value)
if isinstance(existing, threading._Event):
if isinstance(existing, Event):
# Set Event.result so other threads waiting on it have
# immediate access without needing to poll the cache again.
existing.result = value
@ -128,49 +128,51 @@ class AntiStampedeCache(dict):
class MemoryCache(Cache):
"""An in-memory cache for varying response content.
Each key in self.store is a URI, and each value is an AntiStampedeCache.
The response for any given URI may vary based on the values of
"selecting request headers"; that is, those named in the Vary
response header. We assume the list of header names to be constant
for each URI throughout the lifetime of the application, and store
that list in ``self.store[uri].selecting_headers``.
The items contained in ``self.store[uri]`` have keys which are tuples of
request header values (in the same order as the names in its
selecting_headers), and values which are the actual responses.
"""
maxobjects = 1000
"""The maximum number of cached objects; defaults to 1000."""
maxobj_size = 100000
"""The maximum size of each cached object in bytes; defaults to 100 KB."""
maxsize = 10000000
"""The maximum size of the entire cache in bytes; defaults to 10 MB."""
delay = 600
"""Seconds until the cached content expires; defaults to 600 (10 minutes)."""
"""Seconds until the cached content expires; defaults to 600 (10 minutes).
"""
antistampede_timeout = 5
"""Seconds to wait for other threads to release a cache lock."""
expire_freq = 0.1
"""Seconds to sleep between cache expiration sweeps."""
debug = False
def __init__(self):
self.clear()
# Run self.expire_cache in a separate daemon thread.
t = threading.Thread(target=self.expire_cache, name='expire_cache')
self.expiration_thread = t
set_daemon(t, True)
t.start()
def clear(self):
"""Reset the cache to its initial, empty state."""
self.store = {}
@ -181,10 +183,10 @@ class MemoryCache(Cache):
self.tot_expires = 0
self.tot_non_modified = 0
self.cursize = 0
def expire_cache(self):
"""Continuously examine cached objects, expiring stale ones.
This function is designed to be run in its own daemon thread,
referenced at ``self.expiration_thread``.
"""
@ -207,17 +209,17 @@ class MemoryCache(Cache):
pass
del self.expirations[expiration_time]
time.sleep(self.expire_freq)
def get(self):
"""Return the current variant if in the cache, else None."""
request = cherrypy.serving.request
self.tot_gets += 1
uri = cherrypy.url(qs=request.query_string)
uricache = self.store.get(uri)
if uricache is None:
return None
header_values = [request.headers.get(h, '')
for h in uricache.selecting_headers]
variant = uricache.wait(key=tuple(sorted(header_values)),
@ -226,12 +228,12 @@ class MemoryCache(Cache):
if variant is not None:
self.tot_hist += 1
return variant
def put(self, variant, size):
"""Store the current variant in the cache."""
request = cherrypy.serving.request
response = cherrypy.serving.response
uri = cherrypy.url(qs=request.query_string)
uricache = self.store.get(uri)
if uricache is None:
@ -239,24 +241,24 @@ class MemoryCache(Cache):
uricache.selecting_headers = [
e.value for e in response.headers.elements('Vary')]
self.store[uri] = uricache
if len(self.store) < self.maxobjects:
total_size = self.cursize + size
# checks if there's space for the object
if (size < self.maxobj_size and total_size < self.maxsize):
# add to the expirations list
expiration_time = response.time + self.delay
bucket = self.expirations.setdefault(expiration_time, [])
bucket.append((size, uri, uricache.selecting_headers))
# add to the cache
header_values = [request.headers.get(h, '')
for h in uricache.selecting_headers]
uricache[tuple(sorted(header_values))] = variant
self.tot_puts += 1
self.cursize = total_size
def delete(self):
"""Remove ALL cached variants of the current resource."""
uri = cherrypy.url(qs=cherrypy.serving.request.query_string)
@ -265,12 +267,12 @@ class MemoryCache(Cache):
def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
"""Try to obtain cached output. If fresh enough, raise HTTPError(304).
If POST, PUT, or DELETE:
* invalidates (deletes) any cached response for this resource
* sets request.cached = False
* sets request.cacheable = False
else if a cached copy exists:
* sets request.cached = True
* sets request.cacheable = False
@ -280,7 +282,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
if necessary.
* sets response.status and response.body to the cached values
* returns True
otherwise:
* sets request.cached = False
* sets request.cacheable = True
@ -288,16 +290,16 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
"""
request = cherrypy.serving.request
response = cherrypy.serving.response
if not hasattr(cherrypy, "_cache"):
# Make a process-wide Cache object.
cherrypy._cache = kwargs.pop("cache_class", MemoryCache)()
# Take all remaining kwargs and set them on the Cache object.
for k, v in kwargs.items():
setattr(cherrypy._cache, k, v)
cherrypy._cache.debug = debug
# POST, PUT, DELETE should invalidate (delete) the cached copy.
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
if request.method in invalid_methods:
@ -308,12 +310,12 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
request.cached = False
request.cacheable = False
return False
if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
request.cached = False
request.cacheable = True
return False
cache_data = cherrypy._cache.get()
request.cached = bool(cache_data)
request.cacheable = not request.cached
@ -325,17 +327,19 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
directive = atoms.pop(0)
if directive == 'max-age':
if len(atoms) != 1 or not atoms[0].isdigit():
raise cherrypy.HTTPError(400, "Invalid Cache-Control header")
raise cherrypy.HTTPError(
400, "Invalid Cache-Control header")
max_age = int(atoms[0])
break
elif directive == 'no-cache':
if debug:
cherrypy.log('Ignoring cache due to Cache-Control: no-cache',
'TOOLS.CACHING')
cherrypy.log(
'Ignoring cache due to Cache-Control: no-cache',
'TOOLS.CACHING')
request.cached = False
request.cacheable = True
return False
if debug:
cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
s, h, b, create_time = cache_data
@ -347,15 +351,16 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
request.cached = False
request.cacheable = True
return False
# Copy the response headers. See http://www.cherrypy.org/ticket/721.
# Copy the response headers. See
# https://bitbucket.org/cherrypy/cherrypy/issue/721.
response.headers = rh = httputil.HeaderMap()
for k in h:
dict.__setitem__(rh, k, dict.__getitem__(h, k))
# Add the required Age header
response.headers["Age"] = str(age)
try:
# Note that validate_since depends on a Last-Modified header;
# this was put into the cached copy, and should have been
@ -366,7 +371,7 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
if x.status == 304:
cherrypy._cache.tot_non_modified += 1
raise
# serve it & get out from the request
response.status = s
response.body = b
@ -379,29 +384,29 @@ def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs):
def tee_output():
"""Tee response output to cache storage. Internal."""
# Used by CachingTool by attaching to request.hooks
request = cherrypy.serving.request
if 'no-store' in request.headers.values('Cache-Control'):
return
def tee(body):
"""Tee response.body into a list."""
if ('no-cache' in response.headers.values('Pragma') or
'no-store' in response.headers.values('Cache-Control')):
'no-store' in response.headers.values('Cache-Control')):
for chunk in body:
yield chunk
return
output = []
for chunk in body:
output.append(chunk)
yield chunk
# save the cache data
body = ntob('').join(output)
cherrypy._cache.put((response.status, response.headers or {},
body, response.time), len(body))
response = cherrypy.serving.response
response.body = tee(response.body)
@ -415,25 +420,25 @@ def expires(secs=0, force=False, debug=False):
expire. The 'Expires' header will be set to response.time + secs.
If secs is zero, the 'Expires' header is set one year in the past, and
the following "cache prevention" headers are also set:
* Pragma: no-cache
* Cache-Control': no-cache, must-revalidate
force
If False, the following headers are checked:
* Etag
* Last-Modified
* Age
* Expires
If any are already present, none of the above response headers are set.
"""
response = cherrypy.serving.response
headers = response.headers
cacheable = False
if not force:
# some header names that indicate that the response can be cached
@ -441,7 +446,7 @@ def expires(secs=0, force=False, debug=False):
if indicator in headers:
cacheable = True
break
if not cacheable and not force:
if debug:
cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES')
@ -450,7 +455,7 @@ def expires(secs=0, force=False, debug=False):
cherrypy.log('request is cacheable', 'TOOLS.EXPIRES')
if isinstance(secs, datetime.timedelta):
secs = (86400 * secs.days) + secs.seconds
if secs == 0:
if force or ("Pragma" not in headers):
headers["Pragma"] = "no-cache"

View File

@ -1,7 +1,7 @@
"""Code-coverage tools for CherryPy.
To use this module, or the coverage tools in the test suite,
you need to download 'coverage.py', either Gareth Rees' `original
you need to download 'coverage.py', either Gareth Rees' `original
implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
or Ned Batchelder's `enhanced version:
<http://www.nedbatchelder.com/code/modules/coverage.html>`_
@ -24,23 +24,27 @@ import re
import sys
import cgi
from cherrypy._cpcompat import quote_plus
import os, os.path
import os
import os.path
localFile = os.path.join(os.path.dirname(__file__), "coverage.cache")
the_coverage = None
try:
from coverage import coverage
the_coverage = coverage(data_file=localFile)
def start():
the_coverage.start()
except ImportError:
# Setting the_coverage to None will raise errors
# that need to be trapped downstream.
the_coverage = None
import warnings
warnings.warn("No code coverage will be performed; coverage.py could not be imported.")
warnings.warn(
"No code coverage will be performed; "
"coverage.py could not be imported.")
def start():
pass
start.priority = 20
@ -69,7 +73,7 @@ TEMPLATE_MENU = """<html>
font-size: small;
font-weight: bold;
font-style: italic;
margin-top: 5px;
margin-top: 5px;
}
input { border: 1px solid #ccc; padding: 2px; }
.directory {
@ -118,15 +122,18 @@ TEMPLATE_FORM = """
<div id="options">
<form action='menu' method=GET>
<input type='hidden' name='base' value='%(base)s' />
Show percentages <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
Hide files over <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
Show percentages
<input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
Hide files over
<input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
Exclude files matching<br />
<input type='text' id='exclude' name='exclude' value='%(exclude)s' size='20' />
<input type='text' id='exclude' name='exclude'
value='%(exclude)s' size='20' />
<br />
<input type='submit' value='Change view' id="submit"/>
</form>
</div>"""
</div>"""
TEMPLATE_FRAMESET = """<html>
<head><title>CherryPy coverage data</title></head>
@ -173,7 +180,10 @@ TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
<td>%s</td>
</tr>\n"""
TEMPLATE_ITEM = "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
TEMPLATE_ITEM = (
"%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
)
def _percent(statements, missing):
s = len(statements)
@ -182,24 +192,31 @@ def _percent(statements, missing):
return int(round(100.0 * e / s))
return 0
def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
coverage=the_coverage):
# Show the directory name and any of our children
dirs = [k for k, v in root.items() if v]
dirs.sort()
for name in dirs:
newpath = os.path.join(path, name)
if newpath.lower().startswith(base):
relpath = newpath[len(base):]
yield "| " * relpath.count(os.sep)
yield "<a class='directory' href='menu?base=%s&exclude=%s'>%s</a>\n" % \
(newpath, quote_plus(exclude), name)
for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage):
yield (
"<a class='directory' "
"href='menu?base=%s&exclude=%s'>%s</a>\n" %
(newpath, quote_plus(exclude), name)
)
for chunk in _show_branch(
root[name], base, newpath, pct, showpct,
exclude, coverage=coverage
):
yield chunk
# Now list the files
if path.lower().startswith(base):
relpath = path[len(base):]
@ -207,7 +224,7 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
files.sort()
for name in files:
newpath = os.path.join(path, name)
pc_str = ""
if showpct:
try:
@ -217,22 +234,24 @@ def _show_branch(root, base, path, pct=0, showpct=False, exclude="",
pass
else:
pc = _percent(statements, missing)
pc_str = ("%3d%% " % pc).replace(' ','&nbsp;')
pc_str = ("%3d%% " % pc).replace(' ', '&nbsp;')
if pc < float(pct) or pc == -1:
pc_str = "<span class='fail'>%s</span>" % pc_str
else:
pc_str = "<span class='pass'>%s</span>" % pc_str
yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1),
pc_str, newpath, name)
def _skip_file(path, exclude):
if exclude:
return bool(re.search(exclude, path))
def _graft(path, tree):
d = tree
p = path
atoms = []
while True:
@ -243,12 +262,13 @@ def _graft(path, tree):
atoms.append(p)
if p != "/":
atoms.append("/")
atoms.reverse()
for node in atoms:
if node:
d = d.setdefault(node, {})
def get_tree(base, exclude, coverage=the_coverage):
"""Return covered module names as a nested dict."""
tree = {}
@ -258,8 +278,9 @@ def get_tree(base, exclude, coverage=the_coverage):
_graft(path, tree)
return tree
class CoverStats(object):
def __init__(self, coverage, root=None):
self.coverage = coverage
if root is None:
@ -268,20 +289,20 @@ class CoverStats(object):
import cherrypy
root = os.path.dirname(cherrypy.__file__)
self.root = root
def index(self):
return TEMPLATE_FRAMESET % self.root.lower()
index.exposed = True
def menu(self, base="/", pct="50", showpct="",
exclude=r'python\d\.\d|test|tut\d|tutorial'):
# The coverage module uses all-lower-case names.
base = base.lower().rstrip(os.sep)
yield TEMPLATE_MENU
yield TEMPLATE_FORM % locals()
# Start by showing links for parent paths
yield "<div id='crumbs'>"
path = ""
@ -292,22 +313,23 @@ class CoverStats(object):
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
% (path, quote_plus(exclude), atom, os.sep))
yield "</div>"
yield "<div id='tree'>"
# Then display the tree
tree = get_tree(base, exclude, self.coverage)
if not tree:
yield "<p>No modules covered.</p>"
else:
for chunk in _show_branch(tree, base, "/", pct,
showpct=='checked', exclude, coverage=self.coverage):
showpct == 'checked', exclude,
coverage=self.coverage):
yield chunk
yield "</div>"
yield "</body></html>"
menu.exposed = True
def annotated_file(self, filename, statements, excluded, missing):
source = open(filename, 'r')
buffer = []
@ -329,9 +351,10 @@ class CoverStats(object):
yield template % (lno, cgi.escape(pastline))
buffer = []
yield template % (lineno, cgi.escape(line))
def report(self, name):
filename, statements, excluded, missing, _ = self.coverage.analysis2(name)
filename, statements, excluded, missing, _ = self.coverage.analysis2(
name)
pc = _percent(statements, missing)
yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
fullpath=name,
@ -350,9 +373,9 @@ def serve(path=localFile, port=8080, root=None):
if coverage is None:
raise ImportError("The coverage module could not be imported.")
from coverage import coverage
cov = coverage(data_file = path)
cov = coverage(data_file=path)
cov.load()
import cherrypy
cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10,
@ -362,4 +385,3 @@ def serve(path=localFile, port=8080, root=None):
if __name__ == "__main__":
serve(*tuple(sys.argv[1:]))

View File

@ -21,33 +21,37 @@ to collect stats to import a third-party module. Therefore, we choose to
re-use the `logging` module by adding a `statistics` object to it.
That `logging.statistics` object is a nested dict. It is not a custom class,
because that would 1) require libraries and applications to import a third-
party module in order to participate, 2) inhibit innovation in extrapolation
approaches and in reporting tools, and 3) be slow. There are, however, some
specifications regarding the structure of the dict.
because that would:
{
+----"SQLAlchemy": {
| "Inserts": 4389745,
| "Inserts per Second":
| lambda s: s["Inserts"] / (time() - s["Start"]),
| C +---"Table Statistics": {
| o | "widgets": {-----------+
N | l | "Rows": 1.3M, | Record
a | l | "Inserts": 400, |
m | e | },---------------------+
e | c | "froobles": {
s | t | "Rows": 7845,
p | i | "Inserts": 0,
a | o | },
c | n +---},
e | "Slow Queries":
| [{"Query": "SELECT * FROM widgets;",
| "Processing Time": 47.840923343,
| },
| ],
+----},
}
1. require libraries and applications to import a third-party module in
order to participate
2. inhibit innovation in extrapolation approaches and in reporting tools, and
3. be slow.
There are, however, some specifications regarding the structure of the dict.::
{
+----"SQLAlchemy": {
| "Inserts": 4389745,
| "Inserts per Second":
| lambda s: s["Inserts"] / (time() - s["Start"]),
| C +---"Table Statistics": {
| o | "widgets": {-----------+
N | l | "Rows": 1.3M, | Record
a | l | "Inserts": 400, |
m | e | },---------------------+
e | c | "froobles": {
s | t | "Rows": 7845,
p | i | "Inserts": 0,
a | o | },
c | n +---},
e | "Slow Queries":
| [{"Query": "SELECT * FROM widgets;",
| "Processing Time": 47.840923343,
| },
| ],
+----},
}
The `logging.statistics` dict has four levels. The topmost level is nothing
more than a set of names to introduce modularity, usually along the lines of
@ -65,13 +69,13 @@ Each namespace, then, is a dict of named statistical values, such as
good on a report: spaces and capitalization are just fine.
In addition to scalars, values in a namespace MAY be a (third-layer)
dict, or a list, called a "collection". For example, the CherryPy StatsTool
keeps track of what each request is doing (or has most recently done)
in a 'Requests' collection, where each key is a thread ID; each
dict, or a list, called a "collection". For example, the CherryPy
:class:`StatsTool` keeps track of what each request is doing (or has most
recently done) in a 'Requests' collection, where each key is a thread ID; each
value in the subdict MUST be a fourth dict (whew!) of statistical data about
each thread. We call each subdict in the collection a "record". Similarly,
the StatsTool also keeps a list of slow queries, where each record contains
data about each slow query, in order.
the :class:`StatsTool` also keeps a list of slow queries, where each record
contains data about each slow query, in order.
Values in a namespace or record may also be functions, which brings us to:
@ -86,17 +90,17 @@ scalar values you already have on hand.
When it comes time to report on the gathered data, however, we usually have
much more freedom in what we can calculate. Therefore, whenever reporting
tools (like the provided StatsPage CherryPy class) fetch the contents of
`logging.statistics` for reporting, they first call `extrapolate_statistics`
(passing the whole `statistics` dict as the only argument). This makes a
deep copy of the statistics dict so that the reporting tool can both iterate
over it and even change it without harming the original. But it also expands
any functions in the dict by calling them. For example, you might have a
'Current Time' entry in the namespace with the value "lambda scope: time.time()".
The "scope" parameter is the current namespace dict (or record, if we're
currently expanding one of those instead), allowing you access to existing
static entries. If you're truly evil, you can even modify more than one entry
at a time.
tools (like the provided :class:`StatsPage` CherryPy class) fetch the contents
of `logging.statistics` for reporting, they first call
`extrapolate_statistics` (passing the whole `statistics` dict as the only
argument). This makes a deep copy of the statistics dict so that the
reporting tool can both iterate over it and even change it without harming
the original. But it also expands any functions in the dict by calling them.
For example, you might have a 'Current Time' entry in the namespace with the
value "lambda scope: time.time()". The "scope" parameter is the current
namespace dict (or record, if we're currently expanding one of those
instead), allowing you access to existing static entries. If you're truly
evil, you can even modify more than one entry at a time.
However, don't try to calculate an entry and then use its value in further
extrapolations; the order in which the functions are called is not guaranteed.
@ -108,19 +112,20 @@ After the whole thing has been extrapolated, it's time for:
Reporting
---------
The StatsPage class grabs the `logging.statistics` dict, extrapolates it all,
and then transforms it to HTML for easy viewing. Each namespace gets its own
header and attribute table, plus an extra table for each collection. This is
NOT part of the statistics specification; other tools can format how they like.
The :class:`StatsPage` class grabs the `logging.statistics` dict, extrapolates
it all, and then transforms it to HTML for easy viewing. Each namespace gets
its own header and attribute table, plus an extra table for each collection.
This is NOT part of the statistics specification; other tools can format how
they like.
You can control which columns are output and how they are formatted by updating
StatsPage.formatting, which is a dict that mirrors the keys and nesting of
`logging.statistics`. The difference is that, instead of data values, it has
formatting values. Use None for a given key to indicate to the StatsPage that a
given column should not be output. Use a string with formatting (such as '%.3f')
to interpolate the value(s), or use a callable (such as lambda v: v.isoformat())
for more advanced formatting. Any entry which is not mentioned in the formatting
dict is output unchanged.
given column should not be output. Use a string with formatting
(such as '%.3f') to interpolate the value(s), or use a callable (such as
lambda v: v.isoformat()) for more advanced formatting. Any entry which is not
mentioned in the formatting dict is output unchanged.
Monitoring
----------
@ -145,12 +150,12 @@ entries to False or True, if present.
Usage
=====
To collect statistics on CherryPy applications:
To collect statistics on CherryPy applications::
from cherrypy.lib import cpstats
appconfig['/']['tools.cpstats.on'] = True
To collect statistics on your own code:
To collect statistics on your own code::
import logging
# Initialize the repository
@ -172,20 +177,22 @@ To collect statistics on your own code:
if mystats.get('Enabled', False):
mystats['Important Events'] += 1
To report statistics:
To report statistics::
root.cpstats = cpstats.StatsPage()
To format statistics reports:
To format statistics reports::
See 'Reporting', above.
"""
# -------------------------------- Statistics -------------------------------- #
# ------------------------------- Statistics -------------------------------- #
import logging
if not hasattr(logging, 'statistics'): logging.statistics = {}
if not hasattr(logging, 'statistics'):
logging.statistics = {}
def extrapolate_statistics(scope):
"""Return an extrapolated copy of the given scope."""
@ -201,7 +208,7 @@ def extrapolate_statistics(scope):
return c
# --------------------- CherryPy Applications Statistics --------------------- #
# -------------------- CherryPy Applications Statistics --------------------- #
import threading
import time
@ -211,12 +218,20 @@ import cherrypy
appstats = logging.statistics.setdefault('CherryPy Applications', {})
appstats.update({
'Enabled': True,
'Bytes Read/Request': lambda s: (s['Total Requests'] and
(s['Total Bytes Read'] / float(s['Total Requests'])) or 0.0),
'Bytes Read/Request': lambda s: (
s['Total Requests'] and
(s['Total Bytes Read'] / float(s['Total Requests'])) or
0.0
),
'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s),
'Bytes Written/Request': lambda s: (s['Total Requests'] and
(s['Total Bytes Written'] / float(s['Total Requests'])) or 0.0),
'Bytes Written/Second': lambda s: s['Total Bytes Written'] / s['Uptime'](s),
'Bytes Written/Request': lambda s: (
s['Total Requests'] and
(s['Total Bytes Written'] / float(s['Total Requests'])) or
0.0
),
'Bytes Written/Second': lambda s: (
s['Total Bytes Written'] / s['Uptime'](s)
),
'Current Time': lambda s: time.time(),
'Current Requests': 0,
'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s),
@ -228,28 +243,29 @@ appstats.update({
'Total Time': 0,
'Uptime': lambda s: time.time() - s['Start Time'],
'Requests': {},
})
})
proc_time = lambda s: time.time() - s['Start Time']
class ByteCountWrapper(object):
"""Wraps a file-like object, counting the number of bytes read."""
def __init__(self, rfile):
self.rfile = rfile
self.bytes_read = 0
def read(self, size=-1):
data = self.rfile.read(size)
self.bytes_read += len(data)
return data
def readline(self, size=-1):
data = self.rfile.readline(size)
self.bytes_read += len(data)
return data
def readlines(self, sizehint=0):
# Shamelessly stolen from StringIO
total = 0
@ -262,13 +278,13 @@ class ByteCountWrapper(object):
break
line = self.readline()
return lines
def close(self):
self.rfile.close()
def __iter__(self):
return self
def next(self):
data = self.rfile.next()
self.bytes_read += len(data)
@ -279,30 +295,31 @@ average_uriset_time = lambda s: s['Count'] and (s['Sum'] / s['Count']) or 0
class StatsTool(cherrypy.Tool):
"""Record various information about the current request."""
def __init__(self):
cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
def _setup(self):
"""Hook this tool into cherrypy.request.
The standard CherryPy request object will automatically call this
method when the tool is "turned on" in config.
"""
if appstats.get('Enabled', False):
cherrypy.Tool._setup(self)
self.record_start()
def record_start(self):
"""Record the beginning of a request."""
request = cherrypy.serving.request
if not hasattr(request.rfile, 'bytes_read'):
request.rfile = ByteCountWrapper(request.rfile)
request.body.fp = request.rfile
r = request.remote
appstats['Current Requests'] += 1
appstats['Total Requests'] += 1
appstats['Requests'][threading._get_ident()] = {
@ -315,37 +332,39 @@ class StatsTool(cherrypy.Tool):
'Request-Line': request.request_line,
'Response Status': None,
'Start Time': time.time(),
}
}
def record_stop(self, uriset=None, slow_queries=1.0, slow_queries_count=100,
debug=False, **kwargs):
def record_stop(
self, uriset=None, slow_queries=1.0, slow_queries_count=100,
debug=False, **kwargs):
"""Record the end of a request."""
resp = cherrypy.serving.response
w = appstats['Requests'][threading._get_ident()]
r = cherrypy.request.rfile.bytes_read
w['Bytes Read'] = r
appstats['Total Bytes Read'] += r
if resp.stream:
w['Bytes Written'] = 'chunked'
else:
cl = int(resp.headers.get('Content-Length', 0))
w['Bytes Written'] = cl
appstats['Total Bytes Written'] += cl
w['Response Status'] = getattr(resp, 'output_status', None) or resp.status
w['Response Status'] = getattr(
resp, 'output_status', None) or resp.status
w['End Time'] = time.time()
p = w['End Time'] - w['Start Time']
w['Processing Time'] = p
appstats['Total Time'] += p
appstats['Current Requests'] -= 1
if debug:
cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS')
if uriset:
rs = appstats.setdefault('URI Set Tracking', {})
r = rs.setdefault(uriset, {
@ -357,7 +376,7 @@ class StatsTool(cherrypy.Tool):
r['Max'] = p
r['Count'] += 1
r['Sum'] += p
if slow_queries and p > slow_queries:
sq = appstats.setdefault('Slow Queries', [])
sq.append(w.copy())
@ -388,6 +407,7 @@ 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 pause_resume(ns):
def _pause_resume(enabled):
pause_disabled = ''
@ -410,7 +430,7 @@ def pause_resume(ns):
class StatsPage(object):
formatting = {
'CherryPy Applications': {
'Enabled': pause_resume('CherryPy Applications'),
@ -427,20 +447,20 @@ class StatsPage(object):
'End Time': None,
'Processing Time': '%.3f',
'Start Time': iso_format,
},
},
'URI Set Tracking': {
'Avg': '%.3f',
'Max': '%.3f',
'Min': '%.3f',
'Sum': '%.3f',
},
},
'Requests': {
'Bytes Read': '%s',
'Bytes Written': '%s',
'End Time': None,
'Processing Time': '%.3f',
'Start Time': None,
},
},
},
'CherryPy WSGIServer': {
'Enabled': pause_resume('CherryPy WSGIServer'),
@ -448,8 +468,7 @@ class StatsPage(object):
'Start time': iso_format,
},
}
def index(self):
# Transform the raw data into pretty output for HTML
yield """
@ -500,18 +519,25 @@ table.stats2 th {
""" % title
for i, (key, value) in enumerate(scalars):
colnum = i % 3
if colnum == 0: yield """
if colnum == 0:
yield """
<tr>"""
yield (
"""
<th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" %
vars()
)
if colnum == 2:
yield """
</tr>"""
if colnum == 0:
yield """
<th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" % vars()
if colnum == 2: yield """
</tr>"""
if colnum == 0: yield """
<th></th><td></td>
<th></th><td></td>
</tr>"""
elif colnum == 1: yield """
elif colnum == 1:
yield """
<th></th><td></td>
</tr>"""
yield """
@ -547,7 +573,7 @@ table.stats2 th {
</html>
"""
index.exposed = True
def get_namespaces(self):
"""Yield (title, scalars, collections) for each namespace."""
s = extrapolate_statistics(logging.statistics)
@ -574,7 +600,7 @@ table.stats2 th {
v = format % v
scalars.append((k, v))
yield title, scalars, collections
def get_dict_collection(self, v, formatting):
"""Return ([headers], [rows]) for the given collection."""
# E.g., the 'Requests' dict.
@ -588,7 +614,7 @@ table.stats2 th {
if k3 not in headers:
headers.append(k3)
headers.sort()
subrows = []
for k2, record in sorted(v.items()):
subrow = [k2]
@ -604,9 +630,9 @@ table.stats2 th {
v3 = format % v3
subrow.append(v3)
subrows.append(subrow)
return headers, subrows
def get_list_collection(self, v, formatting):
"""Return ([headers], [subrows]) for the given collection."""
# E.g., the 'Slow Queries' list.
@ -620,7 +646,7 @@ table.stats2 th {
if k3 not in headers:
headers.append(k3)
headers.sort()
subrows = []
for record in v:
subrow = []
@ -636,27 +662,26 @@ table.stats2 th {
v3 = format % v3
subrow.append(v3)
subrows.append(subrow)
return headers, subrows
if json is not None:
def data(self):
s = extrapolate_statistics(logging.statistics)
cherrypy.response.headers['Content-Type'] = 'application/json'
return json.dumps(s, sort_keys=True, indent=4)
data.exposed = True
def pause(self, namespace):
logging.statistics.get(namespace, {})['Enabled'] = False
raise cherrypy.HTTPRedirect('./')
pause.exposed = True
pause.cp_config = {'tools.allow.on': True,
'tools.allow.methods': ['POST']}
def resume(self, namespace):
logging.statistics.get(namespace, {})['Enabled'] = True
raise cherrypy.HTTPRedirect('./')
resume.exposed = True
resume.cp_config = {'tools.allow.on': True,
'tools.allow.methods': ['POST']}

View File

@ -4,20 +4,21 @@ import logging
import re
import cherrypy
from cherrypy._cpcompat import basestring, ntob, md5, set
from cherrypy._cpcompat import basestring, md5, set, unicodestr
from cherrypy.lib import httputil as _httputil
from cherrypy.lib import is_iterator
# Conditional HTTP request support #
def validate_etags(autotags=False, debug=False):
"""Validate the current ETag against If-Match, If-None-Match headers.
If autotags is True, an ETag response-header value will be provided
from an MD5 hash of the response body (unless some other code has
already provided an ETag header). If False (the default), the ETag
will not be automatic.
WARNING: the autotags feature is not designed for URL's which allow
methods other than GET. For example, if a POST to the same URL returns
no content, the automatic ETag will be incorrect, breaking a fundamental
@ -27,15 +28,15 @@ def validate_etags(autotags=False, debug=False):
See :rfc:`2616` Section 14.24.
"""
response = cherrypy.serving.response
# Guard against being run twice.
if hasattr(response, "ETag"):
return
status, reason, msg = _httputil.valid_status(response.status)
etag = response.headers.get('ETag')
# Automatic ETag generation. See warning in docstring.
if etag:
if debug:
@ -52,9 +53,9 @@ def validate_etags(autotags=False, debug=False):
if debug:
cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS')
response.headers['ETag'] = etag
response.ETag = etag
# "If the request would, without the If-Match header field, result in
# anything other than a 2xx or 412 status, then the If-Match header
# MUST be ignored."
@ -62,7 +63,7 @@ def validate_etags(autotags=False, debug=False):
cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS')
if status >= 200 and status <= 299:
request = cherrypy.serving.request
conditions = request.headers.elements('If-Match') or []
conditions = [str(x) for x in conditions]
if debug:
@ -71,7 +72,7 @@ def validate_etags(autotags=False, debug=False):
if conditions and not (conditions == ["*"] or etag in conditions):
raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did "
"not match %r" % (etag, conditions))
conditions = request.headers.elements('If-None-Match') or []
conditions = [str(x) for x in conditions]
if debug:
@ -79,16 +80,18 @@ def validate_etags(autotags=False, debug=False):
'TOOLS.ETAGS')
if conditions == ["*"] or etag in conditions:
if debug:
cherrypy.log('request.method: %s' % request.method, 'TOOLS.ETAGS')
cherrypy.log('request.method: %s' %
request.method, 'TOOLS.ETAGS')
if request.method in ("GET", "HEAD"):
raise cherrypy.HTTPRedirect([], 304)
else:
raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r "
"matched %r" % (etag, conditions))
def validate_since():
"""Validate the current Last-Modified against If-Modified-Since headers.
If no code has set the Last-Modified response header, then no validation
will be performed.
"""
@ -96,14 +99,14 @@ def validate_since():
lastmod = response.headers.get('Last-Modified')
if lastmod:
status, reason, msg = _httputil.valid_status(response.status)
request = cherrypy.serving.request
since = request.headers.get('If-Unmodified-Since')
if since and since != lastmod:
if (status >= 200 and status <= 299) or status == 412:
raise cherrypy.HTTPError(412)
since = request.headers.get('If-Modified-Since')
if since and since == lastmod:
if (status >= 200 and status <= 299) or status == 304:
@ -117,11 +120,11 @@ def validate_since():
def allow(methods=None, debug=False):
"""Raise 405 if request.method not in methods (default ['GET', 'HEAD']).
The given methods are case-insensitive, and may be in any order.
If only one method is allowed, you may supply a single string;
if more than one, supply a list of strings.
Regardless of whether the current method is allowed or not, this
also emits an 'Allow' response header, containing the given methods.
"""
@ -132,7 +135,7 @@ def allow(methods=None, debug=False):
methods = ['GET', 'HEAD']
elif 'GET' in methods and 'HEAD' not in methods:
methods.append('HEAD')
cherrypy.response.headers['Allow'] = ', '.join(methods)
if cherrypy.request.method not in methods:
if debug:
@ -148,27 +151,27 @@ def allow(methods=None, debug=False):
def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
scheme='X-Forwarded-Proto', debug=False):
"""Change the base URL (scheme://host[:port][/path]).
For running a CP server behind Apache, lighttpd, or other HTTP server.
For Apache and lighttpd, you should leave the 'local' argument at the
default value of 'X-Forwarded-Host'. For Squid, you probably want to set
tools.proxy.local = 'Origin'.
If you want the new request.base to include path info (not just the host),
you must explicitly set base to the full base path, and ALSO set 'local'
to '', so that the X-Forwarded-Host request header (which never includes
path info) does not override it. Regardless, the value for 'base' MUST
NOT end in a slash.
cherrypy.request.remote.ip (the IP address of the client) will be
rewritten if the header specified by the 'remote' arg is valid.
By default, 'remote' is set to 'X-Forwarded-For'. If you do not
want to rewrite remote.ip, set the 'remote' arg to an empty string.
"""
request = cherrypy.serving.request
if scheme:
s = request.headers.get(scheme, None)
if debug:
@ -181,7 +184,7 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
scheme = s
if not scheme:
scheme = request.base[:request.base.find("://")]
if local:
lbase = request.headers.get(local, None)
if debug:
@ -194,27 +197,27 @@ def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
base = '127.0.0.1'
else:
base = '127.0.0.1:%s' % port
if base.find("://") == -1:
# add http:// or https:// if needed
base = scheme + "://" + base
request.base = base
if remote:
xff = request.headers.get(remote)
if debug:
cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
if xff:
if remote == 'X-Forwarded-For':
# See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/
xff = xff.split(',')[-1].strip()
#Bug #1268
xff = xff.split(',')[0].strip()
request.remote.ip = xff
def ignore_headers(headers=('Range',), debug=False):
"""Delete request headers whose field names are included in 'headers'.
This is a useful tool for working behind certain HTTP servers;
for example, Apache duplicates the work that CP does for 'Range'
headers, and will doubly-truncate the response.
@ -241,10 +244,10 @@ response_headers.failsafe = True
def referer(pattern, accept=True, accept_missing=False, error=403,
message='Forbidden Referer header.', debug=False):
"""Raise HTTPError if Referer header does/does not match the given pattern.
pattern
A regular expression pattern to test against the Referer.
accept
If True, the Referer must match the pattern; if False,
the Referer must NOT match the pattern.
@ -254,10 +257,10 @@ def referer(pattern, accept=True, accept_missing=False, error=403,
error
The HTTP error code to return to the client on failure.
message
A string to include in the response body on failure.
"""
try:
ref = cherrypy.serving.request.headers['Referer']
@ -272,44 +275,48 @@ def referer(pattern, accept=True, accept_missing=False, error=403,
cherrypy.log('No Referer header', 'TOOLS.REFERER')
if accept_missing:
return
raise cherrypy.HTTPError(error, message)
class SessionAuth(object):
"""Assert that the user is logged in."""
session_key = "username"
debug = False
def check_username_and_password(self, username, password):
pass
def anonymous(self):
"""Provide a temporary user name for anonymous users."""
pass
def on_login(self, username):
pass
def on_logout(self, username):
pass
def on_check(self, username):
pass
def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
return ntob("""<html><body>
def login_screen(self, from_page='..', username='', error_msg='',
**kwargs):
return (unicodestr("""<html><body>
Message: %(error_msg)s
<form method="post" action="do_login">
Login: <input type="text" name="username" value="%(username)s" size="10" /><br />
Password: <input type="password" name="password" size="10" /><br />
<input type="hidden" name="from_page" value="%(from_page)s" /><br />
Login: <input type="text" name="username" value="%(username)s" size="10" />
<br />
Password: <input type="password" name="password" size="10" />
<br />
<input type="hidden" name="from_page" value="%(from_page)s" />
<br />
<input type="submit" />
</form>
</body></html>""" % {'from_page': from_page, 'username': username,
'error_msg': error_msg}, "utf-8")
</body></html>""") % vars()).encode("utf-8")
def do_login(self, username, password, from_page='..', **kwargs):
"""Login. May raise redirect, or return True if request handled."""
response = cherrypy.serving.response
@ -326,7 +333,7 @@ Message: %(error_msg)s
cherrypy.session[self.session_key] = username
self.on_login(username)
raise cherrypy.HTTPRedirect(from_page or "/")
def do_logout(self, from_page='..', **kwargs):
"""Logout. May raise redirect, or return True if request handled."""
sess = cherrypy.session
@ -336,61 +343,62 @@ Message: %(error_msg)s
cherrypy.serving.request.login = None
self.on_logout(username)
raise cherrypy.HTTPRedirect(from_page)
def do_check(self):
"""Assert username. May raise redirect, or return True if request handled."""
"""Assert username. Raise redirect, or return True if request handled.
"""
sess = cherrypy.session
request = cherrypy.serving.request
response = cherrypy.serving.response
username = sess.get(self.session_key)
if not username:
sess[self.session_key] = username = self.anonymous()
if self.debug:
cherrypy.log('No session[username], trying anonymous', 'TOOLS.SESSAUTH')
self._debug_message('No session[username], trying anonymous')
if not username:
url = cherrypy.url(qs=request.query_string)
if self.debug:
cherrypy.log('No username, routing to login_screen with '
'from_page %r' % url, 'TOOLS.SESSAUTH')
self._debug_message(
'No username, routing to login_screen with from_page %(url)r',
locals(),
)
response.body = self.login_screen(url)
if "Content-Length" in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
return True
if self.debug:
cherrypy.log('Setting request.login to %r' % username, 'TOOLS.SESSAUTH')
self._debug_message('Setting request.login to %(username)r', locals())
request.login = username
self.on_check(username)
def _debug_message(self, template, context={}):
if not self.debug:
return
cherrypy.log(template % context, 'TOOLS.SESSAUTH')
def run(self):
request = cherrypy.serving.request
response = cherrypy.serving.response
path = request.path_info
if path.endswith('login_screen'):
if self.debug:
cherrypy.log('routing %r to login_screen' % path, 'TOOLS.SESSAUTH')
return self.login_screen(**request.params)
self._debug_message('routing %(path)r to login_screen', locals())
response.body = self.login_screen()
return True
elif path.endswith('do_login'):
if request.method != 'POST':
response.headers['Allow'] = "POST"
if self.debug:
cherrypy.log('do_login requires POST', 'TOOLS.SESSAUTH')
self._debug_message('do_login requires POST')
raise cherrypy.HTTPError(405)
if self.debug:
cherrypy.log('routing %r to do_login' % path, 'TOOLS.SESSAUTH')
self._debug_message('routing %(path)r to do_login', locals())
return self.do_login(**request.params)
elif path.endswith('do_logout'):
if request.method != 'POST':
response.headers['Allow'] = "POST"
raise cherrypy.HTTPError(405)
if self.debug:
cherrypy.log('routing %r to do_logout' % path, 'TOOLS.SESSAUTH')
self._debug_message('routing %(path)r to do_logout', locals())
return self.do_logout(**request.params)
else:
if self.debug:
cherrypy.log('No special path, running do_check', 'TOOLS.SESSAUTH')
self._debug_message('No special path, running do_check')
return self.do_check()
@ -412,15 +420,17 @@ def log_traceback(severity=logging.ERROR, debug=False):
"""Write the last error's traceback to the cherrypy error log."""
cherrypy.log("", "HTTP", severity=severity, traceback=True)
def log_request_headers(debug=False):
"""Write request headers to the cherrypy error log."""
h = [" %s: %s" % (k, v) for k, v in cherrypy.serving.request.header_list]
cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP")
def log_hooks(debug=False):
"""Write request.hooks to the cherrypy error log."""
request = cherrypy.serving.request
msg = []
# Sort by the standard points if possible.
from cherrypy import _cprequest
@ -428,7 +438,7 @@ def log_hooks(debug=False):
for k in request.hooks.keys():
if k not in points:
points.append(k)
for k in points:
msg.append(" %s:" % k)
v = request.hooks.get(k, [])
@ -438,6 +448,7 @@ def log_hooks(debug=False):
cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
':\n' + '\n'.join(msg), "HTTP")
def redirect(url='', internal=True, debug=False):
"""Raise InternalRedirect or HTTPRedirect to the given url."""
if debug:
@ -449,11 +460,12 @@ def redirect(url='', internal=True, debug=False):
else:
raise cherrypy.HTTPRedirect(url)
def trailing_slash(missing=True, extra=False, status=None, debug=False):
"""Redirect if path_info has (missing|extra) trailing slash."""
request = cherrypy.serving.request
pi = request.path_info
if debug:
cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
(request.is_index, missing, extra, pi),
@ -470,17 +482,17 @@ def trailing_slash(missing=True, extra=False, status=None, debug=False):
new_url = cherrypy.url(pi[:-1], request.query_string)
raise cherrypy.HTTPRedirect(new_url, status=status or 301)
def flatten(debug=False):
"""Wrap response.body in a generator that recursively iterates over body.
This allows cherrypy.response.body to consist of 'nested generators';
that is, a set of generators that yield generators.
"""
import types
def flattener(input):
numchunks = 0
for x in input:
if not isinstance(x, types.GeneratorType):
if not is_iterator(x):
numchunks += 1
yield x
else:
@ -495,9 +507,9 @@ def flatten(debug=False):
def accept(media=None, debug=False):
"""Return the client's preferred media-type (from the given Content-Types).
If 'media' is None (the default), no test will be performed.
If 'media' is provided, it should be the Content-Type value (as a string)
or values (as a list or tuple of strings) which the current resource
can emit. The client's acceptable media ranges (as declared in the
@ -505,16 +517,16 @@ def accept(media=None, debug=False):
values; the first such string is returned. That is, the return value
will always be one of the strings provided in the 'media' arg (or None
if 'media' is None).
If no match is found, then HTTPError 406 (Not Acceptable) is raised.
Note that most web browsers send */* as a (low-quality) acceptable
media range, which should match any Content-Type. In addition, "...if
no Accept header field is present, then it is assumed that the client
accepts all media types."
Matching types are checked in order of client preference first,
and then in the order of the given 'media' values.
Note that this function does not honor accept-params (other than "q").
"""
if not media:
@ -522,7 +534,7 @@ def accept(media=None, debug=False):
if isinstance(media, basestring):
media = [media]
request = cherrypy.serving.request
# Parse the Accept request header, and try to match one
# of the requested media-ranges (in order of preference).
ranges = request.headers.elements('Accept')
@ -556,7 +568,7 @@ def accept(media=None, debug=False):
cherrypy.log('Match due to %s' % element.value,
'TOOLS.ACCEPT')
return element.value
# No suitable media-range found.
ah = request.headers.get('Accept')
if ah is None:
@ -569,22 +581,22 @@ def accept(media=None, debug=False):
class MonitoredHeaderMap(_httputil.HeaderMap):
def __init__(self):
self.accessed_headers = set()
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):
@ -593,25 +605,26 @@ class MonitoredHeaderMap(_httputil.HeaderMap):
def autovary(ignore=None, debug=False):
"""Auto-populate the Vary response header based on request.header access."""
"""Auto-populate the Vary response header based on request.header access.
"""
request = cherrypy.serving.request
req_h = request.headers
request.headers = MonitoredHeaderMap()
request.headers.update(req_h)
if ignore is None:
ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type'])
def set_response_header():
resp_h = cherrypy.serving.response.headers
v = set([e.value for e in resp_h.elements('Vary')])
if debug:
cherrypy.log('Accessed headers: %s' % request.headers.accessed_headers,
'TOOLS.AUTOVARY')
cherrypy.log(
'Accessed headers: %s' % request.headers.accessed_headers,
'TOOLS.AUTOVARY')
v = v.union(request.headers.accessed_headers)
v = v.difference(ignore)
v = list(v)
v.sort()
resp_h['Vary'] = ', '.join(v)
request.hooks.attach('before_finalize', set_response_header, 95)

View File

@ -4,24 +4,25 @@ import time
import cherrypy
from cherrypy._cpcompat import basestring, BytesIO, ntob, set, unicodestr
from cherrypy.lib import file_generator
from cherrypy.lib import is_closable_iterator
from cherrypy.lib import set_vary_header
def decode(encoding=None, default_encoding='utf-8'):
"""Replace or extend the list of charsets used to decode a request entity.
Either argument may be a single string or a list of strings.
encoding
If not None, restricts the set of charsets attempted while decoding
a request entity to the given set (even if a different charset is given in
the Content-Type request header).
a request entity to the given set (even if a different charset is
given in the Content-Type request header).
default_encoding
Only in effect if the 'encoding' argument is not given.
If given, the set of charsets attempted while decoding a request entity is
*extended* with the given value(s).
If given, the set of charsets attempted while decoding a request
entity is *extended* with the given value(s).
"""
body = cherrypy.request.body
if encoding is not None:
@ -33,9 +34,34 @@ def decode(encoding=None, default_encoding='utf-8'):
default_encoding = [default_encoding]
body.attempt_charsets = body.attempt_charsets + default_encoding
class UTF8StreamEncoder:
def __init__(self, iterator):
self._iterator = iterator
def __iter__(self):
return self
def next(self):
return self.__next__()
def __next__(self):
res = next(self._iterator)
if isinstance(res, unicodestr):
res = res.encode('utf-8')
return res
def close(self):
if is_closable_iterator(self._iterator):
self._iterator.close()
def __getattr__(self, attr):
if attr.startswith('__'):
raise AttributeError(self, attr)
return getattr(self._iterator, attr)
class ResponseEncoder:
default_encoding = 'utf-8'
failmsg = "Response body could not be encoded with %r."
encoding = None
@ -43,11 +69,11 @@ class ResponseEncoder:
text_only = True
add_charset = True
debug = False
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
self.attempted_charsets = set()
request = cherrypy.serving.request
if request.handler is not None:
@ -56,17 +82,17 @@ class ResponseEncoder:
cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE')
self.oldhandler = request.handler
request.handler = self
def encode_stream(self, encoding):
"""Encode a streaming response body.
Use a generator wrapper, and just pray it works as the stream is
being written out.
"""
if encoding in self.attempted_charsets:
return False
self.attempted_charsets.add(encoding)
def encoder(body):
for chunk in body:
if isinstance(chunk, unicodestr):
@ -74,31 +100,30 @@ class ResponseEncoder:
yield chunk
self.body = encoder(self.body)
return True
def encode_string(self, encoding):
"""Encode a buffered response body."""
if encoding in self.attempted_charsets:
return False
self.attempted_charsets.add(encoding)
try:
body = []
for chunk in self.body:
if isinstance(chunk, unicodestr):
body = []
for chunk in self.body:
if isinstance(chunk, unicodestr):
try:
chunk = chunk.encode(encoding, self.errors)
body.append(chunk)
self.body = body
except (LookupError, UnicodeError):
return False
else:
return True
except (LookupError, UnicodeError):
return False
body.append(chunk)
self.body = body
return True
def find_acceptable_charset(self):
request = cherrypy.serving.request
response = cherrypy.serving.response
if self.debug:
cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE')
cherrypy.log('response.stream %r' %
response.stream, 'TOOLS.ENCODE')
if response.stream:
encoder = self.encode_stream
else:
@ -115,22 +140,24 @@ class ResponseEncoder:
# >>> len(t.encode("utf7"))
# 8
del response.headers["Content-Length"]
# Parse the Accept-Charset request header, and try to provide one
# of the requested charsets (in order of user preference).
encs = request.headers.elements('Accept-Charset')
charsets = [enc.value.lower() for enc in encs]
if self.debug:
cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE')
if self.encoding is not None:
# If specified, force this encoding to be used, or fail.
encoding = self.encoding.lower()
if self.debug:
cherrypy.log('Specified encoding %r' % encoding, 'TOOLS.ENCODE')
cherrypy.log('Specified encoding %r' %
encoding, 'TOOLS.ENCODE')
if (not charsets) or "*" in charsets or encoding in charsets:
if self.debug:
cherrypy.log('Attempting encoding %r' % encoding, 'TOOLS.ENCODE')
cherrypy.log('Attempting encoding %r' %
encoding, 'TOOLS.ENCODE')
if encoder(encoding):
return encoding
else:
@ -142,7 +169,8 @@ class ResponseEncoder:
if encoder(self.default_encoding):
return self.default_encoding
else:
raise cherrypy.HTTPError(500, self.failmsg % self.default_encoding)
raise cherrypy.HTTPError(500, self.failmsg %
self.default_encoding)
else:
for element in encs:
if element.qvalue > 0:
@ -160,7 +188,7 @@ class ResponseEncoder:
'0)' % element, 'TOOLS.ENCODE')
if encoder(encoding):
return encoding
if "*" not in charsets:
# If no "*" is present in an Accept-Charset field, then all
# character sets not explicitly mentioned get a quality
@ -173,20 +201,21 @@ class ResponseEncoder:
'TOOLS.ENCODE')
if encoder(iso):
return iso
# No suitable encoding found.
ac = request.headers.get('Accept-Charset')
if ac is None:
msg = "Your client did not send an Accept-Charset header."
else:
msg = "Your client sent this Accept-Charset header: %s." % ac
msg += " We tried these charsets: %s." % ", ".join(self.attempted_charsets)
_charsets = ", ".join(sorted(self.attempted_charsets))
msg += " We tried these charsets: %s." % (_charsets,)
raise cherrypy.HTTPError(406, msg)
def __call__(self, *args, **kwargs):
response = cherrypy.serving.response
self.body = self.oldhandler(*args, **kwargs)
if isinstance(self.body, basestring):
# strings get wrapped in a list because iterating over a single
# item list is much faster than iterating over every character
@ -200,46 +229,49 @@ class ResponseEncoder:
self.body = file_generator(self.body)
elif self.body is None:
self.body = []
ct = response.headers.elements("Content-Type")
if self.debug:
cherrypy.log('Content-Type: %r' % [str(h) for h in ct], 'TOOLS.ENCODE')
if ct:
cherrypy.log('Content-Type: %r' % [str(h)
for h in ct], 'TOOLS.ENCODE')
if ct and self.add_charset:
ct = ct[0]
if self.text_only:
if ct.value.lower().startswith("text/"):
if self.debug:
cherrypy.log('Content-Type %s starts with "text/"' % ct,
'TOOLS.ENCODE')
cherrypy.log(
'Content-Type %s starts with "text/"' % ct,
'TOOLS.ENCODE')
do_find = True
else:
if self.debug:
cherrypy.log('Not finding because Content-Type %s does '
'not start with "text/"' % ct,
cherrypy.log('Not finding because Content-Type %s '
'does not start with "text/"' % ct,
'TOOLS.ENCODE')
do_find = False
else:
if self.debug:
cherrypy.log('Finding because not text_only', 'TOOLS.ENCODE')
cherrypy.log('Finding because not text_only',
'TOOLS.ENCODE')
do_find = True
if do_find:
# Set "charset=..." param on response Content-Type header
ct.params['charset'] = self.find_acceptable_charset()
if self.add_charset:
if self.debug:
cherrypy.log('Setting Content-Type %s' % ct,
'TOOLS.ENCODE')
response.headers["Content-Type"] = str(ct)
if self.debug:
cherrypy.log('Setting Content-Type %s' % ct,
'TOOLS.ENCODE')
response.headers["Content-Type"] = str(ct)
return self.body
# GZIP
def compress(body, compress_level):
"""Compress 'body' at the given compress_level."""
import zlib
# See http://www.gzip.org/zlib/rfc-gzip.html
yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker
yield ntob('\x08') # CM: compression method
@ -248,7 +280,7 @@ def compress(body, compress_level):
yield struct.pack("<L", int(time.time()) & int('FFFFFFFF', 16))
yield ntob('\x02') # XFL: max compression, slowest algo
yield ntob('\xff') # OS: unknown
crc = zlib.crc32(ntob(""))
size = 0
zobj = zlib.compressobj(compress_level,
@ -259,15 +291,16 @@ def compress(body, compress_level):
crc = zlib.crc32(line, crc)
yield zobj.compress(line)
yield zobj.flush()
# CRC32: 4 bytes
yield struct.pack("<L", crc & int('FFFFFFFF', 16))
# ISIZE: 4 bytes
yield struct.pack("<L", size & int('FFFFFFFF', 16))
def decompress(body):
import gzip
zbuf = BytesIO()
zbuf.write(body)
zbuf.seek(0)
@ -277,9 +310,10 @@ def decompress(body):
return data
def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
debug=False):
"""Try to gzip the response body if Content-Type in mime_types.
cherrypy.response.headers['Content-Type'] must be set to one of the
values in the mime_types arg before calling this function.
@ -287,32 +321,32 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
* type/subtype
* type/*
* type/*+subtype
No compression is performed if any of the following hold:
* The client sends no Accept-Encoding request header
* No 'gzip' or 'x-gzip' is present in the Accept-Encoding header
* No 'gzip' or 'x-gzip' with a qvalue > 0 is present
* The 'identity' value is given with a qvalue > 0.
"""
request = cherrypy.serving.request
response = cherrypy.serving.response
set_vary_header(response, "Accept-Encoding")
if not response.body:
# Response body is empty (might be a 304 for instance)
if debug:
cherrypy.log('No response body', context='TOOLS.GZIP')
return
# If returning cached content (which should already have been gzipped),
# don't re-zip.
if getattr(request, "cached", False):
if debug:
cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
return
acceptable = request.headers.elements('Accept-Encoding')
if not acceptable:
# If no Accept-Encoding field is present in a request,
@ -325,7 +359,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
if debug:
cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP')
return
ct = response.headers.get('Content-Type', '').split(';')[0]
for coding in acceptable:
if coding.value == 'identity' and coding.qvalue != 0:
@ -339,7 +373,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
cherrypy.log('Zero gzip qvalue: %s' % coding,
context='TOOLS.GZIP')
return
if ct not in mime_types:
# If the list of provided mime-types contains tokens
# such as 'text/*' or 'application/*+xml',
@ -370,7 +404,7 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
cherrypy.log('Content-Type %s not in mime_types %r' %
(ct, mime_types), context='TOOLS.GZIP')
return
if debug:
cherrypy.log('Gzipping', context='TOOLS.GZIP')
# Return a generator that compresses the page
@ -379,10 +413,9 @@ def gzip(compress_level=5, mime_types=['text/html', 'text/plain'], debug=False):
if "Content-Length" in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
return
if debug:
cherrypy.log('No acceptable encoding found.', context='GZIP')
cherrypy.HTTPError(406, "identity, gzip").set_response()

View File

@ -3,7 +3,7 @@ import inspect
import os
import sys
import time
try:
import objgraph
except ImportError:
@ -15,6 +15,7 @@ from cherrypy.process.plugins import SimplePlugin
class ReferrerTree(object):
"""An object which gathers all referrers of an object to a given depth."""
peek_length = 40
@ -89,6 +90,7 @@ class ReferrerTree(object):
def format(self, tree):
"""Return a list of string reprs from a nested list of referrers."""
output = []
def ascend(branch, depth=1):
for parent, grandparents in branch:
output.append((" " * depth) + self._format(parent))
@ -103,15 +105,15 @@ def get_instances(cls):
class RequestCounter(SimplePlugin):
def start(self):
self.count = 0
def before_request(self):
self.count += 1
def after_request(self):
self.count -=1
self.count -= 1
request_counter = RequestCounter(cherrypy.engine)
request_counter.subscribe()
@ -129,15 +131,17 @@ def get_context(obj):
class GCRoot(object):
"""A CherryPy page handler for testing reference leaks."""
classes = [(_cprequest.Request, 2, 2,
"Should be 1 in this request thread and 1 in the main thread."),
(_cprequest.Response, 2, 2,
"Should be 1 in this request thread and 1 in the main thread."),
(_cpwsgi.AppResponse, 1, 1,
"Should be 1 in this request thread only."),
]
classes = [
(_cprequest.Request, 2, 2,
"Should be 1 in this request thread and 1 in the main thread."),
(_cprequest.Response, 2, 2,
"Should be 1 in this request thread and 1 in the main thread."),
(_cpwsgi.AppResponse, 1, 1,
"Should be 1 in this request thread only."),
]
def index(self):
return "Hello, world!"
@ -145,14 +149,14 @@ class GCRoot(object):
def stats(self):
output = ["Statistics:"]
for trial in range(10):
if request_counter.count > 0:
break
time.sleep(0.5)
else:
output.append("\nNot all requests closed properly.")
# gc_collect isn't perfectly synchronous, because it may
# break reference cycles that then take time to fully
# finalize. Call it thrice and hope for the best.
@ -208,7 +212,6 @@ class GCRoot(object):
t = ReferrerTree(ignore=[objs], maxdepth=3)
tree = t.ascend(obj)
output.extend(t.format(tree))
return "\n".join(output)
stats.exposed = True

View File

@ -4,4 +4,3 @@ warnings.warn('cherrypy.lib.http has been deprecated and will be removed '
DeprecationWarning)
from cherrypy.lib.httputil import *

View File

@ -1,5 +1,6 @@
"""
This module defines functions to implement HTTP Digest Authentication (:rfc:`2617`).
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.
@ -7,13 +8,14 @@ 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.
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'.
@ -21,7 +23,8 @@ 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:
Peter van Kampen for its recipe which implement most of Digest
authentication:
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378
"""
@ -29,27 +32,27 @@ __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:
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,
* 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
* 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.
* 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
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.
"""
@ -57,7 +60,7 @@ __all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse",
"parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey",
"calculateNonce", "SUPPORTED_QOP")
################################################################################
##########################################################################
import time
from cherrypy._cpcompat import base64_decode, ntob, md5
from cherrypy._cpcompat import parse_http_list, parse_keqv_list
@ -70,16 +73,17 @@ 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 (),
# SHA: lambda val: sha.new(ntob(val)).hexdigest (),
}
def calculateNonce (realm, algorithm = MD5):
def calculateNonce(realm, algorithm=MD5):
"""This is an auxaliary function that calculates 'nonce' value. It is used
to handle sessions."""
@ -89,44 +93,47 @@ def calculateNonce (realm, algorithm = MD5):
try:
encoder = DIGEST_AUTH_ENCODERS[algorithm]
except KeyError:
raise NotImplementedError ("The chosen algorithm (%s) does not have "\
"an implementation yet" % algorithm)
raise NotImplementedError("The chosen algorithm (%s) does not have "
"an implementation yet" % algorithm)
return encoder ("%d:%s" % (time.time(), realm))
return encoder("%d:%s" % (time.time(), realm))
def digestAuth (realm, algorithm = MD5, nonce = None, qop = AUTH):
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)
nonce = calculateNonce(realm, algorithm)
return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
realm, nonce, algorithm, qop
)
def basicAuth (realm):
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):
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)
return digestAuth(realm) + " " + basicAuth(realm)
################################################################################
##########################################################################
# Parse authorization parameters
#
def _parseDigestAuthorization (auth_params):
def _parseDigestAuthorization(auth_params):
# Convert the auth params to a dict
items = parse_http_list(auth_params)
params = parse_keqv_list(items)
@ -140,8 +147,8 @@ def _parseDigestAuthorization (auth_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):
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
@ -152,7 +159,7 @@ def _parseDigestAuthorization (auth_params):
return params
def _parseBasicAuthorization (auth_params):
def _parseBasicAuthorization(auth_params):
username, password = base64_decode(auth_params).split(":", 1)
return {"username": username, "password": password}
@ -161,18 +168,19 @@ AUTH_SCHEMES = {
"digest": _parseDigestAuthorization,
}
def parseAuthorization (credentials):
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 ()
auth_scheme, auth_params = credentials.split(" ", 1)
auth_scheme = auth_scheme.lower()
parser = AUTH_SCHEMES[auth_scheme]
params = parser (auth_params)
params = parser(auth_params)
if params is None:
return
@ -182,12 +190,12 @@ def parseAuthorization (credentials):
return params
################################################################################
##########################################################################
# Check provided response for a valid password
#
def md5SessionKey (params, password):
def md5SessionKey(params, password):
"""
If the "algorithm" directive's value is "MD5-sess", then A1
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.
@ -210,10 +218,11 @@ def md5SessionKey (params, password):
params_copy[key] = params[key]
params_copy["algorithm"] = MD5_SESS
return _A1 (params_copy, password)
return _A1(params_copy, password)
def _A1(params, password):
algorithm = params.get ("algorithm", MD5)
algorithm = params.get("algorithm", MD5)
H = DIGEST_AUTH_ENCODERS[algorithm]
if algorithm == MD5:
@ -227,7 +236,7 @@ def _A1(params, password):
# 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))
h_a1 = H("%s:%s:%s" % (params["username"], params["realm"], password))
return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"])
@ -235,13 +244,13 @@ 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")
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", "")
entity_body = kwargs.get("entity_body", "")
H = kwargs["H"]
return "%s:%s:%s" % (
@ -251,20 +260,22 @@ def _A2(params, method, kwargs):
)
else:
raise NotImplementedError ("The 'qop' method is unknown: %s" % qop)
raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs):
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)
algorithm = params.get("algorithm", MD5)
H = DIGEST_AUTH_ENCODERS[algorithm]
KD = lambda secret, data: H(secret + ":" + data)
qop = params.get ("qop", None)
qop = params.get("qop", None)
H_A2 = H(_A2(params, method, kwargs))
@ -297,7 +308,8 @@ def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwarg
return KD(H_A1, request)
def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs):
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:
@ -312,43 +324,48 @@ def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs
if auth_map['realm'] != kwargs.get('realm', None):
return False
response = _computeDigestResponse(auth_map, password, method, A1,**kwargs)
response = _computeDigestResponse(
auth_map, password, method, A1, **kwargs)
return response == auth_map["response"]
def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs):
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:
return encrypt(auth_map["password"], auth_map["username"]) == password
candidate = encrypt(auth_map["password"], auth_map["username"])
except TypeError:
return encrypt(auth_map["password"]) == password
# 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):
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
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
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)
return checker(auth_map, password, method=method, encrypt=encrypt,
**kwargs)

View File

@ -8,27 +8,27 @@ to a public caning.
"""
from binascii import b2a_base64
from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou, reversed, sorted
from cherrypy._cpcompat import basestring, bytestr, iteritems, nativestr, unicodestr, unquote_qs
from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou
from cherrypy._cpcompat import basestring, bytestr, iteritems, nativestr
from cherrypy._cpcompat import reversed, sorted, unicodestr, unquote_qs
response_codes = BaseHTTPRequestHandler.responses.copy()
# From http://www.cherrypy.org/ticket/361
# From https://bitbucket.org/cherrypy/cherrypy/issue/361
response_codes[500] = ('Internal Server Error',
'The server encountered an unexpected condition '
'which prevented it from fulfilling the request.')
'The server encountered an unexpected condition '
'which prevented it from fulfilling the request.')
response_codes[503] = ('Service Unavailable',
'The server is currently unable to handle the '
'request due to a temporary overloading or '
'maintenance of the server.')
'The server is currently unable to handle the '
'request due to a temporary overloading or '
'maintenance of the server.')
import re
import urllib
def urljoin(*atoms):
"""Return the given path \*atoms, joined into a single URL.
This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank.
"""
@ -38,9 +38,10 @@ def urljoin(*atoms):
# Special-case the final url of "", and return "/" instead.
return url or "/"
def urljoin_bytes(*atoms):
"""Return the given path *atoms, joined into a single URL.
This will correctly join a SCRIPT_NAME and PATH_INFO into the
original URL, even if either atom is blank.
"""
@ -50,24 +51,26 @@ def urljoin_bytes(*atoms):
# Special-case the final url of "", and return "/" instead.
return url or ntob("/")
def protocol_from_http(protocol_str):
"""Return a protocol tuple from the given 'HTTP/x.y' string."""
return int(protocol_str[5]), int(protocol_str[7])
def get_ranges(headervalue, content_length):
"""Return a list of (start, stop) indices from a Range header, or None.
Each (start, stop) tuple will be composed of two ints, which are suitable
for use in a slicing operation. That is, the header "Range: bytes=3-6",
if applied against a Python string, is requesting resource[3:7]. This
function will return the list [(3, 7)].
If this function returns an empty list, you should return HTTP 416.
"""
if not headervalue:
return None
result = []
bytesunit, byteranges = headervalue.split("=", 1)
for brange in byteranges.split(","):
@ -100,36 +103,44 @@ def get_ranges(headervalue, content_length):
# See rfc quote above.
return None
# Negative subscript (last N bytes)
result.append((content_length - int(stop), content_length))
#
# RFC 2616 Section 14.35.1:
# If the entity is shorter than the specified suffix-length,
# the entire entity-body is used.
if int(stop) > content_length:
result.append((0, content_length))
else:
result.append((content_length - int(stop), content_length))
return result
class HeaderElement(object):
"""An element (with parameters) from an HTTP header's element list."""
def __init__(self, value, params=None):
self.value = value
if params is None:
params = {}
self.params = params
def __cmp__(self, other):
return cmp(self.value, other.value)
def __lt__(self, other):
return self.value < other.value
def __str__(self):
p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)]
return "%s%s" % (self.value, "".join(p))
return str("%s%s" % (self.value, "".join(p)))
def __bytes__(self):
return ntob(self.__str__())
def __unicode__(self):
return ntou(self.__str__())
def parse(elementstr):
"""Transform 'token;key=val' to ('token', {'key': 'val'})."""
# Split the element into a value and parameters. The 'value' may
@ -150,7 +161,7 @@ class HeaderElement(object):
params[key] = val
return initial_value, params
parse = staticmethod(parse)
def from_str(cls, elementstr):
"""Construct an instance from a string of the form 'token;key=val'."""
ival, params = cls.parse(elementstr)
@ -160,16 +171,18 @@ class HeaderElement(object):
q_separator = re.compile(r'; *q *=')
class AcceptElement(HeaderElement):
"""An element (with parameters) from an Accept* header's element list.
AcceptElement objects are comparable; the more-preferred object will be
"less than" the less-preferred object. They are also therefore sortable;
if you sort a list of AcceptElement objects, they will be listed in
priority order; the most preferred value will be first. Yes, it should
have been the other way around, but it's too late to fix now.
"""
def from_str(cls, elementstr):
qvalue = None
# The first "q" parameter (if any) separates the initial
@ -180,48 +193,50 @@ class AcceptElement(HeaderElement):
# The qvalue for an Accept header can have extensions. The other
# headers cannot, but it's easier to parse them as if they did.
qvalue = HeaderElement.from_str(atoms[0].strip())
media_type, params = cls.parse(media_range)
if qvalue is not None:
params["q"] = qvalue
return cls(media_type, params)
from_str = classmethod(from_str)
def qvalue(self):
val = self.params.get("q", "1")
if isinstance(val, HeaderElement):
val = val.value
return float(val)
qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
def __cmp__(self, other):
diff = cmp(self.qvalue, other.qvalue)
if diff == 0:
diff = cmp(str(self), str(other))
return diff
def __lt__(self, other):
if self.qvalue == other.qvalue:
return str(self) < str(other)
else:
return self.qvalue < other.qvalue
RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)')
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.
"""
if not fieldvalue:
return []
result = []
for element in fieldvalue.split(","):
for element in RE_HEADER_SPLIT.split(fieldvalue):
if fieldname.startswith("Accept") or fieldname == 'TE':
hv = AcceptElement.from_str(element)
else:
hv = HeaderElement.from_str(element)
result.append(hv)
return list(reversed(sorted(result)))
def decode_TEXT(value):
r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr")."""
try:
@ -237,18 +252,19 @@ def decode_TEXT(value):
decodedvalue += atom
return decodedvalue
def valid_status(status):
"""Return legal HTTP status Code, Reason-phrase and Message.
The status arg must be an int, or a str that begins with an int.
If status is an int, or a str and no reason-phrase is supplied,
a default reason-phrase will be provided.
"""
if not status:
status = 200
status = str(status)
parts = status.split(" ", 1)
if len(parts) == 1:
@ -258,26 +274,26 @@ def valid_status(status):
else:
code, reason = parts
reason = reason.strip()
try:
code = int(code)
except ValueError:
raise ValueError("Illegal response status from server "
"(%s is non-numeric)." % repr(code))
if code < 100 or code > 599:
raise ValueError("Illegal response status from server "
"(%s is out of range)." % repr(code))
if code not in response_codes:
# code is unknown but not illegal
default_reason, message = "", ""
else:
default_reason, message = response_codes[code]
if reason is None:
reason = default_reason
return code, reason, message
@ -287,21 +303,21 @@ def valid_status(status):
def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
"""Parse a query given as a string argument.
Arguments:
qs: URL-encoded query string to be parsed
keep_blank_values: flag indicating whether blank values in
URL encoded queries should be treated as blank strings. A
true value indicates that blanks should be retained as blank
strings. The default false value indicates that blank values
are to be ignored and treated as if they were not included.
strict_parsing: flag indicating what to do with parsing errors. If
false (the default), errors are silently ignored. If true,
errors raise a ValueError exception.
Returns a dict, as G-d intended.
"""
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
@ -332,9 +348,10 @@ def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
"""Build a params dictionary from a query_string.
Duplicate key/value pairs in the provided query_string will be
returned as {'key': [val1, val2, ...]}. Single key/values will
be returned as strings: {'key': 'value'}.
@ -350,41 +367,42 @@ def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
class CaseInsensitiveDict(dict):
"""A case-insensitive dict subclass.
Each key is changed on entry to str(key).title().
"""
def __getitem__(self, key):
return dict.__getitem__(self, 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 dict.has_key(self, str(key).title())
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:
@ -392,7 +410,7 @@ class CaseInsensitiveDict(dict):
except KeyError:
self[key] = x
return x
def pop(self, key, default):
return dict.pop(self, str(key).title(), default)
@ -404,103 +422,115 @@ class CaseInsensitiveDict(dict):
# replaced with a single SP before interpretation of the TEXT value."
if nativestr == bytestr:
header_translate_table = ''.join([chr(i) for i in xrange(256)])
header_translate_deletechars = ''.join([chr(i) for i in xrange(32)]) + chr(127)
header_translate_deletechars = ''.join(
[chr(i) for i in xrange(32)]) + chr(127)
else:
header_translate_table = None
header_translate_deletechars = bytes(range(32)) + bytes([127])
class HeaderMap(CaseInsensitiveDict):
"""A dict subclass for HTTP request and response headers.
Each key is changed on entry to str(key).title(). This allows headers
to be case-insensitive and avoid duplicates.
Values are header values (decoded according to :rfc:`2047` if necessary).
"""
protocol=(1, 1)
protocol = (1, 1)
encodings = ["ISO-8859-1"]
# Someday, when http-bis is done, this will probably get dropped
# since few servers, clients, or intermediaries do it. But until then,
# we're going to obey the spec as is.
# "Words of *TEXT MAY contain characters from character sets other than
# ISO-8859-1 only when encoded according to the rules of RFC 2047."
use_rfc_2047 = True
def elements(self, key):
"""Return a sorted list of HeaderElements for the given header."""
key = str(key).title()
value = self.get(key)
return header_elements(key, value)
def values(self, key):
"""Return a sorted list of HeaderElement.value for the given header."""
return [e.value for e in self.elements(key)]
def output(self):
"""Transform self into a list of (name, value) tuples."""
header_list = []
for k, v in self.items():
return list(self.encode_header_items(self.items()))
def encode_header_items(cls, header_items):
"""
Prepare the sequence of name, value tuples into a form suitable for
transmitting on the wire for HTTP.
"""
for k, v in header_items:
if isinstance(k, unicodestr):
k = self.encode(k)
k = cls.encode(k)
if not isinstance(v, basestring):
v = str(v)
if isinstance(v, unicodestr):
v = self.encode(v)
v = cls.encode(v)
# See header_translate_* constants above.
# Replace only if you really know what you're doing.
k = k.translate(header_translate_table, header_translate_deletechars)
v = v.translate(header_translate_table, header_translate_deletechars)
header_list.append((k, v))
return header_list
def encode(self, v):
k = k.translate(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)
def encode(cls, v):
"""Return the given header name or value, encoded for HTTP output."""
for enc in self.encodings:
for enc in cls.encodings:
try:
return v.encode(enc)
except UnicodeEncodeError:
continue
if self.protocol == (1, 1) and self.use_rfc_2047:
# Encode RFC-2047 TEXT
# (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
if cls.protocol == (1, 1) and cls.use_rfc_2047:
# Encode RFC-2047 TEXT
# (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
# We do our own here instead of using the email module
# because we never want to fold lines--folding has
# been deprecated by the HTTP working group.
v = b2a_base64(v.encode('utf-8'))
return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
raise ValueError("Could not encode header part %r using "
"any of the encodings %r." %
(v, self.encodings))
(v, cls.encodings))
encode = classmethod(encode)
class Host(object):
"""An internet address.
name
Should be the client's host name. If not available (because no DNS
lookup is performed), the IP address should be used instead.
"""
ip = "0.0.0.0"
port = 80
name = "unknown.tld"
def __init__(self, ip, port, name=None):
self.ip = ip
self.port = port
if name is None:
name = ip
self.name = name
def __repr__(self):
return "httputil.Host(%r, %r, %r)" % (self.ip, self.port, self.name)

View File

@ -1,20 +1,21 @@
import sys
import cherrypy
from cherrypy._cpcompat import basestring, ntou, json, json_encode, json_decode
from cherrypy._cpcompat import basestring, ntou, json_encode, json_decode
def json_processor(entity):
"""Read application/json data into request.json."""
if not entity.headers.get(ntou("Content-Length"), ntou("")):
raise cherrypy.HTTPError(411)
body = entity.fp.read()
try:
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')],
force=True, debug=False, processor = json_processor):
force=True, debug=False, processor=json_processor):
"""Add a processor to parse JSON request entities:
The default processor places the parsed data into request.json.
@ -22,11 +23,11 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
be deserialized from JSON to the Python equivalent, and the result
stored at cherrypy.request.json. The 'content_type' argument may
be a Content-Type string or a list of allowable Content-Type strings.
If the 'force' argument is True (the default), then entities of other
content types will not be allowed; "415 Unsupported Media Type" is
raised instead.
Supply your own processor to use a custom decoder, or to handle the parsed
data differently. The processor can be configured via
tools.json_in.processor or via the decorator method.
@ -35,14 +36,14 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
request header, or it will raise "411 Length Required". If for any
other reason the request entity cannot be deserialized from JSON,
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
if isinstance(content_type, basestring):
content_type = [content_type]
if force:
if debug:
cherrypy.log('Removing body processors %s' %
@ -51,19 +52,22 @@ def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
request.body.default_proc = cherrypy.HTTPError(
415, 'Expected an entity of content type %s' %
', '.join(content_type))
for ct in content_type:
if debug:
cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN')
request.body.processors[ct] = processor
def json_handler(*args, **kwargs):
value = cherrypy.serving.request._json_inner_handler(*args, **kwargs)
return json_encode(value)
def json_out(content_type='application/json', debug=False, handler=json_handler):
def json_out(content_type='application/json', debug=False,
handler=json_handler):
"""Wrap request.handler to serialize its output to JSON. Sets Content-Type.
If the given content_type is None, the Content-Type response header
is not set.
@ -75,6 +79,11 @@ def json_out(content_type='application/json', debug=False, handler=json_handler)
package importable; otherwise, ValueError is raised during processing.
"""
request = cherrypy.serving.request
# request.handler may be set to None by e.g. the caching tool
# to signal to all components that a response body has already
# been attached, in which case we don't need to wrap anything.
if request.handler is None:
return
if debug:
cherrypy.log('Replacing %s with JSON handler' % request.handler,
'TOOLS.JSON_OUT')
@ -82,6 +91,6 @@ def json_out(content_type='application/json', debug=False, handler=json_handler)
request.handler = handler
if content_type is not None:
if debug:
cherrypy.log('Setting Content-Type to %s' % content_type, 'TOOLS.JSON_OUT')
cherrypy.log('Setting Content-Type to %s' %
content_type, 'TOOLS.JSON_OUT')
cherrypy.serving.response.headers['Content-Type'] = content_type

View File

@ -0,0 +1,147 @@
"""
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

@ -0,0 +1,47 @@
import datetime
class NeverExpires(object):
def expired(self):
return False
class Timer(object):
"""
A simple timer that will indicate when an expiration time has passed.
"""
def __init__(self, expiration):
"Create a timer that expires at `expiration` (UTC datetime)"
self.expiration = expiration
@classmethod
def after(cls, elapsed):
"""
Return a timer that will expire after `elapsed` passes.
"""
return cls(datetime.datetime.utcnow() + elapsed)
def expired(self):
return datetime.datetime.utcnow() >= self.expiration
class LockTimeout(Exception):
"An exception when a lock could not be acquired before a timeout period"
class LockChecker(object):
"""
Keep track of the time and detect if a timeout has expired
"""
def __init__(self, session_id, timeout):
self.session_id = session_id
if timeout:
self.timer = Timer.after(timeout)
else:
self.timer = NeverExpires()
def expired(self):
if self.timer.expired():
raise LockTimeout(
"Timeout acquiring lock for %(session_id)s" % vars(self))
return False

View File

@ -6,17 +6,17 @@ CherryPy users
You can profile any of your pages as follows::
from cherrypy.lib import profiler
class Root:
p = profile.Profiler("/path/to/profile/dir")
def index(self):
self.p.run(self._index)
index.exposed = True
def _index(self):
return "Hello, world!"
cherrypy.tree.mount(Root())
You can also turn on profiling for all requests
@ -35,7 +35,8 @@ module from the command line, it will call ``serve()`` for you.
def new_func_strip_path(func_name):
"""Make profiler output more readable by adding ``__init__`` modules' parents"""
"""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
@ -49,23 +50,25 @@ except ImportError:
profile = None
pstats = None
import os, os.path
import os
import os.path
import sys
import warnings
from cherrypy._cpcompat import BytesIO
from cherrypy._cpcompat import StringIO
_count = 0
class Profiler(object):
def __init__(self, path=None):
if not path:
path = os.path.join(os.path.dirname(__file__), "profile")
self.path = path
if not os.path.exists(path):
os.makedirs(path)
def run(self, func, *args, **params):
"""Dump profile data into self.path."""
global _count
@ -75,17 +78,17 @@ class Profiler(object):
result = prof.runcall(func, *args, **params)
prof.dump_stats(path)
return result
def statfiles(self):
""":rtype: list of available profiles.
"""
return [f for f in os.listdir(self.path)
if f.startswith("cp_") and f.endswith(".prof")]
def stats(self, filename, sortby='cumulative'):
""":rtype stats(index): output of print_stats() for the given profile.
"""
sio = BytesIO()
sio = StringIO()
if sys.version_info >= (2, 5):
s = pstats.Stats(os.path.join(self.path, filename), stream=sio)
s.strip_dirs()
@ -106,7 +109,7 @@ class Profiler(object):
response = sio.getvalue()
sio.close()
return response
def index(self):
return """<html>
<head><title>CherryPy profile data</title></head>
@ -117,16 +120,17 @@ class Profiler(object):
</html>
"""
index.exposed = True
def menu(self):
yield "<h2>Profiling runs</h2>"
yield "<p>Click on one of the runs below to see profiling data.</p>"
runs = self.statfiles()
runs.sort()
for i in runs:
yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (i, i)
yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (
i, i)
menu.exposed = True
def report(self, filename):
import cherrypy
cherrypy.response.headers['Content-Type'] = 'text/plain'
@ -135,50 +139,53 @@ class Profiler(object):
class ProfileAggregator(Profiler):
def __init__(self, path=None):
Profiler.__init__(self, path)
global _count
self.count = _count = _count + 1
self.profiler = profile.Profile()
def run(self, func, *args):
def run(self, func, *args, **params):
path = os.path.join(self.path, "cp_%04d.prof" % self.count)
result = self.profiler.runcall(func, *args)
result = self.profiler.runcall(func, *args, **params)
self.profiler.dump_stats(path)
return result
class make_app:
def __init__(self, nextapp, path=None, aggregate=False):
"""Make a WSGI middleware app which wraps 'nextapp' with profiling.
nextapp
the WSGI application to wrap, usually an instance of
cherrypy.Application.
path
where to dump the profiling output.
aggregate
if True, profile data for all HTTP requests will go in
a single file. If False (the default), each HTTP request will
dump its profile data into a separate file.
"""
if profile is None or pstats is None:
msg = ("Your installation of Python does not have a profile module. "
"If you're on Debian, try `sudo apt-get install python-profiler`. "
"See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
msg = ("Your installation of Python does not have a profile "
"module. If you're on Debian, try "
"`sudo apt-get install python-profiler`. "
"See http://www.cherrypy.org/wiki/ProfilingOnDebian "
"for details.")
warnings.warn(msg)
self.nextapp = nextapp
self.aggregate = aggregate
if aggregate:
self.profiler = ProfileAggregator(path)
else:
self.profiler = Profiler(path)
def __call__(self, environ, start_response):
def gather():
result = []
@ -191,10 +198,12 @@ class make_app:
def serve(path=None, port=8080):
if profile is None or pstats is None:
msg = ("Your installation of Python does not have a profile module. "
"If you're on Debian, try `sudo apt-get install python-profiler`. "
"See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.")
"If you're on Debian, try "
"`sudo apt-get install python-profiler`. "
"See http://www.cherrypy.org/wiki/ProfilingOnDebian "
"for details.")
warnings.warn(msg)
import cherrypy
cherrypy.config.update({'server.socket_port': int(port),
'server.thread_pool': 10,
@ -205,4 +214,3 @@ def serve(path=None, port=8080):
if __name__ == "__main__":
serve(*tuple(sys.argv[1:]))

View File

@ -44,6 +44,7 @@ except ImportError:
import operator as _operator
import sys
def as_dict(config):
"""Return a dict from 'config' whether it is a dict, file, or filename."""
if isinstance(config, basestring):
@ -54,26 +55,27 @@ def as_dict(config):
class NamespaceSet(dict):
"""A dict of config namespace names and handlers.
Each config entry should begin with a namespace name; the corresponding
namespace handler will be called once for each config entry in that
namespace, and will be passed two arguments: the config key (with the
namespace removed) and the config value.
Namespace handlers may be any Python callable; they may also be
Python 2.5-style 'context managers', in which case their __enter__
method should return a callable to be used as the handler.
See cherrypy.tools (the Toolbox class) for an example.
"""
def __call__(self, config):
"""Iterate through config and pass it to each namespace handler.
config
A flat dict, where keys use dots to separate
namespaces, and values are arbitrary.
The first name in each config key is used to look up the corresponding
namespace handler. For example, a config entry of {'tools.gzip.on': v}
will call the 'tools' namespace handler with the args: ('gzip.on', v)
@ -85,7 +87,7 @@ class NamespaceSet(dict):
ns, name = k.split(".", 1)
bucket = ns_confs.setdefault(ns, {})
bucket[name] = config[k]
# I chose __enter__ and __exit__ so someday this could be
# rewritten using Python 2.5's 'with' statement:
# for ns, handler in self.iteritems():
@ -116,11 +118,11 @@ class NamespaceSet(dict):
else:
for k, v in ns_confs.get(ns, {}).items():
handler(k, v)
def __repr__(self):
return "%s.%s(%s)" % (self.__module__, self.__class__.__name__,
dict.__repr__(self))
def __copy__(self):
newobj = self.__class__()
newobj.update(self)
@ -129,27 +131,28 @@ class NamespaceSet(dict):
class Config(dict):
"""A dict-like set of configuration data, with defaults and namespaces.
May take a file, filename, or dict.
"""
defaults = {}
environments = {}
namespaces = NamespaceSet()
def __init__(self, file=None, **kwargs):
self.reset()
if file is not None:
self.update(file)
if kwargs:
self.update(kwargs)
def reset(self):
"""Reset self to default values."""
self.clear()
dict.update(self, self.defaults)
def update(self, config):
"""Update self from a dict, file or filename."""
if isinstance(config, basestring):
@ -161,7 +164,7 @@ class Config(dict):
else:
config = config.copy()
self._apply(config)
def _apply(self, config):
"""Update self from a dict."""
which_env = config.get('environment')
@ -170,23 +173,24 @@ class Config(dict):
for k in env:
if k not in config:
config[k] = env[k]
dict.update(self, config)
self.namespaces(config)
def __setitem__(self, k, v):
dict.__setitem__(self, k, v)
self.namespaces({k: v})
class Parser(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.
"""
def optionxform(self, optionstr):
return optionstr
def read(self, filenames):
if isinstance(filenames, basestring):
filenames = [filenames]
@ -200,7 +204,7 @@ class Parser(ConfigParser):
self._read(fp, filename)
finally:
fp.close()
def as_dict(self, raw=False, vars=None):
"""Convert an INI file to a dictionary"""
# Load INI file into a dict
@ -220,7 +224,7 @@ class Parser(ConfigParser):
raise ValueError(msg, x.__class__.__name__, x.args)
result[section][option] = value
return result
def dict_from_file(self, file):
if hasattr(file, 'read'):
self.readfp(file)
@ -233,14 +237,14 @@ class Parser(ConfigParser):
class _Builder2:
def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None:
raise TypeError("unrepr does not recognize %s" %
repr(o.__class__.__name__))
return m(o)
def astnode(self, s):
"""Return a Python2 ast Node compiled from a string."""
try:
@ -249,40 +253,58 @@ class _Builder2:
# Fallback to eval when compiler package is not available,
# e.g. IronPython 1.0.
return eval(s)
p = compiler.parse("__tempvalue__ = " + s)
return p.getChildren()[1].getChildren()[0].getChildren()[1]
def build_Subscript(self, o):
expr, flags, subs = o.getChildren()
expr = self.build(expr)
subs = self.build(subs)
return expr[subs]
def build_CallFunc(self, o):
children = map(self.build, o.getChildren())
callee = children.pop(0)
kwargs = children.pop() or {}
starargs = children.pop() or ()
args = tuple(children) + tuple(starargs)
children = o.getChildren()
# Build callee from first child
callee = self.build(children[0])
# Build args and kwargs from remaining children
args = []
kwargs = {}
for child in children[1:]:
class_name = child.__class__.__name__
# None is ignored
if class_name == 'NoneType':
continue
# Keywords become kwargs
if class_name == 'Keyword':
kwargs.update(self.build(child))
# Everything else becomes args
else :
args.append(self.build(child))
return callee(*args, **kwargs)
def build_Keyword(self, o):
key, value_obj = o.getChildren()
value = self.build(value_obj)
kw_dict = {key: value}
return kw_dict
def build_List(self, o):
return map(self.build, o.getChildren())
def build_Const(self, o):
return o.value
def build_Dict(self, o):
d = {}
i = iter(map(self.build, o.getChildren()))
for el in i:
d[el] = i.next()
return d
def build_Tuple(self, o):
return tuple(self.build_List(o))
def build_Name(self, o):
name = o.name
if name == 'None':
@ -291,21 +313,21 @@ class _Builder2:
return True
if name == 'False':
return False
# See if the Name is a package or module. If it is, import it.
try:
return modules(name)
except ImportError:
pass
# See if the Name is in builtins.
try:
return getattr(builtins, name)
except AttributeError:
pass
raise TypeError("unrepr could not resolve the name %s" % repr(name))
def build_Add(self, o):
left, right = map(self.build, o.getChildren())
return left + right
@ -313,30 +335,30 @@ class _Builder2:
def build_Mul(self, o):
left, right = map(self.build, o.getChildren())
return left * right
def build_Getattr(self, o):
parent = self.build(o.expr)
return getattr(parent, o.attrname)
def build_NoneType(self, o):
return None
def build_UnarySub(self, o):
return -self.build(o.getChildren()[0])
def build_UnaryAdd(self, o):
return self.build(o.getChildren()[0])
class _Builder3:
def build(self, o):
m = getattr(self, 'build_' + o.__class__.__name__, None)
if m is None:
raise TypeError("unrepr does not recognize %s" %
repr(o.__class__.__name__))
return m(o)
def astnode(self, s):
"""Return a Python3 ast Node compiled from a string."""
try:
@ -351,46 +373,46 @@ class _Builder3:
def build_Subscript(self, o):
return self.build(o.value)[self.build(o.slice)]
def build_Index(self, o):
return self.build(o.value)
def build_Call(self, o):
callee = self.build(o.func)
if o.args is None:
args = ()
else:
args = tuple([self.build(a) for a in o.args])
else:
args = tuple([self.build(a) for a in o.args])
if o.starargs is None:
starargs = ()
else:
starargs = self.build(o.starargs)
if o.kwargs is None:
kwargs = {}
else:
kwargs = self.build(o.kwargs)
return callee(*(args + starargs), **kwargs)
def build_List(self, o):
return list(map(self.build, o.elts))
def build_Str(self, o):
return o.s
def build_Num(self, o):
return o.n
def build_Dict(self, o):
return dict([(self.build(k), self.build(v))
for k, v in zip(o.keys, o.values)])
def build_Tuple(self, o):
return tuple(self.build_List(o))
def build_Name(self, o):
name = o.id
if name == 'None':
@ -399,28 +421,31 @@ class _Builder3:
return True
if name == 'False':
return False
# See if the Name is a package or module. If it is, import it.
try:
return modules(name)
except ImportError:
pass
# See if the Name is in builtins.
try:
import builtins
return getattr(builtins, name)
except AttributeError:
pass
raise TypeError("unrepr could not resolve the name %s" % repr(name))
def build_NameConstant(self, o):
return o.value
def build_UnaryOp(self, o):
op, operand = map(self.build, [o.op, o.operand])
return op(operand)
def build_BinOp(self, o):
left, op, right = map(self.build, [o.left, o.op, o.right])
left, op, right = map(self.build, [o.left, o.op, o.right])
return op(left, right)
def build_Add(self, o):
@ -428,7 +453,7 @@ class _Builder3:
def build_Mult(self, o):
return _operator.mul
def build_USub(self, o):
return _operator.neg
@ -454,23 +479,18 @@ def unrepr(s):
def modules(modulePath):
"""Load a module and retrieve a reference to that module."""
try:
mod = sys.modules[modulePath]
if mod is None:
raise KeyError()
except KeyError:
# The last [''] is important.
mod = __import__(modulePath, globals(), locals(), [''])
return mod
__import__(modulePath)
return sys.modules[modulePath]
def attributes(full_attribute_name):
"""Load a module and retrieve an attribute of that module."""
# Parse out the path, module, and attribute
last_dot = full_attribute_name.rfind(".")
attr_name = full_attribute_name[last_dot + 1:]
mod_path = full_attribute_name[:last_dot]
mod = modules(mod_path)
# Let an AttributeError propagate outward.
try:
@ -478,8 +498,6 @@ def attributes(full_attribute_name):
except AttributeError:
raise AttributeError("'%s' object has no attribute '%s'"
% (mod_path, attr_name))
# Return a reference to the attribute.
return attr

File diff suppressed because it is too large Load Diff

View File

@ -1,40 +1,41 @@
import os
import re
import stat
import mimetypes
try:
from io import UnsupportedOperation
except ImportError:
UnsupportedOperation = object()
import logging
import mimetypes
mimetypes.init()
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'
import os
import re
import stat
import time
import cherrypy
from cherrypy._cpcompat import ntob, unquote
from cherrypy.lib import cptools, httputil, file_generator_limited
def serve_file(path, content_type=None, disposition=None, name=None, debug=False):
mimetypes.init()
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'
def serve_file(path, content_type=None, disposition=None, name=None,
debug=False):
"""Set status, headers, and body in order to serve the given path.
The Content-Type header will be set to the content_type arg, if provided.
If not provided, the Content-Type will be guessed by the file extension
of the 'path' argument.
If disposition is not None, the Content-Disposition header will be set
to "<disposition>; filename=<name>". If name is None, it will be set
to the basename of path. If disposition is None, no Content-Disposition
header will be written.
"""
response = cherrypy.serving.response
# If path is relative, users should fix it by making path absolute.
# That is, CherryPy should not guess where the application root is.
# It certainly should *not* use cwd (since CP may be invoked from a
@ -45,26 +46,26 @@ def serve_file(path, content_type=None, disposition=None, name=None, debug=False
if debug:
cherrypy.log(msg, 'TOOLS.STATICFILE')
raise ValueError(msg)
try:
st = os.stat(path)
except OSError:
if debug:
cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
raise cherrypy.NotFound()
# Check if path is a directory.
if stat.S_ISDIR(st.st_mode):
# Let the caller deal with it as they like.
if debug:
cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
raise cherrypy.NotFound()
# Set the Last-Modified response header, so that
# modified-since validation code can work.
response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
cptools.validate_since()
if content_type is None:
# Set content-type based on filename extension
ext = ""
@ -76,7 +77,7 @@ def serve_file(path, content_type=None, disposition=None, name=None, debug=False
response.headers['Content-Type'] = content_type
if debug:
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
cd = None
if disposition is not None:
if name is None:
@ -85,19 +86,20 @@ def serve_file(path, content_type=None, disposition=None, name=None, debug=False
response.headers["Content-Disposition"] = cd
if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
# Set Content-Length and use an iterable (file object)
# this way CP won't load the whole file in memory
content_length = st.st_size
fileobj = open(path, 'rb')
return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
debug=False):
"""Set status, headers, and body in order to serve the given file object.
The Content-Type header will be set to the content_type arg, if provided.
If disposition is not None, the Content-Disposition header will be set
to "<disposition>; filename=<name>". If name is None, 'filename' will
not be set. If disposition is None, no Content-Disposition header will
@ -110,9 +112,9 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
serve_fileobj(), expecting that the data would be served starting from that
position.
"""
response = cherrypy.serving.response
try:
st = os.fstat(fileobj.fileno())
except AttributeError:
@ -127,12 +129,12 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
cptools.validate_since()
content_length = st.st_size
if content_type is not None:
response.headers['Content-Type'] = content_type
if debug:
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
cd = None
if disposition is not None:
if name is None:
@ -142,13 +144,14 @@ def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
response.headers["Content-Disposition"] = cd
if debug:
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
def _serve_fileobj(fileobj, content_type, content_length, debug=False):
"""Internal. Set response.body to the given file object, perhaps ranged."""
response = cherrypy.serving.response
# HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
request = cherrypy.serving.request
if request.protocol >= (1, 1):
@ -156,11 +159,12 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
r = httputil.get_ranges(request.headers.get('Range'), content_length)
if r == []:
response.headers['Content-Range'] = "bytes */%s" % content_length
message = "Invalid Range (first-byte-pos greater than Content-Length)"
message = ("Invalid Range (first-byte-pos greater than "
"Content-Length)")
if debug:
cherrypy.log(message, 'TOOLS.STATIC')
raise cherrypy.HTTPError(416, message)
if r:
if len(r) == 1:
# Return a single-part response.
@ -169,8 +173,9 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
stop = content_length
r_len = stop - start
if debug:
cherrypy.log('Single part; start: %r, stop: %r' % (start, stop),
'TOOLS.STATIC')
cherrypy.log(
'Single part; start: %r, stop: %r' % (start, stop),
'TOOLS.STATIC')
response.status = "206 Partial Content"
response.headers['Content-Range'] = (
"bytes %s-%s/%s" % (start, stop - 1, content_length))
@ -182,36 +187,42 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
response.status = "206 Partial Content"
try:
# Python 3
from email.generator import _make_boundary as choose_boundary
from email.generator import _make_boundary as make_boundary
except ImportError:
# Python 2
from mimetools import choose_boundary
boundary = choose_boundary()
from mimetools import choose_boundary as make_boundary
boundary = make_boundary()
ct = "multipart/byteranges; boundary=%s" % boundary
response.headers['Content-Type'] = ct
if "Content-Length" in response.headers:
# Delete Content-Length header so finalize() recalcs it.
del response.headers["Content-Length"]
def file_ranges():
# Apache compatibility:
yield ntob("\r\n")
for start, stop in r:
if debug:
cherrypy.log('Multipart; start: %r, stop: %r' % (start, stop),
'TOOLS.STATIC')
cherrypy.log(
'Multipart; start: %r, stop: %r' % (
start, stop),
'TOOLS.STATIC')
yield ntob("--" + boundary, 'ascii')
yield ntob("\r\nContent-type: %s" % content_type, 'ascii')
yield ntob("\r\nContent-range: bytes %s-%s/%s\r\n\r\n"
% (start, stop - 1, content_length), 'ascii')
yield ntob("\r\nContent-type: %s" % content_type,
'ascii')
yield ntob(
"\r\nContent-range: bytes %s-%s/%s\r\n\r\n" % (
start, stop - 1, content_length),
'ascii')
fileobj.seek(start)
for chunk in file_generator_limited(fileobj, stop-start):
gen = file_generator_limited(fileobj, stop - start)
for chunk in gen:
yield chunk
yield ntob("\r\n")
# Final boundary
yield ntob("--" + boundary + "--", 'ascii')
# Apache compatibility:
yield ntob("\r\n")
response.body = file_ranges()
@ -219,13 +230,14 @@ def _serve_fileobj(fileobj, content_type, content_length, debug=False):
else:
if debug:
cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
# Set Content-Length and use an iterable (file object)
# this way CP won't load the whole file in memory
response.headers['Content-Length'] = content_length
response.body = fileobj
return response.body
def serve_download(path, name=None):
"""Serve 'path' as an application/x-download attachment."""
# This is such a common idiom I felt it deserved its own wrapper.
@ -252,20 +264,21 @@ def _attempt(filename, content_types, debug=False):
cherrypy.log('NotFound', 'TOOLS.STATICFILE')
return False
def staticdir(section, dir, root="", match="", content_types=None, index="",
debug=False):
"""Serve a static resource from the given (root +) dir.
match
If given, request.path_info will be searched for the given
regular expression before attempting to serve static content.
content_types
If given, it should be a Python dictionary of
{file-extension: content-type} pairs, where 'file-extension' is
a string (e.g. "gif") and 'content-type' is the value to write
out in the Content-Type response header (e.g. "image/gif").
index
If provided, it should be the (relative) name of a file to
serve for directory requests. For example, if the dir argument is
@ -277,13 +290,13 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
if debug:
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
return False
if match and not re.search(match, request.path_info):
if debug:
cherrypy.log('request.path_info %r does not match pattern %r' %
(request.path_info, match), 'TOOLS.STATICDIR')
return False
# Allow the use of '~' to refer to a user's home directory.
dir = os.path.expanduser(dir)
@ -295,7 +308,7 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
cherrypy.log(msg, 'TOOLS.STATICDIR')
raise ValueError(msg)
dir = os.path.join(root, dir)
# Determine where we are in the object tree relative to 'section'
# (where the static tool was defined).
if section == 'global':
@ -303,19 +316,19 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
section = section.rstrip(r"\/")
branch = request.path_info[len(section) + 1:]
branch = unquote(branch.lstrip(r"\/"))
# If branch is "", filename will end in a slash
filename = os.path.join(dir, branch)
if debug:
cherrypy.log('Checking file %r to fulfill %r' %
(filename, request.path_info), 'TOOLS.STATICDIR')
# There's a chance that the branch pulled from the URL might
# have ".." or similar uplevel attacks in it. Check that the final
# filename is a child of dir.
if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
raise cherrypy.HTTPError(403) # Forbidden
raise cherrypy.HTTPError(403) # Forbidden
handled = _attempt(filename, content_types)
if not handled:
# Check for an index file if a folder was requested.
@ -325,39 +338,41 @@ def staticdir(section, dir, root="", match="", content_types=None, index="",
request.is_index = filename[-1] in (r"\/")
return handled
def staticfile(filename, root=None, match="", content_types=None, debug=False):
"""Serve a static resource from the given (root +) filename.
match
If given, request.path_info will be searched for the given
regular expression before attempting to serve static content.
content_types
If given, it should be a Python dictionary of
{file-extension: content-type} pairs, where 'file-extension' is
a string (e.g. "gif") and 'content-type' is the value to write
out in the Content-Type response header (e.g. "image/gif").
"""
request = cherrypy.serving.request
if request.method not in ('GET', 'HEAD'):
if debug:
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
return False
if match and not re.search(match, request.path_info):
if debug:
cherrypy.log('request.path_info %r does not match pattern %r' %
(request.path_info, match), 'TOOLS.STATICFILE')
return False
# If filename is relative, make absolute using "root".
if not os.path.isabs(filename):
if not root:
msg = "Static tool requires an absolute filename (got '%s')." % filename
msg = "Static tool requires an absolute filename (got '%s')." % (
filename,)
if debug:
cherrypy.log(msg, 'TOOLS.STATICFILE')
raise ValueError(msg)
filename = os.path.join(root, filename)
return _attempt(filename, content_types, debug=debug)

View File

@ -3,6 +3,7 @@ import sys
import cherrypy
from cherrypy._cpcompat import ntob
def get_xmlrpclib():
try:
import xmlrpc.client as x
@ -10,6 +11,7 @@ def get_xmlrpclib():
import xmlrpclib as x
return x
def process_body():
"""Return (params, method) from request body."""
try:
@ -48,8 +50,8 @@ def respond(body, encoding='utf-8', allow_none=0):
encoding=encoding,
allow_none=allow_none))
def on_error(*args, **kwargs):
body = str(sys.exc_info()[1])
xmlrpclib = get_xmlrpclib()
_set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body)))

View File

@ -7,7 +7,8 @@ import sys
import time
import threading
from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, set
from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident
from cherrypy._cpcompat import ntob, set, Timer, SetDaemonProperty
# _module__file__base is used by Autoreload to make
# absolute any filenames retrieved from sys.modules which are not
@ -19,8 +20,8 @@ from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, s
# changes the current directory by executing os.chdir(), then the next time
# Autoreload runs, it will not be able to find any filenames which are
# not absolute paths, because the current directory is not the same as when the
# module was first imported. Autoreload will then wrongly conclude the file has
# "changed", and initiate the shutdown/re-exec sequence.
# module was first imported. Autoreload will then wrongly conclude the file
# has "changed", and initiate the shutdown/re-exec sequence.
# See ticket #917.
# For this workaround to have a decent probability of success, this module
# needs to be imported as early as possible, before the app has much chance
@ -29,14 +30,16 @@ _module__file__base = os.getcwd()
class SimplePlugin(object):
"""Plugin base class which auto-subscribes methods for known channels."""
bus = None
"""A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine."""
"""A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine.
"""
def __init__(self, bus):
self.bus = bus
def subscribe(self):
"""Register this object as a (multi-channel) listener on the bus."""
for channel in self.bus.listeners:
@ -44,7 +47,7 @@ class SimplePlugin(object):
method = getattr(self, channel, None)
if method is not None:
self.bus.subscribe(channel, method)
def unsubscribe(self):
"""Unregister this object as a listener on the bus."""
for channel in self.bus.listeners:
@ -54,42 +57,42 @@ class SimplePlugin(object):
self.bus.unsubscribe(channel, method)
class SignalHandler(object):
"""Register bus channels (and listeners) for system signals.
You can modify what signals your application listens for, and what it does
when it receives signals, by modifying :attr:`SignalHandler.handlers`,
a dict of {signal name: callback} pairs. The default set is::
handlers = {'SIGTERM': self.bus.exit,
'SIGHUP': self.handle_SIGHUP,
'SIGUSR1': self.bus.graceful,
}
The :func:`SignalHandler.handle_SIGHUP`` method calls
:func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>`
if the process is daemonized, but
:func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>`
if the process is attached to a TTY. This is because Unix window
managers tend to send SIGHUP to terminal windows when the user closes them.
Feel free to add signals which are not available on every platform. The
:class:`SignalHandler` will ignore errors raised from attempting to register
handlers for unknown signals.
Feel free to add signals which are not available on every platform.
The :class:`SignalHandler` will ignore errors raised from attempting
to register handlers for unknown signals.
"""
handlers = {}
"""A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit)."""
signals = {}
"""A map from signal numbers to names."""
for k, v in vars(_signal).items():
if k.startswith('SIG') and not k.startswith('SIG_'):
signals[v] = k
del k, v
def __init__(self, bus):
self.bus = bus
# Set default handlers
@ -106,12 +109,12 @@ class SignalHandler(object):
self.handlers['SIGINT'] = self._jython_SIGINT_handler
self._previous_handlers = {}
def _jython_SIGINT_handler(self, signum=None, frame=None):
# See http://bugs.jython.org/issue1313
self.bus.log('Keyboard Interrupt: shutting down bus')
self.bus.exit()
def subscribe(self):
"""Subscribe self.handlers to signals."""
for sig, func in self.handlers.items():
@ -119,18 +122,18 @@ class SignalHandler(object):
self.set_handler(sig, func)
except ValueError:
pass
def unsubscribe(self):
"""Unsubscribe self.handlers from signals."""
for signum, handler in self._previous_handlers.items():
signame = self.signals[signum]
if handler is None:
self.bus.log("Restoring %s handler to SIG_DFL." % signame)
handler = _signal.SIG_DFL
else:
self.bus.log("Restoring %s handler %r." % (signame, handler))
try:
our_handler = _signal.signal(signum, handler)
if our_handler is None:
@ -140,13 +143,13 @@ class SignalHandler(object):
except ValueError:
self.bus.log("Unable to restore %s handler %r." %
(signame, handler), level=40, traceback=True)
def set_handler(self, signal, listener=None):
"""Subscribe a handler for the given signal (number or name).
If the optional 'listener' argument is provided, it will be
subscribed as a listener for the given signal's channel.
If the given signal name or number is not available on the current
platform, ValueError is raised.
"""
@ -161,20 +164,20 @@ class SignalHandler(object):
except KeyError:
raise ValueError("No such signal: %r" % signal)
signum = signal
prev = _signal.signal(signum, self._handle_signal)
self._previous_handlers[signum] = prev
if listener is not None:
self.bus.log("Listening for %s." % signame)
self.bus.subscribe(signame, listener)
def _handle_signal(self, signum=None, frame=None):
"""Python signal handler (self.set_handler subscribes it for you)."""
signame = self.signals[signum]
self.bus.log("Caught signal %s." % signame)
self.bus.publish(signame)
def handle_SIGHUP(self):
"""Restart if daemonized, else exit."""
if os.isatty(sys.stdin.fileno()):
@ -187,26 +190,29 @@ class SignalHandler(object):
try:
import pwd, grp
import pwd
import grp
except ImportError:
pwd, grp = None, None
class DropPrivileges(SimplePlugin):
"""Drop privileges. uid/gid arguments not available on Windows.
Special thanks to Gavin Baker: http://antonym.org/node/100.
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):
SimplePlugin.__init__(self, bus)
self.finalized = False
self.uid = uid
self.gid = gid
self.umask = umask
def _get_uid(self):
return self._uid
def _set_uid(self, val):
if val is not None:
if pwd is None:
@ -217,10 +223,11 @@ class DropPrivileges(SimplePlugin):
val = pwd.getpwnam(val)[2]
self._uid = val
uid = property(_get_uid, _set_uid,
doc="The uid under which to run. Availability: Unix.")
doc="The uid under which to run. Availability: Unix.")
def _get_gid(self):
return self._gid
def _set_gid(self, val):
if val is not None:
if grp is None:
@ -231,10 +238,11 @@ class DropPrivileges(SimplePlugin):
val = grp.getgrnam(val)[2]
self._gid = val
gid = property(_get_gid, _set_gid,
doc="The gid under which to run. Availability: Unix.")
doc="The gid under which to run. Availability: Unix.")
def _get_umask(self):
return self._umask
def _set_umask(self, val):
if val is not None:
try:
@ -244,13 +252,16 @@ class DropPrivileges(SimplePlugin):
level=30)
val = None
self._umask = val
umask = property(_get_umask, _set_umask,
doc="""The default permission mode for newly created files and directories.
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):
# uid/gid
def current_ids():
@ -261,7 +272,7 @@ class DropPrivileges(SimplePlugin):
if grp:
group = grp.getgrgid(os.getgid())[0]
return name, group
if self.finalized:
if not (self.uid is None and self.gid is None):
self.bus.log('Already running as uid: %r gid: %r' %
@ -278,7 +289,7 @@ class DropPrivileges(SimplePlugin):
if self.uid is not None:
os.setuid(self.uid)
self.bus.log('Running as uid: %r gid: %r' % current_ids())
# umask
if self.finalized:
if self.umask is not None:
@ -290,7 +301,7 @@ class DropPrivileges(SimplePlugin):
old_umask = os.umask(self.umask)
self.bus.log('umask old: %03o, new: %03o' %
(old_umask, self.umask))
self.finalized = True
# This is slightly higher than the priority for server.start
# in order to facilitate the most common use: starting on a low
@ -299,12 +310,13 @@ class DropPrivileges(SimplePlugin):
class Daemonizer(SimplePlugin):
"""Daemonize the running script.
Use this with a Web Site Process Bus via::
Daemonizer(bus).subscribe()
When this component finishes, the process is completely decoupled from
the parent environment. Please note that when this component is used,
the return code from the parent process will still be 0 if a startup
@ -312,9 +324,9 @@ class Daemonizer(SimplePlugin):
process still return proper exit codes. Therefore, if you use this
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
indicates if the process successfully finished the first fork.
indicates if the process succesfully finished the first fork.
"""
def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
stderr='/dev/null'):
SimplePlugin.__init__(self, bus)
@ -322,11 +334,11 @@ class Daemonizer(SimplePlugin):
self.stdout = stdout
self.stderr = stderr
self.finalized = False
def start(self):
if self.finalized:
self.bus.log('Already deamonized.')
# forking has issues with threads:
# http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
# "The general problem with making fork() work in a multi-threaded
@ -336,15 +348,15 @@ class Daemonizer(SimplePlugin):
self.bus.log('There are %r active threads. '
'Daemonizing now may cause strange failures.' %
threading.enumerate(), level=30)
# 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)
# and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
# Finish up with the current stdout/stderr
sys.stdout.flush()
sys.stderr.flush()
# Do first fork.
try:
pid = os.fork()
@ -360,23 +372,23 @@ class Daemonizer(SimplePlugin):
exc = sys.exc_info()[1]
sys.exit("%s: fork #1 failed: (%d) %s\n"
% (sys.argv[0], exc.errno, exc.strerror))
os.setsid()
# Do second fork
try:
pid = os.fork()
if pid > 0:
self.bus.log('Forking twice.')
os._exit(0) # Exit second parent
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)
si = open(self.stdin, "r")
so = open(self.stdout, "a+")
se = open(self.stderr, "a+")
@ -387,30 +399,31 @@ class Daemonizer(SimplePlugin):
os.dup2(si.fileno(), sys.stdin.fileno())
os.dup2(so.fileno(), sys.stdout.fileno())
os.dup2(se.fileno(), sys.stderr.fileno())
self.bus.log('Daemonized to PID: %s' % os.getpid())
self.finalized = True
start.priority = 65
class PIDFile(SimplePlugin):
"""Maintain a PID file via a WSPBus."""
def __init__(self, bus, pidfile):
SimplePlugin.__init__(self, bus)
self.pidfile = pidfile
self.finalized = False
def start(self):
pid = os.getpid()
if self.finalized:
self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
else:
open(self.pidfile, "wb").write(ntob("%s" % pid, 'utf8'))
open(self.pidfile, "wb").write(ntob("%s\n" % pid, 'utf8'))
self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
self.finalized = True
start.priority = 70
def exit(self):
try:
os.remove(self.pidfile)
@ -421,14 +434,20 @@ class PIDFile(SimplePlugin):
pass
class PerpetualTimer(threading._Timer):
"""A responsive subclass of threading._Timer whose run() method repeats.
class PerpetualTimer(Timer):
"""A responsive subclass of threading.Timer whose run() method repeats.
Use this timer only when you really need a very interruptible timer;
this checks its 'finished' condition up to 20 times a second, which can
results in pretty high CPU usage
results in pretty high CPU usage
"""
def __init__(self, *args, **kwargs):
"Override parent constructor to allow 'bus' to be provided."
self.bus = kwargs.pop('bus', None)
super(PerpetualTimer, self).__init__(*args, **kwargs)
def run(self):
while True:
self.finished.wait(self.interval)
@ -437,22 +456,25 @@ class PerpetualTimer(threading._Timer):
try:
self.function(*self.args, **self.kwargs)
except Exception:
self.bus.log("Error in perpetual timer thread function %r." %
self.function, level=40, traceback=True)
if self.bus:
self.bus.log(
"Error in perpetual timer thread function %r." %
self.function, level=40, traceback=True)
# Quit on first error to avoid massive logs.
raise
class BackgroundTask(threading.Thread):
class BackgroundTask(SetDaemonProperty, threading.Thread):
"""A subclass of threading.Thread whose run() method repeats.
Use this class for most repeating tasks. It uses time.sleep() to wait
for each interval, which isn't very responsive; that is, even if you call
self.cancel(), you'll have to wait until the sleep() call finishes before
the thread stops. To compensate, it defaults to being daemonic, which means
it won't delay stopping the whole process.
"""
def __init__(self, interval, function, args=[], kwargs={}, bus=None):
threading.Thread.__init__(self)
self.interval = interval
@ -461,10 +483,13 @@ class BackgroundTask(threading.Thread):
self.kwargs = kwargs
self.running = False
self.bus = bus
# default to daemonic
self.daemon = True
def cancel(self):
self.running = False
def run(self):
self.running = True
while self.running:
@ -479,48 +504,49 @@ class BackgroundTask(threading.Thread):
% self.function, level=40, traceback=True)
# Quit on first error to avoid massive logs.
raise
def _set_daemon(self):
return True
class Monitor(SimplePlugin):
"""WSPBus listener to periodically run a callback in its own thread."""
callback = None
"""The function to call at intervals."""
frequency = 60
"""The time in seconds between callback runs."""
thread = None
"""A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>` thread."""
"""A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>`
thread.
"""
def __init__(self, bus, callback, frequency=60, name=None):
SimplePlugin.__init__(self, bus)
self.callback = callback
self.frequency = frequency
self.thread = None
self.name = name
def start(self):
"""Start our callback in its own background thread."""
if self.frequency > 0:
threadname = self.name or self.__class__.__name__
if self.thread is None:
self.thread = BackgroundTask(self.frequency, self.callback,
bus = self.bus)
bus=self.bus)
self.thread.setName(threadname)
self.thread.start()
self.bus.log("Started monitor thread %r." % threadname)
else:
self.bus.log("Monitor thread %r already started." % threadname)
start.priority = 70
def stop(self):
"""Stop our callback's background task thread."""
if self.thread is None:
self.bus.log("No thread running for %s." % self.name or self.__class__.__name__)
self.bus.log("No thread running for %s." %
self.name or self.__class__.__name__)
else:
if self.thread is not threading.currentThread():
name = self.thread.getName()
@ -530,7 +556,7 @@ class Monitor(SimplePlugin):
self.thread.join()
self.bus.log("Stopped thread %r." % name)
self.thread = None
def graceful(self):
"""Stop the callback's background task thread and restart it."""
self.stop()
@ -538,102 +564,111 @@ class Monitor(SimplePlugin):
class Autoreloader(Monitor):
"""Monitor which re-executes the process when files change.
This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`)
if any of the files it monitors change (or is deleted). By default, the
autoreloader monitors all imported modules; you can add to the
set by adding to ``autoreload.files``::
cherrypy.engine.autoreload.files.add(myFile)
If there are imported files you do *not* wish to monitor, you can adjust the
``match`` attribute, a regular expression. For example, to stop monitoring
cherrypy itself::
If there are imported files you do *not* wish to monitor, you can
adjust the ``match`` attribute, a regular expression. For example,
to stop monitoring cherrypy itself::
cherrypy.engine.autoreload.match = r'^(?!cherrypy).+'
Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins,
the autoreload plugin takes a ``frequency`` argument. The default is
1 second; that is, the autoreloader will examine files once each second.
"""
files = None
"""The set of files to poll for modifications."""
frequency = 1
"""The interval in seconds at which to poll for modified files."""
match = '.*'
"""A regular expression by which to match filenames."""
def __init__(self, bus, frequency=1, match='.*'):
self.mtimes = {}
self.files = set()
self.match = match
Monitor.__init__(self, bus, self.run, frequency)
def start(self):
"""Start our own background task thread for self.run."""
if self.thread is None:
self.mtimes = {}
Monitor.start(self)
start.priority = 70
start.priority = 70
def sysfiles(self):
"""Return a Set of sys.modules filenames to monitor."""
files = set()
for k, m in sys.modules.items():
for k, m in list(sys.modules.items()):
if re.match(self.match, k):
if hasattr(m, '__loader__') and hasattr(m.__loader__, 'archive'):
if (
hasattr(m, '__loader__') and
hasattr(m.__loader__, 'archive')
):
f = m.__loader__.archive
else:
f = getattr(m, '__file__', None)
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
f = os.path.normpath(os.path.join(_module__file__base, f))
# ensure absolute paths so a os.chdir() in the app
# doesn't break me
f = os.path.normpath(
os.path.join(_module__file__base, f))
files.add(f)
return files
def run(self):
"""Reload the process if registered files have been modified."""
for filename in self.sysfiles() | self.files:
if filename:
if filename.endswith('.pyc'):
filename = filename[:-1]
oldtime = self.mtimes.get(filename, 0)
if oldtime is None:
# Module with no .py file. Skip it.
continue
try:
mtime = os.stat(filename).st_mtime
except OSError:
# Either a module with no .py file, or it's been deleted.
mtime = None
if filename not in self.mtimes:
# If a module has no .py file, this will be None.
self.mtimes[filename] = mtime
else:
if mtime is None or mtime > oldtime:
# The file has been deleted or modified.
self.bus.log("Restarting because %s changed." % filename)
self.bus.log("Restarting because %s changed." %
filename)
self.thread.cancel()
self.bus.log("Stopped thread %r." % self.thread.getName())
self.bus.log("Stopped thread %r." %
self.thread.getName())
self.bus.restart()
return
class ThreadManager(SimplePlugin):
"""Manager for HTTP request threads.
If you have control over thread creation and destruction, publish to
the 'acquire_thread' and 'release_thread' channels (for each thread).
This will register/unregister the current thread and publish to
'start_thread' and 'stop_thread' listeners in the bus as needed.
If threads are created and destroyed by code you do not control
(e.g., Apache), then, at the beginning of every HTTP request,
publish to 'acquire_thread' only. You should not publish to
@ -641,10 +676,10 @@ class ThreadManager(SimplePlugin):
the thread will be re-used or not. The bus will call
'stop_thread' listeners for you when it stops.
"""
threads = None
"""A map of {thread ident: index number} pairs."""
def __init__(self, bus):
self.threads = {}
SimplePlugin.__init__(self, bus)
@ -655,7 +690,7 @@ class ThreadManager(SimplePlugin):
def acquire_thread(self):
"""Run 'start_thread' listeners for the current thread.
If the current thread has already been seen, any 'start_thread'
listeners will not be run again.
"""
@ -666,18 +701,17 @@ class ThreadManager(SimplePlugin):
i = len(self.threads) + 1
self.threads[thread_ident] = i
self.bus.publish('start_thread', i)
def release_thread(self):
"""Release the current thread and run 'stop_thread' listeners."""
thread_ident = get_thread_ident()
i = self.threads.pop(thread_ident, None)
if i is not None:
self.bus.publish('stop_thread', i)
def stop(self):
"""Release all threads and run all 'stop_thread' listeners."""
for thread_ident, i in self.threads.items():
self.bus.publish('stop_thread', i)
self.threads.clear()
graceful = stop

View File

@ -13,7 +13,9 @@ protocols, etc.), you can manually register each one and then start them all
with engine.start::
s1 = ServerAdapter(cherrypy.engine, MyWSGIServer(host='0.0.0.0', port=80))
s2 = ServerAdapter(cherrypy.engine, another.HTTPServer(host='127.0.0.1', SSL=True))
s2 = ServerAdapter(cherrypy.engine,
another.HTTPServer(host='127.0.0.1',
SSL=True))
s1.subscribe()
s2.subscribe()
cherrypy.engine.start()
@ -54,16 +56,16 @@ hello.py::
#!/usr/bin/python
import cherrypy
class HelloWorld:
\"""Sample request handler class.\"""
def index(self):
return "Hello world!"
index.exposed = True
cherrypy.tree.mount(HelloWorld())
# CherryPy autoreload must be disabled for the flup server to work
cherrypy.config.update({'engine.autoreload_on':False})
cherrypy.config.update({'engine.autoreload.on':False})
Then run :doc:`/deployguide/cherryd` with the '-f' arg::
@ -107,75 +109,92 @@ directive, configure your fastcgi script like the following::
} # end of $HTTP["url"] =~ "^/"
Please see `Lighttpd FastCGI Docs
<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for an explanation
of the possible configuration options.
<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for
an explanation of the possible configuration options.
"""
import sys
import time
import warnings
class ServerAdapter(object):
"""Adapter for an HTTP server.
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 with bus.start:
start them all with bus.start::
s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
s1.subscribe()
s2.subscribe()
bus.start()
"""
def __init__(self, bus, httpserver=None, bind_addr=None):
self.bus = bus
self.httpserver = httpserver
self.bind_addr = bind_addr
self.interrupt = None
self.running = False
def subscribe(self):
self.bus.subscribe('start', self.start)
self.bus.subscribe('stop', self.stop)
def unsubscribe(self):
self.bus.unsubscribe('start', self.start)
self.bus.unsubscribe('stop', self.stop)
def start(self):
"""Start the HTTP server."""
if self.bind_addr is None:
on_what = "unknown interface (dynamic?)"
elif isinstance(self.bind_addr, tuple):
host, port = self.bind_addr
on_what = "%s:%s" % (host, port)
on_what = self._get_base()
else:
on_what = "socket file: %s" % self.bind_addr
if self.running:
self.bus.log("Already serving on %s" % on_what)
return
self.interrupt = None
if not self.httpserver:
raise ValueError("No HTTP server has been created.")
# Start the httpserver in a new thread.
if isinstance(self.bind_addr, tuple):
wait_for_free_port(*self.bind_addr)
import threading
t = threading.Thread(target=self._start_http_thread)
t.setName("HTTPServer " + t.getName())
t.start()
self.wait()
self.running = True
self.bus.log("Serving on %s" % on_what)
start.priority = 75
def _get_base(self):
if not self.httpserver:
return ''
host, port = self.bind_addr
if getattr(self.httpserver, 'ssl_certificate', None) or \
getattr(self.httpserver, 'ssl_adapter', None):
scheme = "https"
if port != 443:
host += ":%s" % port
else:
scheme = "http"
if port != 80:
host += ":%s" % port
return "%s://%s" % (scheme, host)
def _start_http_thread(self):
"""HTTP servers MUST be running in new threads, so that the
main thread persists to receive KeyboardInterrupt's. If an
@ -200,19 +219,19 @@ class ServerAdapter(object):
traceback=True, level=40)
self.bus.exit()
raise
def wait(self):
"""Wait until the HTTP server is ready to receive requests."""
while not getattr(self.httpserver, "ready", False):
if self.interrupt:
raise self.interrupt
time.sleep(.1)
# Wait for port to be occupied
if isinstance(self.bind_addr, tuple):
host, port = self.bind_addr
wait_for_occupied_port(host, port)
def stop(self):
"""Stop the HTTP server."""
if self.running:
@ -226,7 +245,7 @@ class ServerAdapter(object):
else:
self.bus.log("HTTP Server %s already shut down" % self.httpserver)
stop.priority = 25
def restart(self):
"""Restart the HTTP server."""
self.stop()
@ -234,31 +253,33 @@ class ServerAdapter(object):
class FlupCGIServer(object):
"""Adapter for a flup.server.cgi.WSGIServer."""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.ready = False
def start(self):
"""Start the CGI server."""
# We have to instantiate the server class here because its __init__
# starts a threadpool. If we do it too early, daemonize won't work.
from flup.server.cgi import WSGIServer
self.cgiserver = WSGIServer(*self.args, **self.kwargs)
self.ready = True
self.cgiserver.run()
def stop(self):
"""Stop the HTTP server."""
self.ready = False
class FlupFCGIServer(object):
"""Adapter for a flup.server.fcgi.WSGIServer."""
def __init__(self, *args, **kwargs):
if kwargs.get('bindAddress', None) is None:
import socket
@ -270,7 +291,7 @@ class FlupFCGIServer(object):
self.args = args
self.kwargs = kwargs
self.ready = False
def start(self):
"""Start the FCGI server."""
# We have to instantiate the server class here because its __init__
@ -290,24 +311,26 @@ class FlupFCGIServer(object):
self.fcgiserver._oldSIGs = []
self.ready = True
self.fcgiserver.run()
def stop(self):
"""Stop the HTTP server."""
# Forcibly stop the fcgi server main event loop.
self.fcgiserver._keepGoing = False
# Force all worker threads to die off.
self.fcgiserver._threadPool.maxSpare = self.fcgiserver._threadPool._idleCount
self.fcgiserver._threadPool.maxSpare = (
self.fcgiserver._threadPool._idleCount)
self.ready = False
class FlupSCGIServer(object):
"""Adapter for a flup.server.scgi.WSGIServer."""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
self.ready = False
def start(self):
"""Start the SCGI server."""
# We have to instantiate the server class here because its __init__
@ -327,7 +350,7 @@ class FlupSCGIServer(object):
self.scgiserver._oldSIGs = []
self.ready = True
self.scgiserver.run()
def stop(self):
"""Stop the HTTP server."""
self.ready = False
@ -344,19 +367,21 @@ def client_host(server_host):
return '127.0.0.1'
if server_host in ('::', '::0', '::0.0.0.0'):
# :: is IN6ADDR_ANY, which should answer on localhost.
# ::0 and ::0.0.0.0 are non-canonical but common ways to write IN6ADDR_ANY.
# ::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:
@ -364,10 +389,12 @@ def check_port(host, port, timeout=1.0):
socket.SOCK_STREAM)
except socket.gaierror:
if ':' in host:
info = [(socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0))]
info = [(
socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0)
)]
else:
info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))]
for res in info:
af, socktype, proto, canonname, sa = res
s = None
@ -378,25 +405,27 @@ def check_port(host, port, timeout=1.0):
s.settimeout(timeout)
s.connect((host, port))
s.close()
raise IOError("Port %s is in use on %s; perhaps the previous "
"httpserver did not shut down properly." %
(repr(port), repr(host)))
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
@ -406,22 +435,32 @@ def wait_for_free_port(host, port, timeout=None):
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)
raise IOError("Port %r not bound on %r" % (port, host))
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

@ -11,17 +11,18 @@ from cherrypy.process import wspbus, plugins
class ConsoleCtrlHandler(plugins.SimplePlugin):
"""A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
def __init__(self, bus):
self.is_set = False
plugins.SimplePlugin.__init__(self, bus)
def start(self):
if self.is_set:
self.bus.log('Handler for console events already set.', level=40)
return
result = win32api.SetConsoleCtrlHandler(self.handle, 1)
if result == 0:
self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
@ -29,38 +30,38 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
else:
self.bus.log('Set handler for console events.', level=40)
self.is_set = True
def stop(self):
if not self.is_set:
self.bus.log('Handler for console events already off.', level=40)
return
try:
result = win32api.SetConsoleCtrlHandler(self.handle, 0)
except ValueError:
# "ValueError: The object has not been registered"
result = 1
if result == 0:
self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
win32api.GetLastError(), level=40)
else:
self.bus.log('Removed handler for console events.', level=40)
self.is_set = False
def handle(self, event):
"""Handle console control events (like Ctrl-C)."""
if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT,
win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT,
win32con.CTRL_CLOSE_EVENT):
self.bus.log('Console event %s: shutting down bus' % event)
# Remove self immediately so repeated Ctrl-C doesn't re-call it.
try:
self.stop()
except ValueError:
pass
self.bus.exit()
# 'First to return True stops the calls'
return 1
@ -68,15 +69,16 @@ class ConsoleCtrlHandler(plugins.SimplePlugin):
class Win32Bus(wspbus.Bus):
"""A Web Site Process Bus implementation for Win32.
Instead of time.sleep, this bus blocks using native win32event objects.
"""
def __init__(self):
self.events = {}
wspbus.Bus.__init__(self)
def _get_state_event(self, state):
"""Return a win32event for the given state (creating it if needed)."""
try:
@ -87,18 +89,19 @@ class Win32Bus(wspbus.Bus):
(state.name, os.getpid()))
self.events[state] = event
return event
def _get_state(self):
return self._state
def _set_state(self, value):
self._state = value
event = self._get_state_event(value)
win32event.PulseEvent(event)
state = property(_get_state, _set_state)
def wait(self, state, interval=0.1, channel=None):
"""Wait for the given state(s), KeyboardInterrupt or SystemExit.
Since this class uses native win32event objects, the interval
argument is ignored.
"""
@ -106,7 +109,8 @@ class Win32Bus(wspbus.Bus):
# Don't wait for an event that beat us to the punch ;)
if self.state not in state:
events = tuple([self._get_state_event(s) for s in state])
win32event.WaitForMultipleObjects(events, 0, win32event.INFINITE)
win32event.WaitForMultipleObjects(
events, 0, win32event.INFINITE)
else:
# Don't wait for an event that beat us to the punch ;)
if self.state != state:
@ -115,16 +119,17 @@ class Win32Bus(wspbus.Bus):
class _ControlCodes(dict):
"""Control codes used to "signal" a service via ControlService.
User-defined control codes are in the range 128-255. We generally use
the standard Python value for the Linux signal and add 128. Example:
>>> signal.SIGUSR1
10
control_codes['graceful'] = 128 + 10
"""
def key_for(self, obj):
"""For the given value, return its corresponding key."""
for key, val in self.items():
@ -145,27 +150,28 @@ def signal_child(service, command):
class PyWebService(win32serviceutil.ServiceFramework):
"""Python Web Service."""
_svc_name_ = "Python Web Service"
_svc_display_name_ = "Python Web Service"
_svc_deps_ = None # sequence of service names on which this depends
_exe_name_ = "pywebsvc"
_exe_args_ = None # Default to no arguments
# Only exists on Windows 2000 or later, ignored on windows NT
_svc_description_ = "Python Web Service"
def SvcDoRun(self):
from cherrypy import process
process.bus.start()
process.bus.block()
def SvcStop(self):
from cherrypy import process
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
process.bus.exit()
def SvcOther(self, control):
process.bus.publish(control_codes.key_for(control))

View File

@ -78,24 +78,27 @@ from cherrypy._cpcompat import set
# sys.executable is a relative-path, and/or cause other problems).
_startup_cwd = os.getcwd()
class ChannelFailures(Exception):
"""Exception raised when errors occur in a listener during Bus.publish()."""
"""Exception raised when errors occur in a listener during Bus.publish().
"""
delimiter = '\n'
def __init__(self, *args, **kwargs):
# Don't use 'super' here; Exceptions are old-style in Py2.4
# See http://www.cherrypy.org/ticket/959
# See https://bitbucket.org/cherrypy/cherrypy/issue/959
Exception.__init__(self, *args, **kwargs)
self._exceptions = list()
def handle_exception(self):
"""Append the current exception to self."""
self._exceptions.append(sys.exc_info()[1])
def get_instances(self):
"""Return a list of seen exception instances."""
return self._exceptions[:]
def __str__(self):
exception_strings = map(repr, self.get_instances())
return self.delimiter.join(exception_strings)
@ -107,12 +110,16 @@ class ChannelFailures(Exception):
__nonzero__ = __bool__
# Use a flag to indicate the state of the bus.
class _StateEnum(object):
class State(object):
name = None
def __repr__(self):
return "states.%s" % self.name
def __setattr__(self, key, value):
if isinstance(value, self.State):
value.name = key
@ -137,20 +144,21 @@ else:
class Bus(object):
"""Process state-machine and messenger for HTTP site deployment.
All listeners for a given channel are guaranteed to be called even
if others at the same channel fail. Each failure is logged, but
execution proceeds on to the next listener. The only way to stop all
processing from inside a listener is to raise SystemExit and stop the
whole server.
"""
states = states
state = states.STOPPED
execv = False
max_cloexec_files = max_files
def __init__(self):
self.execv = False
self.state = states.STOPPED
@ -158,32 +166,32 @@ class Bus(object):
[(channel, set()) for channel
in ('start', 'stop', 'exit', 'graceful', 'log', 'main')])
self._priorities = {}
def subscribe(self, channel, callback, priority=None):
"""Add the given callback at the given channel (if not present)."""
if channel not in self.listeners:
self.listeners[channel] = set()
self.listeners[channel].add(callback)
if priority is None:
priority = getattr(callback, 'priority', 50)
self._priorities[(channel, callback)] = priority
def unsubscribe(self, channel, callback):
"""Discard the given callback (if present)."""
listeners = self.listeners.get(channel)
if listeners and callback in listeners:
listeners.discard(callback)
del self._priorities[(channel, callback)]
def publish(self, channel, *args, **kwargs):
"""Return output of all subscribers for the given channel."""
if channel not in self.listeners:
return []
exc = ChannelFailures()
output = []
items = [(self._priorities[(channel, listener)], listener)
for listener in self.listeners[channel]]
try:
@ -214,7 +222,7 @@ class Bus(object):
if exc:
raise exc
return output
def _clean_exit(self):
"""An atexit handler which asserts the Bus is not running."""
if self.state != states.EXITING:
@ -224,11 +232,11 @@ class Bus(object):
"bus.block() after start(), or call bus.exit() before the "
"main thread exits." % self.state, RuntimeWarning)
self.exit()
def start(self):
"""Start all services."""
atexit.register(self._clean_exit)
self.state = states.STARTING
self.log('Bus STARTING')
try:
@ -248,13 +256,13 @@ class Bus(object):
pass
# Re-raise the original error
raise e_info
def exit(self):
"""Stop all services and prepare to exit the process."""
exitstate = self.state
try:
self.stop()
self.state = states.EXITING
self.log('Bus EXITING')
self.publish('exit')
@ -266,32 +274,32 @@ class Bus(object):
# signal handler, console handler, or atexit handler), so we
# can't just let exceptions propagate out unhandled.
# Assume it's been logged and just die.
os._exit(70) # EX_SOFTWARE
os._exit(70) # EX_SOFTWARE
if exitstate == states.STARTING:
# exit() was called before start() finished, possibly due to
# Ctrl-C because a start listener got stuck. In this case,
# we could get stuck in a loop where Ctrl-C never exits the
# process, so we just call os.exit here.
os._exit(70) # EX_SOFTWARE
os._exit(70) # EX_SOFTWARE
def restart(self):
"""Restart the process (may close connections).
This method does not restart the process from the calling thread;
instead, it stops the bus and asks the main thread to call execv.
"""
self.execv = True
self.exit()
def graceful(self):
"""Advise all services to reload."""
self.log('Bus graceful')
self.publish('graceful')
def block(self, interval=0.1):
"""Wait for the EXITING state, KeyboardInterrupt or SystemExit.
This function is intended to be called only by the main thread.
After waiting for the EXITING state, it also waits for all threads
to terminate, and then calls os.execv if self.execv is True. This
@ -309,15 +317,23 @@ class Bus(object):
self.log('SystemExit raised: shutting down bus')
self.exit()
raise
# Waiting for ALL child threads to finish is necessary on OS X.
# See http://www.cherrypy.org/ticket/581.
# See https://bitbucket.org/cherrypy/cherrypy/issue/581.
# It's also good to let them all shut down before allowing
# the main thread to call atexit handlers.
# See http://www.cherrypy.org/ticket/751.
# See https://bitbucket.org/cherrypy/cherrypy/issue/751.
self.log("Waiting for child threads to terminate...")
for t in threading.enumerate():
if t != threading.currentThread() and t.isAlive():
# Validate the we're not trying to join the MainThread
# that will cause a deadlock and the case exist when
# implemented as a windows service and in any other case
# that another thread executes cherrypy.engine.exit()
if (
t != threading.currentThread() and
t.isAlive() and
not isinstance(t, threading._MainThread)
):
# Note that any dummy (external) threads are always daemonic.
if hasattr(threading.Thread, "daemon"):
# Python 2.6+
@ -327,22 +343,22 @@ class Bus(object):
if not d:
self.log("Waiting for thread %s." % t.getName())
t.join()
if self.execv:
self._do_execv()
def wait(self, state, interval=0.1, channel=None):
"""Poll for the given state(s) at intervals; publish to channel."""
if isinstance(state, (tuple, list)):
states = state
else:
states = [state]
def _wait():
while self.state not in states:
time.sleep(interval)
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
@ -353,18 +369,18 @@ class Bus(object):
sys.modules['psyco'].cannotcompile(_wait)
except (KeyError, AttributeError):
pass
_wait()
def _do_execv(self):
"""Re-execute the current process.
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.
"""
args = sys.argv[:]
self.log('Re-spawning %s' % ' '.join(args))
if sys.platform[:4] == 'java':
from _systemrestart import SystemRestart
raise SystemRestart
@ -377,25 +393,25 @@ class Bus(object):
if self.max_cloexec_files:
self._set_cloexec()
os.execv(sys.executable, args)
def _set_cloexec(self):
"""Set the CLOEXEC flag on all open files (except stdin/out/err).
If self.max_cloexec_files is an integer (the default), then on
platforms which support it, it represents the max open files setting
for the operating system. This function will be called just before
the process is restarted via os.execv() to prevent open files
from persisting into the new process.
Set self.max_cloexec_files to 0 to disable this behavior.
"""
for fd in range(3, self.max_cloexec_files): # skip stdin/out/err
for fd in range(3, self.max_cloexec_files): # skip stdin/out/err
try:
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
except IOError:
continue
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
def stop(self):
"""Stop all services."""
self.state = states.STOPPING
@ -403,7 +419,7 @@ class Bus(object):
self.publish('stop')
self.state = states.STOPPED
self.log('Bus STOPPED')
def start_with_callback(self, func, args=None, kwargs=None):
"""Start 'func' in a new thread T, then start self (and return T)."""
if args is None:
@ -411,18 +427,18 @@ class Bus(object):
if kwargs is None:
kwargs = {}
args = (func,) + args
def _callback(func, *a, **kw):
self.wait(states.STARTED)
func(*a, **kw)
t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
t.setName('Bus Callback ' + t.getName())
t.start()
self.start()
return t
def log(self, msg="", level=20, traceback=False):
"""Log the given message. Append the last traceback if requested."""
if traceback:

View File

@ -0,0 +1,61 @@
"""<MyProject>, a CherryPy application.
Use this as a base for creating new CherryPy applications. When you want
to make a new app, copy and paste this folder to some other location
(maybe site-packages) and rename it to the name of your project,
then tweak as desired.
Even before any tweaking, this should serve a few demonstration pages.
Change to this directory and run:
../cherryd -c site.conf
"""
import cherrypy
from cherrypy import tools, url
import os
local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__))
class Root:
_cp_config = {'tools.log_tracebacks.on': True,
}
def index(self):
return """<html>
<body>Try some <a href='%s?a=7'>other</a> path,
or a <a href='%s?n=14'>default</a> path.<br />
Or, just look at the pretty picture:<br />
<img src='%s' />
</body></html>""" % (url("other"), url("else"),
url("files/made_with_cherrypy_small.png"))
index.exposed = True
def default(self, *args, **kwargs):
return "args: %s kwargs: %s" % (args, kwargs)
default.exposed = True
def other(self, a=2, b='bananas', c=None):
cherrypy.response.headers['Content-Type'] = 'text/plain'
if c is None:
return "Have %d %s." % (int(a), b)
else:
return "Have %d %s, %s." % (int(a), b, c)
other.exposed = True
files = cherrypy.tools.staticdir.handler(
section="/files",
dir=os.path.join(local_dir, "static"),
# Ignore .php files, etc.
match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$',
)
root = Root()
# Uncomment the following to use your own favicon instead of CP's default.
#favicon_path = os.path.join(local_dir, "favicon.ico")
#root.favicon_ico = tools.staticfile.handler(filename=favicon_path)

View File

@ -0,0 +1,22 @@
# Apache2 server conf file for using CherryPy with mod_fcgid.
# This doesn't have to be "C:/", but it has to be a directory somewhere, and
# MUST match the directory used in the FastCgiExternalServer directive, below.
DocumentRoot "C:/"
ServerName 127.0.0.1
Listen 80
LoadModule fastcgi_module modules/mod_fastcgi.dll
LoadModule rewrite_module modules/mod_rewrite.so
Options ExecCGI
SetHandler fastcgi-script
RewriteEngine On
# Send requests for any URI to our fastcgi handler.
RewriteRule ^(.*)$ /fastcgi.pyc [L]
# The FastCgiExternalServer directive defines filename as an external FastCGI application.
# 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
# filename will be handled by this external FastCGI application.
FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088

View File

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

View File

@ -0,0 +1,14 @@
[global]
# Uncomment this when you're done developing
#environment: "production"
server.socket_host: "0.0.0.0"
server.socket_port: 8088
# Uncomment the following lines to run on HTTPS at the same time
#server.2.socket_host: "0.0.0.0"
#server.2.socket_port: 8433
#server.2.ssl_certificate: '../test/test.pem'
#server.2.ssl_private_key: '../test/test.pem'
tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf")

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

View File

@ -0,0 +1,30 @@
"""Regression test suite for CherryPy.
Run 'nosetests -s test/' to exercise all tests.
The '-s' flag instructs nose to output stdout messages, wihch is crucial to
the 'interactive' mode of webtest.py. If you run these tests without the '-s'
flag, don't be surprised if the test seems to hang: it's waiting for your
interactive input.
"""
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"""
from cherrypy import expose, tools
from cherrypy._cpcompat import ntob
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
@tools.response_headers(headers=[('Content-Type', 'application/data')])
def blah(self):
yield ntob("blah")
# This is here to demonstrate that _cp_config = {...} overwrites
# the _cp_config attribute added by the Tool decorator. You have
# to write _cp_config[k] = v or _cp_config.update(...) instead.
blah._cp_config['response.stream'] = True

View File

@ -0,0 +1,68 @@
import os
import sys
import time
starttime = time.time()
import cherrypy
class Root:
def index(self):
return "Hello World"
index.exposed = True
def mtimes(self):
return repr(cherrypy.engine.publish("Autoreloader", "mtimes"))
mtimes.exposed = True
def pid(self):
return str(os.getpid())
pid.exposed = True
def start(self):
return repr(starttime)
start.exposed = True
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()
exit.exposed = True
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', unsub_sig, priority=100)
def starterror():
if cherrypy.config.get('starterror', False):
zerodiv = 1 / 0
cherrypy.engine.subscribe('start', starterror, 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.engine.subscribe('start', log_test_case_name, priority=6)
cherrypy.tree.mount(Root(), '/', {'/': {}})

View File

@ -0,0 +1,422 @@
"""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
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
import re
import sys
import time
import traceback
import cherrypy
from cherrypy._cpcompat import ntob
from cherrypy import _cperror, _cpmodpy
from cherrypy.lib import httputil
AB_PATH = ""
APACHE_PATH = "apache"
SCRIPT_NAME = "/cpbench/users/rdelon/apps/blog"
__all__ = ['ABSession', 'Root', 'print_report',
'run_standard_benchmarks', 'safe_threads',
'size_report', 'startup', 'thread_report',
]
size_cache = {}
class Root:
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>"""
index.exposed = True
def hello(self):
return "Hello, world\r\n"
hello.exposed = True
def sizer(self, size):
resp = size_cache.get(size, None)
if resp is None:
size_cache[size] = resp = "X" * int(size)
return resp
sizer.exposed = True
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,
'engine.deadlock_poll_freq': 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,
},
}
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',
ntob(r'^Complete requests:\s*(\d+)')),
('failed_requests', 'Failed',
ntob(r'^Failed requests:\s*(\d+)')),
('requests_per_second', 'req/sec',
ntob(r'^Requests per second:\s*([0-9.]+)')),
('time_per_request_concurrent', 'msec/req',
ntob(r'^Time per request:\s*([0-9.]+).*concurrent requests\)$')),
('transfer_rate', 'KB/sec',
ntob(r'^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:
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 i, val in enumerate(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__':
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:
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,48 @@
"""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,545 @@
"""A library of helper functions for the CherryPy test suite."""
import datetime
import logging
log = logging.getLogger(__name__)
import os
thisdir = os.path.abspath(os.path.dirname(__file__))
serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
import unittest
import re
import sys
import time
import warnings
import cherrypy
from cherrypy._cpcompat import basestring, copyitems, HTTPSConnection, ntob
from cherrypy.lib import httputil
from cherrypy.lib import gctools
from cherrypy.lib.reprconf import unrepr
from cherrypy.test import webtest
# Use subprocess module from Python 2.7 on Python 2.3-2.6
if sys.version_info < (2, 7):
import cherrypy._cpcompat_subprocess as subprocess
else:
import subprocess
import nose
_testconfig = None
def get_tst_config(overconf={}):
global _testconfig
if _testconfig is None:
conf = {
'scheme': 'http',
'protocol': "HTTP/1.1",
'port': 54583,
'host': '127.0.0.1',
'validate': False,
'conquer': False,
'server': 'wsgi',
}
try:
import testconfig
_conf = testconfig.config.get('supervisor', None)
if _conf is not None:
for k, v in _conf.items():
if isinstance(v, basestring):
_conf[k] = unrepr(v)
conf.update(_conf)
except ImportError:
pass
_testconfig = conf
conf = _testconfig.copy()
conf.update(overconf)
return conf
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)
log_to_stderr = lambda msg, level: 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()
#engine.subscribe('log', log_to_stderr)
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()
for name, server in copyitems(getattr(cherrypy, 'servers', {})):
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.conquer:
try:
import wsgiconq
except ImportError:
warnings.warn(
"Error importing wsgiconq. pyconquer will not run.")
else:
app = wsgiconq.WSGILogger(app, c_calls=True)
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"
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, basestring):
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
_setup_server = classmethod(_setup_server)
def setup_class(cls):
''
# Creates a server
conf = get_tst_config()
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
setup_class = classmethod(setup_class)
def teardown_class(cls):
''
if hasattr(cls, 'setup_server'):
cls.supervisor.stop()
teardown_class = classmethod(teardown_class)
do_gc_test = False
def test_gc(self):
if self.do_gc_test:
self.getPage("/gc/stats")
self.assertBody("Statistics:")
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):
"""Open the url. Return status, headers, body."""
if self.script_name:
url = httputil.urljoin(self.script_name, url)
return webtest.WebCase.getPage(self, url, headers, method, body,
protocol)
def skip(self, msg='skipped '):
raise nose.SkipTest(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">') + ntob('(.*)') + 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,
}
f = open(self.config_file, 'wb')
f.write(ntob(conf, 'utf-8'))
f.close()
def start(self, imports=None):
"""Start cherryd in a subprocess."""
cherrypy._cpserver.wait_for_free_port(self.host, self.port)
args = [
os.path.join(thisdir, '..', 'cherryd'),
'-c', self.config_file,
'-p', self.pid_file,
]
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:
cherrypy._cpserver.wait_for_occupied_port(self.host, self.port)
# 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,199 @@
"""logtest, a unittest.TestCase helper for testing log output."""
import sys
import time
import cherrypy
from cherrypy._cpcompat import basestring, ntob, unicodestr
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 = ntob("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, unicodestr):
marker = marker.encode('utf-8')
data = []
in_region = False
for line in open(logfile, 'rb'):
if in_region:
if (line.startswith(self.markerPrefix) and not marker 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 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, unicodestr):
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, basestring):
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, unicodestr):
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,137 @@
"""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
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
import re
import sys
import time
import cherrypy
from cherrypy.process import plugins, servers
from cherrypy.test import helper
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,125 @@
"""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
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
import re
import sys
import time
import cherrypy
from cherrypy._cpcompat import ntob
from cherrypy.process import plugins, servers
from cherrypy.test import helper
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()

165
libs/cherrypy/test/modpy.py Normal file
View File

@ -0,0 +1,165 @@
"""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
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
import re
import time
from cherrypy.test import helper
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()
import cherrypy
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()
import cherrypy
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,150 @@
"""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
curdir = os.path.abspath(os.path.dirname(__file__))
import re
import sys
import time
import cherrypy
from cherrypy.test import helper, webtest
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
"""
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.
cherrypy._cpserver.wait_for_occupied_port("127.0.0.1", self.port)
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):
import cherrypy
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,158 @@
#!/usr/bin/python
"""A session demonstration app."""
import calendar
from datetime import datetime
import sys
import cherrypy
from cherrypy.lib import sessions
from cherrypy._cpcompat import copyitems
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>
"""
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': copyitems(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,
}
def index(self):
# Must modify data or the session will not be saved.
cherrypy.session['color'] = 'green'
return self.page()
index.exposed = True
def expire(self):
sessions.expire()
return self.page()
expire.exposed = True
def regen(self):
cherrypy.session.regenerate()
# Must modify data or the session will not be saved.
cherrypy.session['color'] = 'yellow'
return self.page()
regen.exposed = True
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: 18 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,93 @@
# This file is part of CherryPy <http://www.cherrypy.org/>
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
import cherrypy
from cherrypy._cpcompat import md5, ntob
from cherrypy.lib import auth_basic
from cherrypy.test import helper
class BasicAuthTest(helper.CPWebCase):
def setup_server():
class Root:
def index(self):
return "This is public."
index.exposed = True
class BasicProtected:
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
index.exposed = True
class BasicProtected2:
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
index.exposed = True
userpassdict = {'xuser': 'xpassword'}
userhashdict = {'xuser': md5(ntob('xpassword')).hexdigest()}
def checkpasshash(realm, user, password):
p = userhashdict.get(user)
return p and p == md5(ntob(password)).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
},
}
root = Root()
root.basic = BasicProtected()
root.basic2 = BasicProtected2()
cherrypy.tree.mount(root, config=conf)
setup_server = staticmethod(setup_server)
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"')
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.")

View File

@ -0,0 +1,139 @@
# This file is part of CherryPy <http://www.cherrypy.org/>
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
import cherrypy
from cherrypy.lib import auth_digest
from cherrypy.test import helper
class DigestAuthTest(helper.CPWebCase):
def setup_server():
class Root:
def index(self):
return "This is public."
index.exposed = True
class DigestProtected:
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
index.exposed = True
def fetch_users():
return {'test': 'test'}
get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(fetch_users())
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'}}
root = Root()
root.digest = DigestProtected()
cherrypy.tree.mount(root, config=conf)
setup_server = staticmethod(setup_server)
def testPublic(self):
self.getPage("/")
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
self.assertBody('This is public.')
def testDigest(self):
self.getPage("/digest/")
self.assertStatus(401)
value = None
for k, v in self.headers:
if k.lower() == "www-authenticate":
if v.startswith("Digest"):
value = v
break
if value is None:
self._handlewebError(
"Digest authentification scheme was not found")
value = value[7:]
items = value.split(', ')
tokens = {}
for item in items:
key, value = item.split('=')
tokens[key.lower()] = value
missing_msg = "%s is missing"
bad_value_msg = "'%s' was expecting '%s' but found '%s'"
nonce = None
if 'realm' not in tokens:
self._handlewebError(missing_msg % 'realm')
elif tokens['realm'] != '"localhost"':
self._handlewebError(bad_value_msg %
('realm', '"localhost"', tokens['realm']))
if 'nonce' not in tokens:
self._handlewebError(missing_msg % 'nonce')
else:
nonce = tokens['nonce'].strip('"')
if 'algorithm' not in tokens:
self._handlewebError(missing_msg % 'algorithm')
elif tokens['algorithm'] != '"MD5"':
self._handlewebError(bad_value_msg %
('algorithm', '"MD5"', tokens['algorithm']))
if 'qop' not in tokens:
self._handlewebError(missing_msg % 'qop')
elif tokens['qop'] != '"auth"':
self._handlewebError(bad_value_msg %
('qop', '"auth"', tokens['qop']))
get_ha1 = auth_digest.get_ha1_dict_plain({'test': 'test'})
# Test user agent response with a wrong value for 'realm'
base_auth = ('Digest username="test", '
'realm="wrong realm", '
'nonce="%s", '
'uri="/digest/", '
'algorithm=MD5, '
'response="%s", '
'qop=auth, '
'nc=%s, '
'cnonce="1522e61005789929"')
auth_header = base_auth % (
nonce, '11111111111111111111111111111111', '00000001')
auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
# calculate the response digest
ha1 = get_ha1(auth.realm, 'test')
response = auth.request_digest(ha1)
# send response with correct response digest, but wrong realm
auth_header = base_auth % (nonce, response, '00000001')
self.getPage('/digest/', [('Authorization', auth_header)])
self.assertStatus(401)
# Test that must pass
base_auth = ('Digest username="test", '
'realm="localhost", '
'nonce="%s", '
'uri="/digest/", '
'algorithm=MD5, '
'response="%s", '
'qop=auth, '
'nc=%s, '
'cnonce="1522e61005789929"')
auth_header = base_auth % (
nonce, '11111111111111111111111111111111', '00000001')
auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
# calculate the response digest
ha1 = get_ha1('localhost', 'test')
response = auth.request_digest(ha1)
# send response with correct response digest
auth_header = base_auth % (nonce, response, '00000001')
self.getPage('/digest/', [('Authorization', auth_header)])
self.assertStatus('200 OK')
self.assertBody("Hello test, you've been authorized.")

View File

@ -0,0 +1,276 @@
import threading
import time
import unittest
import cherrypy
from cherrypy._cpcompat import get_daemon, set
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 get_daemon(t)]
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 get_daemon(t)]
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,342 @@
import datetime
import gzip
from itertools import count
import os
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
import sys
import threading
import time
import urllib
import cherrypy
from cherrypy._cpcompat import next, ntob, quote, xrange
from cherrypy.lib import httputil
gif_bytes = ntob(
'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00'
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
'\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;'
)
from cherrypy.test import helper
class CacheTest(helper.CPWebCase):
def setup_server():
class Root:
_cp_config = {'tools.caching.on': True}
def __init__(self):
self.counter = 0
self.control_counter = 0
self.longlock = threading.Lock()
def index(self):
self.counter += 1
msg = "visit #%s" % self.counter
return msg
index.exposed = True
def control(self):
self.control_counter += 1
return "visit #%s" % self.control_counter
control.exposed = True
def a_gif(self):
cherrypy.response.headers[
'Last-Modified'] = httputil.HTTPDate()
return gif_bytes
a_gif.exposed = True
def long_process(self, seconds='1'):
try:
self.longlock.acquire()
time.sleep(float(seconds))
finally:
self.longlock.release()
return 'success!'
long_process.exposed = True
def clear_cache(self, path):
cherrypy._cache.store[cherrypy.request.base + path].clear()
clear_cache.exposed = True
class VaryHeaderCachingServer(object):
_cp_config = {
'tools.caching.on': True,
'tools.response_headers.on': True,
'tools.response_headers.headers': [
('Vary', 'Our-Varying-Header')
],
}
def __init__(self):
self.counter = count(1)
def index(self):
return "visit #%s" % next(self.counter)
index.exposed = True
class UnCached(object):
_cp_config = {'tools.expires.on': True,
'tools.expires.secs': 60,
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static',
'tools.staticdir.root': curdir,
}
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"
force.exposed = True
force._cp_config = {'tools.expires.secs': 0}
def dynamic(self):
cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
cherrypy.response.headers['Cache-Control'] = 'private'
return "D-d-d-dynamic!"
dynamic.exposed = True
def cacheable(self):
cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
return "Hi, I'm cacheable."
cacheable.exposed = True
def specific(self):
cherrypy.response.headers[
'Etag'] = 'need_this_to_make_me_cacheable'
return "I am being specific"
specific.exposed = True
specific._cp_config = {'tools.expires.secs': 86400}
class Foo(object):
pass
def wrongtype(self):
cherrypy.response.headers[
'Etag'] = 'need_this_to_make_me_cacheable'
return "Woops"
wrongtype.exposed = True
wrongtype._cp_config = {'tools.expires.secs': Foo()}
cherrypy.tree.mount(Root())
cherrypy.tree.mount(UnCached(), "/expires")
cherrypy.tree.mount(VaryHeaderCachingServer(), "/varying_headers")
cherrypy.config.update({'tools.gzip.on': True})
setup_server = staticmethod(setup_server)
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), ntob("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), ntob("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 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")
def test_antistampede(self):
SECONDS = 4
# 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("/long_process?seconds=%d" % SECONDS)
self.assertBody('success!')
self.getPage("/clear_cache?path=" +
quote('/long_process?seconds=%d' % SECONDS, safe=''))
self.assertStatus(200)
start = datetime.datetime.now()
def run():
self.getPage("/long_process?seconds=%d" % SECONDS)
# The response should be the same every time
self.assertBody('success!')
ts = [threading.Thread(target=run) for i in xrange(100)]
for t in ts:
t.start()
for t in ts:
t.join()
self.assertEqualDates(start, datetime.datetime.now(),
# Allow a second (two, for slow hosts)
# for our thread/TCP overhead etc.
seconds=SECONDS + 2)
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,19 @@
import unittest
import nose
from cherrypy import _cpcompat as compat
class StringTester(unittest.TestCase):
def test_ntob_non_native(self):
"""
ntob should raise an Exception on unicode.
(Python 2 only)
See #1132 for discussion.
"""
if compat.py3k:
raise nose.SkipTest("Only useful on Python 2")
self.assertRaises(Exception, compat.ntob, unicode('fight'))

View File

@ -0,0 +1,271 @@
"""Tests for the CherryPy configuration system."""
import os
import sys
import unittest
import cherrypy
import cherrypy._cpcompat as compat
localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
def setup_server():
class Root:
_cp_config = {'foo': 'this',
'bar': 'that'}
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")
index = cherrypy.expose(index, alias=('global_', 'xyz'))
def repr(self, key):
return repr(cherrypy.request.config.get(key, None))
repr.exposed = True
def dbscheme(self):
return self.db
dbscheme.exposed = True
def plain(self, x):
return x
plain.exposed = True
plain._cp_config = {'request.body.attempt_charsets': ['utf-16']}
favicon_ico = cherrypy.tools.staticfile.handler(
filename=os.path.join(localDir, '../favicon.ico'))
class Foo:
_cp_config = {'foo': 'this2',
'baz': 'that2'}
def index(self, key):
return cherrypy.request.config.get(key, "None")
index.exposed = True
nex = index
def silly(self):
return 'Hello world'
silly.exposed = True
silly._cp_config = {'response.headers.X-silly': 'sillyval'}
# Test the expose and config decorators
#@cherrypy.expose
#@cherrypy.config(foo='this3', **{'bax': 'this4'})
def bar(self, key):
return repr(cherrypy.request.config.get(key, None))
bar.exposed = True
bar._cp_config = {'foo': 'this3', 'bax': 'this4'}
class Another:
def index(self, key):
return str(cherrypy.request.config.get(key, "None"))
index.exposed = True
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
class Raw:
_cp_config = {'raw.output': repr}
def incr(self, num):
return num + 1
incr.exposed = True
incr._cp_config = {'raw.input.map': {'num': int}}
if not compat.py3k:
thing3 = "thing3: unicode('test', errors='ignore')"
else:
thing3 = ''
ioconf = compat.StringIO("""
[/]
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 #
from cherrypy.test import helper
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 compat.py3k:
self.getPage("/repr?key=thing3")
self.assertBody(repr(unicode('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=compat.ntob('\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 = compat.StringIO(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")

View File

@ -0,0 +1,128 @@
"""Tests for the CherryPy configuration system."""
import os
import sys
localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
import socket
import time
import cherrypy
# Client-side code #
from cherrypy.test import helper
class ServerConfigTests(helper.CPWebCase):
def setup_server():
class Root:
def index(self):
return cherrypy.request.wsgi_environ['SERVER_PORT']
index.exposed = True
def upload(self, file):
return "Size: %s" % len(file.file.read())
upload.exposed = True
def tinyupload(self):
return cherrypy.request.body.read()
tinyupload.exposed = True
tinyupload._cp_config = {'request.body.maxbytes': 100}
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,
})
setup_server = staticmethod(setup_server)
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://bitbucket.org/cherrypy/cherrypy/issue/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,842 @@
"""Tests for TCP connection handling, including proper and timely close."""
import socket
import sys
import time
timeout = 1
import cherrypy
from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, NotConnected
from cherrypy._cpcompat import BadStatusLine, ntob, tonative, urlopen, unicodestr
from cherrypy.test import webtest
from cherrypy import _cperror
pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
def setup_server():
def raise500():
raise cherrypy.HTTPError(500)
class Root:
def index(self):
return pov
index.exposed = True
page1 = index
page2 = index
page3 = index
def hello(self):
return "Hello, world!"
hello.exposed = True
def timeout(self, t):
return str(cherrypy.server.httpserver.timeout)
timeout.exposed = 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()
stream.exposed = True
stream._cp_config = {'response.stream': True}
def error(self, code=500):
raise cherrypy.HTTPError(code)
error.exposed = True
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()
upload.exposed = True
def custom(self, response_code):
cherrypy.response.status = response_code
return "Code = %s" % response_code
custom.exposed = True
def err_before_read(self):
return "ok"
err_before_read.exposed = True
err_before_read._cp_config = {'hooks.on_start_resource': raise500}
def one_megabyte_of_a(self):
return ["a" * 1024] * 1024
one_megabyte_of_a.exposed = True
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, unicodestr):
chunk = chunk.encode('ISO-8859-1')
newbody.append(chunk)
return newbody
custom_cl.exposed = True
# Turn off the encoding tool so it doens't collapse
# our response body and reclaculate the Content-Length.
custom_cl._cp_config = {'tools.encode.on': False}
cherrypy.tree.mount(Root())
cherrypy.config.update({
'server.max_request_body_size': 1001,
'server.socket_timeout': timeout,
})
from cherrypy.test import helper
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://bitbucket.org/cherrypy/cherrypy/issue/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(ntob('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(ntob('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(ntob('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:
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(ntob('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:
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(ntob('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")
response.begin()
body = response.read(13)
self.assertEqual(response.status, 200)
self.assertEqual(body, ntob("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, ntob("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 = ntob("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(ntob('POST /upload HTTP/1.1'))
conn._output(ntob("Host: %s" % self.HOST, 'ascii'))
conn._output(ntob("Content-Type: text/plain"))
conn._output(ntob("Content-Length: 17"))
conn._output(ntob("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 = ntob("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'" % ntob('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):
remote_data_conn = urlopen('%s://%s:%s/one_megabyte_of_a/' %
(self.scheme, self.HOST, self.PORT,))
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:
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())
upload.exposed = True
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,
})
import errno
socket_reset_errors = []
# Not all of these names will be defined for every platform.
for _ in ("ECONNRESET", "WSAECONNRESET"):
if _ in dir(errno):
socket_reset_errors.append(getattr(errno, _))
class LimitedRequestQueueTests(helper.CPWebCase):
setup_server = staticmethod(setup_upload_server)
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.
import time
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:
raise AssertionError("Overflow conn did not get RST. "
"Got %s instead" % repr(exc.args))
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(ntob("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(ntob('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(ntob('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,730 @@
"""Basic tests for the CherryPy core: request handling."""
import os
localDir = os.path.dirname(__file__)
import sys
import types
import cherrypy
from cherrypy._cpcompat import IncompleteRead, itervalues, ntob
from cherrypy import _cptools, tools
from cherrypy.lib import httputil, static
favicon_path = os.path.join(os.getcwd(), localDir, "../favicon.ico")
# Client-side code #
from cherrypy.test import helper
class CoreRequestHandlingTest(helper.CPWebCase):
def setup_server():
class Root:
def index(self):
return "hello"
index.exposed = True
favicon_ico = tools.staticfile.handler(filename=favicon_path)
def defct(self, newct):
newct = "text/%s" % newct
cherrypy.config.update({'tools.response_headers.on': True,
'tools.response_headers.headers':
[('Content-Type', newct)]})
defct.exposed = True
def baseurl(self, path_info, relative=None):
return cherrypy.url(path_info, relative=bool(relative))
baseurl.exposed = True
root = Root()
if sys.version_info >= (2, 5):
from cherrypy.test._test_decorators import ExposeExamples
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 itervalues(dct):
if isinstance(value, types.FunctionType):
value.exposed = True
setattr(root, name.lower(), cls())
Test = TestType('Test', (object, ), {})
class URL(Test):
_cp_config = {'tools.trailing_slash.on': False}
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 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 = []
def on_end_resource_stage(self):
return repr(self.statuses)
on_end_resource_stage._cp_config = {'tools.log_status.on': True}
class Redirect(Test):
class Error:
_cp_config = {"tools.err_redirect.on": True,
"tools.err_redirect.url": "/errpage",
"tools.err_redirect.internal": False,
}
def index(self):
raise NameError("redirect_test")
index.exposed = True
error = Error()
def index(self):
return "child"
def custom(self, url, code):
raise cherrypy.HTTPRedirect(url, code)
def by_code(self, code):
raise cherrypy.HTTPRedirect("somewhere%20else", code)
by_code._cp_config = {'tools.trailing_slash.extra': True}
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 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("/")
def choke(self):
return 3 / 0
choke.exposed = True
choke._cp_config = {'hooks.before_error_response': redir_custom}
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."
def early_ir(self, arg):
return "whatever"
early_ir._cp_config = {'hooks.before_request_body': redir_custom}
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 ntob("content")
def as_dblyield(self):
yield self.as_yield()
as_dblyield._cp_config = {'tools.flatten.on': True}
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):
for name in names:
cookie = cherrypy.request.cookie[name]
# Python2's SimpleCookie.__setitem__ won't take unicode
# keys.
cherrypy.response.cookie[str(name)] = cookie.value
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=[
(ntob('WWW-Authenticate'), ntob('Negotiate')),
(ntob('WWW-Authenticate'), ntob('Basic realm="foo"')),
])(header_list)
def commas(self):
cherrypy.response.headers[
'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"'
cherrypy.tree.mount(root)
setup_server = staticmethod(setup_server)
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://bitbucket.org/cherrypy/cherrypy/issue/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 as e:
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_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 testCookies(self):
if sys.version_info >= (2, 5):
header_value = lambda x: x
else:
header_value = lambda x: x + ';'
self.getPage("/cookies/single?name=First",
[('Cookie', 'First=Dinsdale;')])
self.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
self.getPage("/cookies/multiple?names=First&names=Last",
[('Cookie', 'First=Dinsdale; Last=Piranha;'),
])
self.assertHeader('Set-Cookie', header_value('First=Dinsdale'))
self.assertHeader('Set-Cookie', header_value('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())
# 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())
# 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):
if not sys.version_info >= (2, 5):
return self.skip("skipped (Python 2.5+ only) ")
# 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):
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:
def index(self):
return "hello"
index.exposed = True
def start_response_error(self):
return "salud!"
start_response_error._cp_config = {'tools.break_header.on': True}
root = Root()
cherrypy.tree.mount(root)
setup_server = staticmethod(setup_server)
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.")

View File

@ -0,0 +1,423 @@
import cherrypy
from cherrypy._cpcompat import sorted, unicodestr
from cherrypy._cptree import Application
from cherrypy.test import helper
script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"]
def setup_server():
class SubSubRoot:
def index(self):
return "SubSubRoot index"
index.exposed = True
def default(self, *args):
return "SubSubRoot default"
default.exposed = True
def handler(self):
return "SubSubRoot handler"
handler.exposed = True
def dispatch(self):
return "SubSubRoot dispatch"
dispatch.exposed = True
subsubnodes = {
'1': SubSubRoot(),
'2': SubSubRoot(),
}
class SubRoot:
def index(self):
return "SubRoot index"
index.exposed = True
def default(self, *args):
return "SubRoot %s" % (args,)
default.exposed = True
def handler(self):
return "SubRoot handler"
handler.exposed = True
def _cp_dispatch(self, vpath):
return subsubnodes.get(vpath[0], None)
subnodes = {
'1': SubRoot(),
'2': SubRoot(),
}
class Root:
def index(self):
return "index"
index.exposed = True
def default(self, *args):
return "default %s" % (args,)
default.exposed = True
def handler(self):
return "handler"
handler.exposed = True
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 unicode(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
class UserContainerNode(object):
exposed = True
def POST(self, name):
"""
Allow the creation of a new Object
"""
return "POST %d" % make_user(name)
def GET(self):
return unicodestr(sorted(user_lookup.keys()))
def dynamic_dispatch(self, vpath):
try:
id = int(vpath[0])
except (ValueError, IndexError):
return None
return UserInstanceNode(id)
class UserInstanceNode(object):
exposed = True
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 unicodestr(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:
def index(self, a, b):
return "custom"
index.exposed = True
def _cp_dispatch(self, vpath):
"""Make sure that if we don't pop anything from vpath,
processing still works.
"""
return self.CustomDispatch()
def index(self, a, b=None):
body = ['a:' + str(a)]
if b is not None:
body.append(',b:' + str(b))
return ''.join(body)
index.exposed = True
def delete(self, a, b):
return 'deleting ' + str(a) + ' and ' + str(b)
delete.exposed = True
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
def index(self):
return "IndexOnly index"
index.exposed = True
class DecoratedPopArgs:
"""Test _cp_dispatch with @cherrypy.popargs."""
def index(self):
return "no params"
index.exposed = True
def hi(self):
return "hi was not interpreted as 'a' param"
hi.exposed = True
DecoratedPopArgs = cherrypy.popargs(
'a', 'b', handler=ABHandler())(DecoratedPopArgs)
class NonDecoratedPopArgs:
"""Test _cp_dispatch = cherrypy.popargs()"""
_cp_dispatch = cherrypy.popargs('a')
def index(self, a):
return "index: " + str(a)
index.exposed = True
class ParameterizedHandler:
"""Special handler created for each request"""
def __init__(self, a):
self.a = a
def index(self):
if 'a' in cherrypy.request.params:
raise Exception(
"Parameterized handler argument ended up in "
"request.params")
return self.a
index.exposed = True
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:
prefix = 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,395 @@
import gzip
import cherrypy
from cherrypy._cpcompat import BytesIO, IncompleteRead, ntob, ntou
europoundUnicode = ntou('\x80\xa3')
sing = ntou("\u6bdb\u6cfd\u4e1c: Sing, Little Birdie?", 'escape')
sing8 = sing.encode('utf-8')
sing16 = sing.encode('utf-16')
from cherrypy.test import helper
class EncodingTests(helper.CPWebCase):
def setup_server():
class Root:
def index(self, param):
assert param == europoundUnicode, "%r != %r" % (
param, europoundUnicode)
yield europoundUnicode
index.exposed = True
def mao_zedong(self):
return sing
mao_zedong.exposed = True
def utf8(self):
return sing8
utf8.exposed = True
utf8._cp_config = {'tools.encode.encoding': 'utf-8'}
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'
return 'Any content'
cookies_and_headers.exposed = True
def reqparams(self, *args, **kwargs):
return ntob(', ').join(
[": ".join((k, v)).encode('utf8')
for k, v in sorted(cherrypy.request.params.items())]
)
reqparams.exposed = True
def nontext(self, *args, **kwargs):
cherrypy.response.headers[
'Content-Type'] = 'application/binary'
return '\x00\x01\x02\x03'
nontext.exposed = True
nontext._cp_config = {'tools.encode.text_only': False,
'tools.encode.add_charset': True,
}
class GZIP:
def index(self):
yield "Hello, world"
index.exposed = True
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"
noshow.exposed = True
# Turn encoding off so the gzip tool is the one doing the collapse.
noshow._cp_config = {'tools.encode.on': False}
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"
noshow_stream.exposed = True
noshow_stream._cp_config = {'response.stream': True}
class Decode:
def extra_charset(self, *args, **kwargs):
return ', '.join([": ".join((k, v))
for k, v in cherrypy.request.params.items()])
extra_charset.exposed = True
extra_charset._cp_config = {
'tools.decode.on': True,
'tools.decode.default_encoding': ['utf-16'],
}
def force_charset(self, *args, **kwargs):
return ', '.join([": ".join((k, v))
for k, v in cherrypy.request.params.items()])
force_charset.exposed = True
force_charset._cp_config = {
'tools.decode.on': True,
'tools.decode.encoding': 'utf-16',
}
root = Root()
root.gzip = GZIP()
root.decode = Decode()
cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}})
setup_server = staticmethod(setup_server)
def test_query_string_decoding(self):
europoundUtf8 = europoundUnicode.encode('utf-8')
self.getPage(ntob('/?param=') + europoundUtf8)
self.assertBody(europoundUtf8)
# Encoded utf8 query strings MUST be parsed correctly.
# Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX
self.getPage("/reqparams?q=%C2%A3")
# The return value will be encoded as utf8.
self.assertBody(ntob("q: \xc2\xa3"))
# Query strings that are incorrectly encoded MUST raise 404.
# Here, q is the POUND SIGN U+00A3 encoded in latin1 and then %HEX
self.getPage("/reqparams?q=%A3")
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 = ntob("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 = ntob("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(ntob("q: \xc2\xa3"))
# ...and in utf16, which is not in the default attempt_charsets list:
body = ntob("\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(ntob("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 = ntob("\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 = ntob("\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(ntob("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 = ntob("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(ntob("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 = ntob("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(ntob("submit: Create, text: ab\xe2\x80\x9cc"))
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(ntob("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")
def testGzip(self):
zbuf = BytesIO()
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
zfile.write(ntob("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")
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')

View File

@ -0,0 +1,84 @@
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy.test import helper
class ETagTest(helper.CPWebCase):
def setup_server():
class Root:
def resource(self):
return "Oh wah ta goo Siam."
resource.exposed = True
def fail(self, code):
code = int(code)
if 300 <= code <= 399:
raise cherrypy.HTTPRedirect([], code)
else:
raise cherrypy.HTTPError(code)
fail.exposed = True
def unicoded(self):
return ntou('I am a \u1ee4nicode string.', 'escape')
unicoded.exposed = True
# In Python 3, tools.encode is on by default
unicoded._cp_config = {'tools.encode.on': True}
conf = {'/': {'tools.etags.on': True,
'tools.etags.autotags': True,
}}
cherrypy.tree.mount(Root(), config=conf)
setup_server = staticmethod(setup_server)
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,245 @@
"""Tests for managing HTTP issues (malformed requests, etc)."""
import errno
import mimetypes
import socket
import sys
import cherrypy
from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob, py3k
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.
"""
BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$'
L = []
for key, filename, value in files:
L.append('--' + BOUNDARY)
L.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
(key, filename))
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
from cherrypy.test import helper
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))
def setup_server():
class Root:
def index(self, *args, **kwargs):
return "Hello world!"
index.exposed = True
def no_body(self, *args, **kwargs):
return "Hello world!"
no_body.exposed = True
no_body._cp_config = {'request.process_request_body': False}
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 py3k:
curchar = chr(curchar)
summary.append("%s * %d" % (curchar, count))
count = 1
curchar = c
if count:
if py3k:
curchar = chr(curchar)
summary.append("%s * %d" % (curchar, count))
return ", ".join(summary)
post_multipart.exposed = True
@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})
setup_server = staticmethod(setup_server)
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(ntob('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))
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)
self.assertBody(", ".join(["%s * 65536" % c for c in alphabet]))
def test_post_filename_with_commas(self):
'''Testing that we can handle filenames with commas. This was
reported as a bug in:
https://bitbucket.org/cherrypy/cherrypy/issue/1146/'''
# We'll upload a bunch of files with differing names.
for fname in ['boop.csv', 'foo, bar.csv', 'bar, xxxx.csv', 'file"name.csv']:
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(ntob('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), ntob("Malformed Request-Line"))
c.close()
def test_request_line_split_issue_1220(self):
Request_URI = "/index?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"
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://bitbucket.org/cherrypy/cherrypy/issue/941
c._output(ntob('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(ntob('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),
ntob("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,193 @@
import cherrypy
from cherrypy._cpcompat import md5, sha, ntob
from cherrypy.lib import httpauth
from cherrypy.test import helper
class HTTPAuthTest(helper.CPWebCase):
def setup_server():
class Root:
def index(self):
return "This is public."
index.exposed = True
class DigestProtected:
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
index.exposed = True
class BasicProtected:
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
index.exposed = True
class BasicProtected2:
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
index.exposed = True
def fetch_users():
return {'test': 'test'}
def sha_password_encrypter(password):
return sha(ntob(password)).hexdigest()
def fetch_password(username):
return sha(ntob('test')).hexdigest()
conf = {
'/digest': {
'tools.digest_auth.on': True,
'tools.digest_auth.realm': 'localhost',
'tools.digest_auth.users': fetch_users
},
'/basic': {
'tools.basic_auth.on': True,
'tools.basic_auth.realm': 'localhost',
'tools.basic_auth.users': {
'test': md5(ntob('test')).hexdigest()
}
},
'/basic2': {
'tools.basic_auth.on': True,
'tools.basic_auth.realm': 'localhost',
'tools.basic_auth.users': fetch_password,
'tools.basic_auth.encrypt': sha_password_encrypter
}
}
root = Root()
root.digest = DigestProtected()
root.basic = BasicProtected()
root.basic2 = BasicProtected2()
cherrypy.tree.mount(root, config=conf)
setup_server = staticmethod(setup_server)
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="localhost"')
self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZX60')])
self.assertStatus(401)
self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZXN0')])
self.assertStatus('200 OK')
self.assertBody("Hello test, you've been authorized.")
def testBasic2(self):
self.getPage("/basic2/")
self.assertStatus(401)
self.assertHeader('WWW-Authenticate', 'Basic realm="localhost"')
self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZX60')])
self.assertStatus(401)
self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZXN0')])
self.assertStatus('200 OK')
self.assertBody("Hello test, you've been authorized.")
def testDigest(self):
self.getPage("/digest/")
self.assertStatus(401)
value = None
for k, v in self.headers:
if k.lower() == "www-authenticate":
if v.startswith("Digest"):
value = v
break
if value is None:
self._handlewebError(
"Digest authentification scheme was not found")
value = value[7:]
items = value.split(', ')
tokens = {}
for item in items:
key, value = item.split('=')
tokens[key.lower()] = value
missing_msg = "%s is missing"
bad_value_msg = "'%s' was expecting '%s' but found '%s'"
nonce = None
if 'realm' not in tokens:
self._handlewebError(missing_msg % 'realm')
elif tokens['realm'] != '"localhost"':
self._handlewebError(bad_value_msg %
('realm', '"localhost"', tokens['realm']))
if 'nonce' not in tokens:
self._handlewebError(missing_msg % 'nonce')
else:
nonce = tokens['nonce'].strip('"')
if 'algorithm' not in tokens:
self._handlewebError(missing_msg % 'algorithm')
elif tokens['algorithm'] != '"MD5"':
self._handlewebError(bad_value_msg %
('algorithm', '"MD5"', tokens['algorithm']))
if 'qop' not in tokens:
self._handlewebError(missing_msg % 'qop')
elif tokens['qop'] != '"auth"':
self._handlewebError(bad_value_msg %
('qop', '"auth"', tokens['qop']))
# Test a wrong 'realm' value
base_auth = (
'Digest '
'username="test", '
'realm="wrong realm", '
'nonce="%s", '
'uri="/digest/", '
'algorithm=MD5, '
'response="%s", '
'qop=auth, '
'nc=%s, '
'cnonce="1522e61005789929"'
)
auth = base_auth % (nonce, '', '00000001')
params = httpauth.parseAuthorization(auth)
response = httpauth._computeDigestResponse(params, 'test')
auth = base_auth % (nonce, response, '00000001')
self.getPage('/digest/', [('Authorization', auth)])
self.assertStatus(401)
# Test that must pass
base_auth = (
'Digest '
'username="test", '
'realm="localhost", '
'nonce="%s", '
'uri="/digest/", '
'algorithm=MD5, '
'response="%s", '
'qop=auth, '
'nc=%s, '
'cnonce="1522e61005789929"'
)
auth = base_auth % (nonce, '', '00000001')
params = httpauth.parseAuthorization(auth)
response = httpauth._computeDigestResponse(params, 'test')
auth = base_auth % (nonce, response, '00000001')
self.getPage('/digest/', [('Authorization', auth)])
self.assertStatus('200 OK')
self.assertBody("Hello test, you've been authorized.")

View File

@ -0,0 +1,29 @@
"""Tests for cherrypy/lib/httputil.py."""
import unittest
from cherrypy.lib import httputil
class UtilityTests(unittest.TestCase):
def test_urljoin(self):
# Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO
self.assertEqual(httputil.urljoin("/sn/", "/pi/"), "/sn/pi/")
self.assertEqual(httputil.urljoin("/sn/", "/pi"), "/sn/pi")
self.assertEqual(httputil.urljoin("/sn/", "/"), "/sn/")
self.assertEqual(httputil.urljoin("/sn/", ""), "/sn/")
self.assertEqual(httputil.urljoin("/sn", "/pi/"), "/sn/pi/")
self.assertEqual(httputil.urljoin("/sn", "/pi"), "/sn/pi")
self.assertEqual(httputil.urljoin("/sn", "/"), "/sn/")
self.assertEqual(httputil.urljoin("/sn", ""), "/sn")
self.assertEqual(httputil.urljoin("/", "/pi/"), "/pi/")
self.assertEqual(httputil.urljoin("/", "/pi"), "/pi")
self.assertEqual(httputil.urljoin("/", "/"), "/")
self.assertEqual(httputil.urljoin("/", ""), "/")
self.assertEqual(httputil.urljoin("", "/pi/"), "/pi/")
self.assertEqual(httputil.urljoin("", "/pi"), "/pi")
self.assertEqual(httputil.urljoin("", "/"), "/")
self.assertEqual(httputil.urljoin("", ""), "/")
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,181 @@
import cherrypy
from cherrypy._cpcompat import unicodestr
class IteratorBase(object):
created = 0
datachunk = u'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!
from cherrypy.test import helper
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 unicodestr(globals()[clsname].created)
@cherrypy.expose
def getall(self, clsname):
cherrypy.response.headers['Content-Type'] = 'text/plain'
return globals()[clsname]()
@cherrypy.expose
def stream(self, clsname):
return self.getall(clsname)
stream._cp_config = {'response.stream': True}
cherrypy.tree.mount(Root())
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':
assert header_value == unicodestr(1024 * 16 * 256), 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,100 @@
import cherrypy
from cherrypy.test import helper
from cherrypy._cpcompat import json
class JsonTest(helper.CPWebCase):
def setup_server():
class Root(object):
def plain(self):
return 'hello'
plain.exposed = True
def json_string(self):
return 'hello'
json_string.exposed = True
json_string._cp_config = {'tools.json_out.on': True}
def json_list(self):
return ['a', 'b', 42]
json_list.exposed = True
json_list._cp_config = {'tools.json_out.on': True}
def json_dict(self):
return {'answer': 42}
json_dict.exposed = True
json_dict._cp_config = {'tools.json_out.on': True}
def json_post(self):
if cherrypy.request.json == [13, 'c']:
return 'ok'
else:
return 'nok'
json_post.exposed = True
json_post._cp_config = {'tools.json_in.on': True}
def json_cached(self):
return 'hello there'
json_cached.exposed = True
json_cached._cp_config = {
'tools.json_out.on': True,
'tools.caching.on': True,
}
root = Root()
cherrypy.tree.mount(root)
setup_server = staticmethod(setup_server)
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,175 @@
"""Basic tests for the CherryPy core: request handling."""
import os
localDir = os.path.dirname(__file__)
import cherrypy
from cherrypy._cpcompat import ntob, ntou, py3k
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:
def index(self):
return "hello"
index.exposed = True
def uni_code(self):
cherrypy.request.login = tartaros
cherrypy.request.remote.name = erebos
uni_code.exposed = True
def slashes(self):
cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1'
slashes.exposed = True
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)'
whitespace.exposed = True
def as_string(self):
return "content"
as_string.exposed = True
def as_yield(self):
yield "content"
as_yield.exposed = True
def error(self):
raise ValueError()
error.exposed = True
error._cp_config = {'tools.log_tracebacks.on': True}
root = Root()
cherrypy.config.update({'log.error_file': error_log,
'log.access_file': access_log,
})
cherrypy.tree.mount(root)
from cherrypy.test import helper, logtest
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())
def testCustomLogFormat(self):
'''Test a customized access_log_format string,
which is a feature of _cplogging.LogManager.access() '''
original_logformat = cherrypy._cplogging.LogManager.access_log_format
cherrypy._cplogging.LogManager.access_log_format = \
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}' \
if py3k else \
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
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')
cherrypy._cplogging.LogManager.access_log_format = original_logformat
def testEscapedOutput(self):
# Test unicode in access log pieces.
self.markLog()
self.getPage("/uni_code")
self.assertStatus(200)
if py3k:
# The repr of a bytestring in py3k 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 py3k:
self.assertLog(-1, ntob('"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 Traceback (most recent call last):')
self.assertLog(-3, 'raise ValueError()')
finally:
ignore.pop()

View File

@ -0,0 +1,135 @@
"""Tests for various MIME issues, including the safe_multipart Tool."""
import cherrypy
from cherrypy._cpcompat import ntob, ntou, sorted
def setup_server():
class Root:
def multipart(self, parts):
return repr(parts)
multipart.exposed = True
def multipart_form_data(self, **kwargs):
return repr(list(sorted(kwargs.items())))
multipart_form_data.exposed = True
def flashupload(self, Filedata, Upload, Filename):
return ("Upload: %s, Filename: %s, Filedata: %r" %
(Upload, Filename, Filedata.file.read()))
flashupload.exposed = True
cherrypy.config.update({'server.max_request_body_size': 0})
cherrypy.tree.mount(Root())
# Client-side code #
from cherrypy.test import helper
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://bitbucket.org/cherrypy/cherrypy/issue/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 = ntob('<?xml version="1.0" encoding="UTF-8"?>\r\n'
'<projectDescription>\r\n'
'</projectDescription>\r\n')
body = (ntob(
'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
'Content-Disposition: form-data; name="Filename"\r\n'
'\r\n'
'.project\r\n'
'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
'Content-Disposition: form-data; '
'name="Filedata"; filename=".project"\r\n'
'Content-Type: application/octet-stream\r\n'
'\r\n')
+ filedata +
ntob('\r\n'
'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
'Content-Disposition: form-data; name="Upload"\r\n'
'\r\n'
'Submit Query\r\n'
# Flash apps omit the trailing \r\n on the last line:
'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--'
))
self.getPage('/flashupload', headers, "POST", body)
self.assertBody("Upload: Submit Query, Filename: .project, "
"Filedata: %r" % filedata)

View File

@ -0,0 +1,215 @@
import os
localDir = os.path.dirname(__file__)
logfile = os.path.join(localDir, "test_misc_tools.log")
import cherrypy
from cherrypy import tools
def setup_server():
class Root:
def index(self):
yield "Hello, world"
index.exposed = True
h = [("Content-Language", "en-GB"), ('Content-Type', 'text/plain')]
tools.response_headers(headers=h)(index)
def other(self):
return "salut"
other.exposed = True
other._cp_config = {
'tools.response_headers.on': True,
'tools.response_headers.headers': [("Content-Language", "fr"),
('Content-Type', 'text/plain')],
'tools.log_hooks.on': True,
}
class Accept:
_cp_config = {'tools.accept.on': True}
def index(self):
return '<a href="feed">Atom feed</a>'
index.exposed = True
# In Python 2.4+, we could use a decorator instead:
# @tools.accept('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>"""
feed.exposed = True
feed._cp_config = {'tools.accept.media': 'application/atom+xml'}
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"
select.exposed = True
class Referer:
def accept(self):
return "Accepted!"
accept.exposed = True
reject = accept
class AutoVary:
def index(self):
# Read a header directly with 'get'
ae = cherrypy.request.headers.get('Accept-Encoding')
# Read a header directly with '__getitem__'
cl = cherrypy.request.headers['Host']
# Read a header directly with '__contains__'
hasif = 'If-Modified-Since' in cherrypy.request.headers
# Read a header directly with 'has_key'
if hasattr(dict, 'has_key'):
# Python 2
has = cherrypy.request.headers.has_key('Range')
else:
# Python 3
has = 'Range' in cherrypy.request.headers
# Call a lib function
mtype = tools.accept.callable(['text/html', 'text/plain'])
return "Hello, world!"
index.exposed = True
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})
from cherrypy.test import helper
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,431 @@
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):
def setup_server():
class Root:
def index(self, name="world"):
return name
index.exposed = True
def foobar(self):
return "bar"
foobar.exposed = True
def default(self, *params, **kwargs):
return "default:" + repr(params)
default.exposed = True
def other(self):
return "other"
other.exposed = True
def extra(self, *p):
return repr(p)
extra.exposed = True
def redirect(self):
raise cherrypy.HTTPRedirect('dir1/', 302)
redirect.exposed = True
def notExposed(self):
return "not exposed"
def confvalue(self):
return cherrypy.request.config.get("user")
confvalue.exposed = True
def redirect_via_url(self, path):
raise cherrypy.HTTPRedirect(cherrypy.url(path))
redirect_via_url.exposed = True
def translate_html(self):
return "OK"
translate_html.exposed = True
def mapped_func(self, ID=None):
return "ID is %s" % ID
mapped_func.exposed = True
setattr(Root, "Von B\xfclow", mapped_func)
class Exposing:
def base(self):
return "expose works!"
cherrypy.expose(base)
cherrypy.expose(base, "1")
cherrypy.expose(base, "2")
class ExposingNewStyle(object):
def base(self):
return "expose works!"
cherrypy.expose(base)
cherrypy.expose(base, "1")
cherrypy.expose(base, "2")
class Dir1:
def index(self):
return "index for dir1"
index.exposed = True
def myMethod(self):
return "myMethod from dir1, path_info is:" + repr(
cherrypy.request.path_info)
myMethod.exposed = True
myMethod._cp_config = {'tools.trailing_slash.extra': True}
def default(self, *params):
return "default for dir1, param is:" + repr(params)
default.exposed = True
class Dir2:
def index(self):
return "index for dir2, path is:" + cherrypy.request.path_info
index.exposed = True
def script_name(self):
return cherrypy.tree.script_name()
script_name.exposed = True
def cherrypy_url(self):
return cherrypy.url("/extra")
cherrypy_url.exposed = True
def posparam(self, *vpath):
return "/".join(vpath)
posparam.exposed = True
class Dir3:
def default(self):
return "default for dir3, not exposed"
class Dir4:
def index(self):
return "index for dir4, not exposed"
class DefNoIndex:
def default(self, *args):
raise cherrypy.HTTPRedirect("contact")
default.exposed = True
# MethodDispatcher code
class ByMethod:
exposed = True
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:
def index(self):
return "made it!"
index.exposed = True
cherrypy.tree.mount(Isolated(), "/isolated")
class AnotherApp:
exposed = True
def GET(self):
return "milk"
cherrypy.tree.mount(AnotherApp(), "/app",
{'/': {'request.dispatch': d}})
setup_server = staticmethod(setup_server)
def testObjectMapping(self):
for url in script_names:
prefix = 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 diferent 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://bitbucket.org/cherrypy/cherrypy/issue/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:
prefix = 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://bitbucket.org/cherrypy/cherrypy/issue/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):
def hello(self):
return "Hello world!"
hello.exposed = True
# 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,137 @@
import cherrypy
from cherrypy.test import helper
script_names = ["", "/path/to/myapp"]
class ProxyTest(helper.CPWebCase):
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)
def pageurl(self):
return self.thisnewpage
pageurl.exposed = True
def index(self):
raise cherrypy.HTTPRedirect('dummy')
index.exposed = True
def remoteip(self):
return cherrypy.request.remote.ip
remoteip.exposed = True
def xhost(self):
raise cherrypy.HTTPRedirect('blah')
xhost.exposed = True
xhost._cp_config = {'tools.proxy.local': 'X-Host',
'tools.trailing_slash.extra': True,
}
def base(self):
return cherrypy.request.base
base.exposed = True
def ssl(self):
return cherrypy.request.base
ssl.exposed = True
ssl._cp_config = {'tools.proxy.scheme': 'X-Forwarded-Ssl'}
def newurl(self):
return ("Browse to <a href='%s'>this page</a>."
% cherrypy.url("/this/new/page"))
newurl.exposed = True
for sn in script_names:
cherrypy.tree.mount(Root(sn), sn)
setup_server = staticmethod(setup_server)
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://bitbucket.org/cherrypy/cherrypy/issue/562).
self.getPage("/xhost/", headers=[('X-Host', 'www.example.test')])
self.assertHeader('Location', "%s://www.example.test/xhost"
% self.scheme)

View File

@ -0,0 +1,59 @@
"""Tests for refleaks."""
from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob
import threading
import cherrypy
data = object()
from cherrypy.test import helper
class ReferenceTests(helper.CPWebCase):
def setup_server():
class Root:
def index(self, *args, **kwargs):
cherrypy.request.thing = data
return "Hello world!"
index.exposed = True
cherrypy.tree.mount(Root())
setup_server = staticmethod(setup_server)
def test_threadlocal_garbage(self):
success = []
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, ntob("Hello world!"))
finally:
c.close()
success.append(True)
ITERATIONS = 25
ts = []
for _ in range(ITERATIONS):
t = threading.Thread(target=getpage)
ts.append(t)
t.start()
for t in ts:
t.join()
self.assertEqual(len(success), ITERATIONS)

View File

@ -0,0 +1,796 @@
"""Basic tests for the cherrypy.Request object."""
import os
localDir = os.path.dirname(__file__)
import sys
import types
from cherrypy._cpcompat import IncompleteRead, ntob, ntou, unicodestr
import cherrypy
from cherrypy import _cptools, tools
from cherrypy.lib import httputil
defined_http_methods = ("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE",
"TRACE", "PROPFIND")
# Client-side code #
from cherrypy.test import helper
class RequestObjectTests(helper.CPWebCase):
def setup_server():
class Root:
def index(self):
return "hello"
index.exposed = True
def scheme(self):
return cherrypy.request.scheme
scheme.exposed = True
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)
def default(self, *args, **kwargs):
return "args: %s kwargs: %s" % (args, sorted(kwargs.items()))
default._cp_config = {'request.query_string_encoding': 'latin1'}
class ParamErrorsCallable(object):
exposed = True
def __call__(self):
return "data"
class ParamErrors(Test):
def one_positional(self, param1):
return "data"
one_positional.exposed = True
def one_positional_args(self, param1, *args):
return "data"
one_positional_args.exposed = True
def one_positional_args_kwargs(self, param1, *args, **kwargs):
return "data"
one_positional_args_kwargs.exposed = True
def one_positional_kwargs(self, param1, **kwargs):
return "data"
one_positional_kwargs.exposed = True
def no_positional(self):
return "data"
no_positional.exposed = True
def no_positional_args(self, *args):
return "data"
no_positional_args.exposed = True
def no_positional_args_kwargs(self, *args, **kwargs):
return "data"
no_positional_args_kwargs.exposed = True
def no_positional_kwargs(self, **kwargs):
return "data"
no_positional_kwargs.exposed = True
callable_object = ParamErrorsCallable()
def raise_type_error(self, **kwargs):
raise TypeError("Client Error")
raise_type_error.exposed = True
def raise_type_error_with_default_param(self, x, y=None):
return '%d' % 'a' # throw an exception
raise_type_error_with_default_param.exposed = True
def callable_error_page(status, **kwargs):
return "Error %s - Well, I'm very sorry but you haven't paid!" % (
status)
class Error(Test):
_cp_config = {'tools.log_tracebacks.on': True,
}
def reason_phrase(self):
raise cherrypy.HTTPError("410 Gone fishin'")
def custom(self, err='404'):
raise cherrypy.HTTPError(
int(err), "No, <b>really</b>, not found!")
custom._cp_config = {
'error_page.404': os.path.join(localDir, "static/index.html"),
'error_page.401': callable_error_page,
}
def custom_default(self):
return 1 + 'a' # raise an unexpected error
custom_default._cp_config = {
'error_page.default': callable_error_page}
def noexist(self):
raise cherrypy.HTTPError(404, "No, <b>really</b>, not found!")
noexist._cp_config = {'error_page.404': "nonexistent.html"}
def page_method(self):
raise ValueError()
def page_yield(self):
yield "howdy"
raise ValueError()
def page_streamed(self):
yield "word up"
raise ValueError()
yield "very oops"
page_streamed._cp_config = {"response.stream": True}
def cause_err_in_finalize(self):
# Since status must start with an int, this should error.
cherrypy.response.status = "ZOO OK"
cause_err_in_finalize._cp_config = {
'request.show_tracebacks': False}
def rethrow(self):
"""Test that an error raised here will be thrown out to
the server.
"""
raise ValueError()
rethrow._cp_config = {'request.throw_errors': True}
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://bitbucket.org/cherrypy/cherrypy/issue/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, unicodestr)
cherrypy.response.headers['ETag'] = val
return val
class HeaderElements(Test):
def get_elements(self, headername):
e = cherrypy.request.headers.elements(headername)
return "\n".join([unicodestr(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:
"""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 = {}
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>"
index.exposed = True
def get(self, ID):
return ("Divorce document %s: %s" %
(ID, self.documents.get(ID, "empty")))
get.exposed = True
root.divorce = Divorce()
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")
},
}
cherrypy.tree.mount(root, config=appconf)
setup_server = staticmethod(setup_server)
def test_scheme(self):
self.getPage("/scheme")
self.assertBody(self.scheme)
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)
# query string parameters are part of the URI, so if they are wrong
# for a particular handler, the status MUST be a 404.
error_msgs = [
'Missing parameters',
'Nothing matches the given URI',
'Multiple values for parameters',
'Unexpected query string parameters',
'Unexpected body parameters',
]
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',
):
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://bitbucket.org/cherrypy/cherrypy/issue/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://bitbucket.org/cherrypy/cherrypy/issue/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(ntob("\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(ntob("\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")
# 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], ntob("TRACE"))
else:
self.assertBody(m)
# 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://bitbucket.org/cherrypy/cherrypy/issue/790.
b = ntob("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://bitbucket.org/cherrypy/cherrypy/issue/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):
if getattr(cherrypy.server, "using_apache", False):
return self.skip("skipped due to known Apache differences... ")
self.getPage("/method/", method="CONNECT")
self.assertBody("CONNECT")
def testEmptyThreadlocals(self):
results = []
for x in range(20):
self.getPage("/threadlocal/")
results.append(self.body)
self.assertEqual(results, [ntob("None")] * 20)

View File

@ -0,0 +1,75 @@
import os
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
import cherrypy
from cherrypy.test import helper
import nose
class RoutesDispatchTest(helper.CPWebCase):
def setup_server():
try:
import routes
except ImportError:
raise nose.SkipTest('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
def index(self, **kwargs):
return "Welcome to %s, pop. %s" % (self.name, self.population)
index._cp_config = {
'tools.response_headers.on': True,
'tools.response_headers.headers': [
('Content-Language', 'en-GB')
]
}
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)
setup_server = staticmethod(setup_server)
def test_Routes_Dispatch(self):
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,487 @@
import os
localDir = os.path.dirname(__file__)
import threading
import time
import cherrypy
from cherrypy._cpcompat import copykeys, HTTPConnection, HTTPSConnection
from cherrypy.lib import sessions
from cherrypy.lib.httputil import response_codes
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():
class Root:
_cp_config = {'tools.sessions.on': True,
'tools.sessions.storage_type': 'ram',
'tools.sessions.storage_path': localDir,
'tools.sessions.timeout': (1.0 / 60),
'tools.sessions.clean_freq': (1.0 / 60),
}
def clear(self):
cherrypy.session.cache.clear()
clear.exposed = True
def data(self):
cherrypy.session['aha'] = 'foo'
return repr(cherrypy.session._data)
data.exposed = True
def testGen(self):
counter = cherrypy.session.get('counter', 0) + 1
cherrypy.session['counter'] = counter
yield str(counter)
testGen.exposed = True
def testStr(self):
counter = cherrypy.session.get('counter', 0) + 1
cherrypy.session['counter'] = counter
return str(counter)
testStr.exposed = True
def setsessiontype(self, newtype):
self.__class__._cp_config.update(
{'tools.sessions.storage_type': newtype})
if hasattr(cherrypy, "session"):
del cherrypy.session
cls = getattr(sessions, newtype.title() + 'Session')
if cls.clean_thread:
cls.clean_thread.stop()
cls.clean_thread.unsubscribe()
del cls.clean_thread
setsessiontype.exposed = True
setsessiontype._cp_config = {'tools.sessions.on': False}
def index(self):
sess = cherrypy.session
c = sess.get('counter', 0) + 1
time.sleep(0.01)
sess['counter'] = c
return str(c)
index.exposed = True
def keyin(self, key):
return str(key in cherrypy.session)
keyin.exposed = True
def delete(self):
cherrypy.session.delete()
sessions.expire()
return "done"
delete.exposed = True
def delkey(self, key):
del cherrypy.session[key]
return "OK"
delkey.exposed = True
def blah(self):
return self._cp_config['tools.sessions.storage_type']
blah.exposed = True
def iredir(self):
raise cherrypy.InternalRedirect('/blah')
iredir.exposed = True
def restricted(self):
return cherrypy.request.method
restricted.exposed = True
restricted._cp_config = {'tools.allow.on': True,
'tools.allow.methods': ['GET']}
def regen(self):
cherrypy.tools.sessions.regenerate()
return "logged in"
regen.exposed = True
def length(self):
return str(len(cherrypy.session))
length.exposed = True
def session_cookie(self):
# Must load() to start the clean thread.
cherrypy.session.load()
return cherrypy.session.id
session_cookie.exposed = True
session_cookie._cp_config = {
'tools.sessions.path': '/session_cookie',
'tools.sessions.name': 'temp',
'tools.sessions.persistent': False}
cherrypy.tree.mount(Root())
from cherrypy.test import helper
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):
os.unlink(os.path.join(localDir, fname))
def test_0_Session(self):
self.getPage('/setsessiontype/ram')
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.assertBody("{'aha': 'foo', 'counter': 3}")
self.getPage('/length', self.cookies)
self.assertBody('2')
self.getPage('/delkey?key=counter', self.cookies)
self.assertStatus(200)
self.getPage('/setsessiontype/file')
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")
f = lambda: [
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('/')
f = lambda: [
x for x in os.listdir(localDir) if x.startswith('session-')]
self.assertNotEqual(f(), [])
time.sleep(2)
self.assertEqual(f(), [])
def test_1_Ram_Concurrency(self):
self.getPage('/setsessiontype/ram')
self._test_Concurrency()
def test_2_File_Concurrency(self):
self.getPage('/setsessiontype/file')
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("file")
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('/setsessiontype/ram')
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(copykeys(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(copykeys(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 = copykeys(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()
s2 = sessions.RamSession()
s2.clean_up()
self.assertEqual(len(sessions.RamSession.locks), 1, 'Clean up should not remove active lock')
t.join()
import socket
try:
import memcache # NOQA
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('/setsessiontype/memcached')
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):
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,
}
def index(self, **kwargs):
return "Hi %s, you are logged in" % cherrypy.request.login
index.exposed = True
cherrypy.tree.mount(Test())
setup_server = staticmethod(setup_server)
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">')

View File

@ -0,0 +1,511 @@
import os
import signal
import socket
import sys
import time
import unittest
import warnings
import cherrypy
import cherrypy.process.servers
from cherrypy._cpcompat import BadStatusLine, ntob
from cherrypy.test import helper
engine = cherrypy.engine
thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
class Dependency:
def __init__(self, bus):
self.bus = bus
self.running = False
self.startcount = 0
self.gracecount = 0
self.threads = {}
def subscribe(self):
self.bus.subscribe('start', self.start)
self.bus.subscribe('stop', self.stop)
self.bus.subscribe('graceful', self.graceful)
self.bus.subscribe('start_thread', self.startthread)
self.bus.subscribe('stop_thread', self.stopthread)
def start(self):
self.running = True
self.startcount += 1
def stop(self):
self.running = False
def graceful(self):
self.gracecount += 1
def startthread(self, thread_id):
self.threads[thread_id] = None
def stopthread(self, thread_id):
del self.threads[thread_id]
db_connection = Dependency(engine)
def setup_server():
class Root:
def index(self):
return "Hello World"
index.exposed = True
def ctrlc(self):
raise KeyboardInterrupt()
ctrlc.exposed = True
def graceful(self):
engine.graceful()
return "app was (gracefully) restarted succesfully"
graceful.exposed = True
def block_explicit(self):
while True:
if cherrypy.response.timed_out:
cherrypy.response.timed_out = False
return "broken!"
time.sleep(0.01)
block_explicit.exposed = True
def block_implicit(self):
time.sleep(0.5)
return "response.timeout = %s" % cherrypy.response.timeout
block_implicit.exposed = True
cherrypy.tree.mount(Root())
cherrypy.config.update({
'environment': 'test_suite',
'engine.deadlock_poll_freq': 0.1,
})
db_connection.subscribe()
# ------------ Enough helpers. Time for real live test cases. ------------ #
class ServerStateTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def setUp(self):
cherrypy.server.socket_timeout = 0.1
self.do_gc_test = False
def test_0_NormalStateFlow(self):
engine.stop()
# Our db_connection should not be running
self.assertEqual(db_connection.running, False)
self.assertEqual(db_connection.startcount, 1)
self.assertEqual(len(db_connection.threads), 0)
# Test server start
engine.start()
self.assertEqual(engine.state, engine.states.STARTED)
host = cherrypy.server.socket_host
port = cherrypy.server.socket_port
self.assertRaises(IOError, cherrypy._cpserver.check_port, host, port)
# The db_connection should be running now
self.assertEqual(db_connection.running, True)
self.assertEqual(db_connection.startcount, 2)
self.assertEqual(len(db_connection.threads), 0)
self.getPage("/")
self.assertBody("Hello World")
self.assertEqual(len(db_connection.threads), 1)
# Test engine stop. This will also stop the HTTP server.
engine.stop()
self.assertEqual(engine.state, engine.states.STOPPED)
# Verify that our custom stop function was called
self.assertEqual(db_connection.running, False)
self.assertEqual(len(db_connection.threads), 0)
# Block the main thread now and verify that exit() works.
def exittest():
self.getPage("/")
self.assertBody("Hello World")
engine.exit()
cherrypy.server.start()
engine.start_with_callback(exittest)
engine.block()
self.assertEqual(engine.state, engine.states.EXITING)
def test_1_Restart(self):
cherrypy.server.start()
engine.start()
# The db_connection should be running now
self.assertEqual(db_connection.running, True)
grace = db_connection.gracecount
self.getPage("/")
self.assertBody("Hello World")
self.assertEqual(len(db_connection.threads), 1)
# Test server restart from this thread
engine.graceful()
self.assertEqual(engine.state, engine.states.STARTED)
self.getPage("/")
self.assertBody("Hello World")
self.assertEqual(db_connection.running, True)
self.assertEqual(db_connection.gracecount, grace + 1)
self.assertEqual(len(db_connection.threads), 1)
# Test server restart from inside a page handler
self.getPage("/graceful")
self.assertEqual(engine.state, engine.states.STARTED)
self.assertBody("app was (gracefully) restarted succesfully")
self.assertEqual(db_connection.running, True)
self.assertEqual(db_connection.gracecount, grace + 2)
# Since we are requesting synchronously, is only one thread used?
# Note that the "/graceful" request has been flushed.
self.assertEqual(len(db_connection.threads), 0)
engine.stop()
self.assertEqual(engine.state, engine.states.STOPPED)
self.assertEqual(db_connection.running, False)
self.assertEqual(len(db_connection.threads), 0)
def test_2_KeyboardInterrupt(self):
# Raise a keyboard interrupt in the HTTP server's main thread.
# We must start the server in this, the main thread
engine.start()
cherrypy.server.start()
self.persistent = True
try:
# Make the first request and assert there's no "Connection: close".
self.getPage("/")
self.assertStatus('200 OK')
self.assertBody("Hello World")
self.assertNoHeader("Connection")
cherrypy.server.httpserver.interrupt = KeyboardInterrupt
engine.block()
self.assertEqual(db_connection.running, False)
self.assertEqual(len(db_connection.threads), 0)
self.assertEqual(engine.state, engine.states.EXITING)
finally:
self.persistent = False
# Raise a keyboard interrupt in a page handler; on multithreaded
# servers, this should occur in one of the worker threads.
# This should raise a BadStatusLine error, since the worker
# thread will just die without writing a response.
engine.start()
cherrypy.server.start()
try:
self.getPage("/ctrlc")
except BadStatusLine:
pass
else:
print(self.body)
self.fail("AssertionError: BadStatusLine not raised")
engine.block()
self.assertEqual(db_connection.running, False)
self.assertEqual(len(db_connection.threads), 0)
def test_3_Deadlocks(self):
cherrypy.config.update({'response.timeout': 0.2})
engine.start()
cherrypy.server.start()
try:
self.assertNotEqual(engine.timeout_monitor.thread, None)
# Request a "normal" page.
self.assertEqual(engine.timeout_monitor.servings, [])
self.getPage("/")
self.assertBody("Hello World")
# request.close is called async.
while engine.timeout_monitor.servings:
sys.stdout.write(".")
time.sleep(0.01)
# Request a page that explicitly checks itself for deadlock.
# The deadlock_timeout should be 2 secs.
self.getPage("/block_explicit")
self.assertBody("broken!")
# Request a page that implicitly breaks deadlock.
# If we deadlock, we want to touch as little code as possible,
# so we won't even call handle_error, just bail ASAP.
self.getPage("/block_implicit")
self.assertStatus(500)
self.assertInBody("raise cherrypy.TimeoutError()")
finally:
engine.exit()
def test_4_Autoreload(self):
# If test_3 has not been executed, the server won't be stopped,
# so we'll have to do it.
if engine.state != engine.states.EXITING:
engine.exit()
# Start the demo script in a new process
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
p.write_conf(extra='test_case_name: "test_4_Autoreload"')
p.start(imports='cherrypy.test._test_states_demo')
try:
self.getPage("/start")
start = float(self.body)
# Give the autoreloader time to cache the file time.
time.sleep(2)
# Touch the file
os.utime(os.path.join(thisdir, "_test_states_demo.py"), None)
# Give the autoreloader time to re-exec the process
time.sleep(2)
host = cherrypy.server.socket_host
port = cherrypy.server.socket_port
cherrypy._cpserver.wait_for_occupied_port(host, port)
self.getPage("/start")
if not (float(self.body) > start):
raise AssertionError("start time %s not greater than %s" %
(float(self.body), start))
finally:
# Shut down the spawned process
self.getPage("/exit")
p.join()
def test_5_Start_Error(self):
# If test_3 has not been executed, the server won't be stopped,
# so we'll have to do it.
if engine.state != engine.states.EXITING:
engine.exit()
# If a process errors during start, it should stop the engine
# and exit with a non-zero exit code.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
wait=True)
p.write_conf(
extra="""starterror: True
test_case_name: "test_5_Start_Error"
"""
)
p.start(imports='cherrypy.test._test_states_demo')
if p.exit_code == 0:
self.fail("Process failed to return nonzero exit code.")
class PluginTests(helper.CPWebCase):
def test_daemonize(self):
if os.name not in ['posix']:
return self.skip("skipped (not on posix) ")
self.HOST = '127.0.0.1'
self.PORT = 8081
# Spawn the process and wait, when this returns, the original process
# is finished. If it daemonized properly, we should still be able
# to access pages.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
wait=True, daemonize=True,
socket_host='127.0.0.1',
socket_port=8081)
p.write_conf(
extra='test_case_name: "test_daemonize"')
p.start(imports='cherrypy.test._test_states_demo')
try:
# Just get the pid of the daemonization process.
self.getPage("/pid")
self.assertStatus(200)
page_pid = int(self.body)
self.assertEqual(page_pid, p.get_pid())
finally:
# Shut down the spawned process
self.getPage("/exit")
p.join()
# Wait until here to test the exit code because we want to ensure
# that we wait for the daemon to finish running before we fail.
if p.exit_code != 0:
self.fail("Daemonized parent process failed to exit cleanly.")
class SignalHandlingTests(helper.CPWebCase):
def test_SIGHUP_tty(self):
# When not daemonized, SIGHUP should shut down the server.
try:
from signal import SIGHUP
except ImportError:
return self.skip("skipped (no SIGHUP) ")
# Spawn the process.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
p.write_conf(
extra='test_case_name: "test_SIGHUP_tty"')
p.start(imports='cherrypy.test._test_states_demo')
# Send a SIGHUP
os.kill(p.get_pid(), SIGHUP)
# This might hang if things aren't working right, but meh.
p.join()
def test_SIGHUP_daemonized(self):
# When daemonized, SIGHUP should restart the server.
try:
from signal import SIGHUP
except ImportError:
return self.skip("skipped (no SIGHUP) ")
if os.name not in ['posix']:
return self.skip("skipped (not on posix) ")
# Spawn the process and wait, when this returns, the original process
# is finished. If it daemonized properly, we should still be able
# to access pages.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
wait=True, daemonize=True)
p.write_conf(
extra='test_case_name: "test_SIGHUP_daemonized"')
p.start(imports='cherrypy.test._test_states_demo')
pid = p.get_pid()
try:
# Send a SIGHUP
os.kill(pid, SIGHUP)
# Give the server some time to restart
time.sleep(2)
self.getPage("/pid")
self.assertStatus(200)
new_pid = int(self.body)
self.assertNotEqual(new_pid, pid)
finally:
# Shut down the spawned process
self.getPage("/exit")
p.join()
def _require_signal_and_kill(self, signal_name):
if not hasattr(signal, signal_name):
self.skip("skipped (no %(signal_name)s)" % vars())
if not hasattr(os, 'kill'):
self.skip("skipped (no os.kill)")
def test_SIGTERM(self):
"SIGTERM should shut down the server whether daemonized or not."
self._require_signal_and_kill('SIGTERM')
# Spawn a normal, undaemonized process.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
p.write_conf(
extra='test_case_name: "test_SIGTERM"')
p.start(imports='cherrypy.test._test_states_demo')
# Send a SIGTERM
os.kill(p.get_pid(), signal.SIGTERM)
# This might hang if things aren't working right, but meh.
p.join()
if os.name in ['posix']:
# Spawn a daemonized process and test again.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
wait=True, daemonize=True)
p.write_conf(
extra='test_case_name: "test_SIGTERM_2"')
p.start(imports='cherrypy.test._test_states_demo')
# Send a SIGTERM
os.kill(p.get_pid(), signal.SIGTERM)
# This might hang if things aren't working right, but meh.
p.join()
def test_signal_handler_unsubscribe(self):
self._require_signal_and_kill('SIGTERM')
# Although Windows has `os.kill` and SIGTERM is defined, the
# platform does not implement signals and sending SIGTERM
# will result in a forced termination of the process.
# Therefore, this test is not suitable for Windows.
if os.name == 'nt':
self.skip("SIGTERM not available")
# Spawn a normal, undaemonized process.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
p.write_conf(
extra="""unsubsig: True
test_case_name: "test_signal_handler_unsubscribe"
""")
p.start(imports='cherrypy.test._test_states_demo')
# Ask the process to quit
os.kill(p.get_pid(), signal.SIGTERM)
# This might hang if things aren't working right, but meh.
p.join()
# Assert the old handler ran.
target_line = open(p.error_log, 'rb').readlines()[-10]
if not ntob("I am an old SIGTERM handler.") in target_line:
self.fail("Old SIGTERM handler did not run.\n%r" % target_line)
class WaitTests(unittest.TestCase):
def test_wait_for_occupied_port_INADDR_ANY(self):
"""
Wait on INADDR_ANY should not raise IOError
In cases where the loopback interface does not exist, CherryPy cannot
effectively determine if a port binding to INADDR_ANY was effected.
In this situation, CherryPy should assume that it failed to detect
the binding (not that the binding failed) and only warn that it could
not verify it.
"""
# At such a time that CherryPy can reliably determine one or more
# viable IP addresses of the host, this test may be removed.
# Simulate the behavior we observe when no loopback interface is
# present by: finding a port that's not occupied, then wait on it.
free_port = self.find_free_port()
servers = cherrypy.process.servers
def with_shorter_timeouts(func):
"""
A context where occupied_port_timeout is much smaller to speed
test runs.
"""
# When we have Python 2.5, simplify using the with_statement.
orig_timeout = servers.occupied_port_timeout
servers.occupied_port_timeout = .07
try:
func()
finally:
servers.occupied_port_timeout = orig_timeout
def do_waiting():
# Wait on the free port that's unbound
with warnings.catch_warnings(record=True) as w:
servers.wait_for_occupied_port('0.0.0.0', free_port)
self.assertEqual(len(w), 1)
self.assertTrue(isinstance(w[0], warnings.WarningMessage))
self.assertTrue(
'Unable to verify that the server is bound on ' in str(w[0]))
# The wait should still raise an IO error if INADDR_ANY was
# not supplied.
self.assertRaises(IOError, servers.wait_for_occupied_port,
'127.0.0.1', free_port)
with_shorter_timeouts(do_waiting)
def find_free_port(self):
"Find a free port by binding to port 0 then unbinding."
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(('', 0))
free_port = sock.getsockname()[1]
sock.close()
return free_port

View File

@ -0,0 +1,339 @@
import os
import sys
from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob
from cherrypy._cpcompat import BytesIO
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
has_space_filepath = os.path.join(curdir, 'static', 'has space.html')
bigfile_filepath = os.path.join(curdir, "static", "bigfile.log")
# The file size needs to be big enough such that half the size of it
# won't be socket-buffered (or server-buffered) all in one go. See
# test_file_stream.
BIGFILE_SIZE = 1024 * 1024 * 4
import cherrypy
from cherrypy.lib import static
from cherrypy.test import helper
class StaticTest(helper.CPWebCase):
def setup_server():
if not os.path.exists(has_space_filepath):
open(has_space_filepath, 'wb').write(ntob('Hello, world\r\n'))
if not os.path.exists(bigfile_filepath) or \
os.path.getsize(bigfile_filepath) != BIGFILE_SIZE:
open(bigfile_filepath, 'wb').write(ntob("x" * BIGFILE_SIZE))
class Root:
def bigfile(self):
from cherrypy.lib import static
self.f = static.serve_file(bigfile_filepath)
return self.f
bigfile.exposed = True
bigfile._cp_config = {'response.stream': True}
def tell(self):
if self.f.input.closed:
return ''
return repr(self.f.input.tell()).rstrip('L')
tell.exposed = True
def fileobj(self):
f = open(os.path.join(curdir, 'style.css'), 'rb')
return static.serve_fileobj(f, content_type='text/css')
fileobj.exposed = True
def bytesio(self):
f = BytesIO(ntob('Fee\nfie\nfo\nfum'))
return static.serve_fileobj(f, content_type='text/plain')
bytesio.exposed = True
class Static:
def index(self):
return 'You want the Baron? You can have the Baron!'
index.exposed = True
def dynamic(self):
return "This is a DYNAMIC page"
dynamic.exposed = True
root = Root()
root.static = Static()
rootconf = {
'/static': {
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static',
'tools.staticdir.root': curdir,
},
'/style.css': {
'tools.staticfile.on': True,
'tools.staticfile.filename': os.path.join(curdir, 'style.css'),
},
'/docroot': {
'tools.staticdir.on': True,
'tools.staticdir.root': curdir,
'tools.staticdir.dir': 'static',
'tools.staticdir.index': 'index.html',
},
'/error': {
'tools.staticdir.on': True,
'request.show_tracebacks': True,
},
'/404test': {
'tools.staticdir.on': True,
'tools.staticdir.root': curdir,
'tools.staticdir.dir': 'static',
'error_page.404': error_page_404,
}
}
rootApp = cherrypy.Application(root)
rootApp.merge(rootconf)
test_app_conf = {
'/test': {
'tools.staticdir.index': 'index.html',
'tools.staticdir.on': True,
'tools.staticdir.root': curdir,
'tools.staticdir.dir': 'static',
},
}
testApp = cherrypy.Application(Static())
testApp.merge(test_app_conf)
vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp})
cherrypy.tree.graft(vhost)
setup_server = staticmethod(setup_server)
def teardown_server():
for f in (has_space_filepath, bigfile_filepath):
if os.path.exists(f):
try:
os.unlink(f)
except:
pass
teardown_server = staticmethod(teardown_server)
def testStatic(self):
self.getPage("/static/index.html")
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html')
self.assertBody('Hello, world\r\n')
# Using a staticdir.root value in a subdir...
self.getPage("/docroot/index.html")
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html')
self.assertBody('Hello, world\r\n')
# Check a filename with spaces in it
self.getPage("/static/has%20space.html")
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html')
self.assertBody('Hello, world\r\n')
self.getPage("/style.css")
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/css')
# Note: The body should be exactly 'Dummy stylesheet\n', but
# unfortunately some tools such as WinZip sometimes turn \n
# into \r\n on Windows when extracting the CherryPy tarball so
# we just check the content
self.assertMatchesBody('^Dummy stylesheet')
def test_fallthrough(self):
# Test that NotFound will then try dynamic handlers (see [878]).
self.getPage("/static/dynamic")
self.assertBody("This is a DYNAMIC page")
# Check a directory via fall-through to dynamic handler.
self.getPage("/static/")
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
self.assertBody('You want the Baron? You can have the Baron!')
def test_index(self):
# Check a directory via "staticdir.index".
self.getPage("/docroot/")
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html')
self.assertBody('Hello, world\r\n')
# The same page should be returned even if redirected.
self.getPage("/docroot")
self.assertStatus(301)
self.assertHeader('Location', '%s/docroot/' % self.base())
self.assertMatchesBody("This resource .* <a href=(['\"])%s/docroot/\\1>"
"%s/docroot/</a>." % (self.base(), self.base()))
def test_config_errors(self):
# Check that we get an error if no .file or .dir
self.getPage("/error/thing.html")
self.assertErrorPage(500)
if sys.version_info >= (3, 3):
errmsg = ntob("TypeError: staticdir\(\) missing 2 "
"required positional arguments")
else:
errmsg = ntob("TypeError: staticdir\(\) takes at least 2 "
"(positional )?arguments \(0 given\)")
self.assertMatchesBody(errmsg)
def test_security(self):
# Test up-level security
self.getPage("/static/../../test/style.css")
self.assertStatus((400, 403))
def test_modif(self):
# Test modified-since on a reasonably-large file
self.getPage("/static/dirback.jpg")
self.assertStatus("200 OK")
lastmod = ""
for k, v in self.headers:
if k == 'Last-Modified':
lastmod = v
ims = ("If-Modified-Since", lastmod)
self.getPage("/static/dirback.jpg", headers=[ims])
self.assertStatus(304)
self.assertNoHeader("Content-Type")
self.assertNoHeader("Content-Length")
self.assertNoHeader("Content-Disposition")
self.assertBody("")
def test_755_vhost(self):
self.getPage("/test/", [('Host', 'virt.net')])
self.assertStatus(200)
self.getPage("/test", [('Host', 'virt.net')])
self.assertStatus(301)
self.assertHeader('Location', self.scheme + '://virt.net/test/')
def test_serve_fileobj(self):
self.getPage("/fileobj")
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/css;charset=utf-8')
self.assertMatchesBody('^Dummy stylesheet')
def test_serve_bytesio(self):
self.getPage("/bytesio")
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
self.assertHeader('Content-Length', 14)
self.assertMatchesBody('Fee\nfie\nfo\nfum')
def test_file_stream(self):
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", "/bigfile", 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)
body = ntob('')
remaining = BIGFILE_SIZE
while remaining > 0:
data = response.fp.read(65536)
if not data:
break
body += data
remaining -= len(data)
if self.scheme == "https":
newconn = HTTPSConnection
else:
newconn = HTTPConnection
s, h, b = helper.webtest.openURL(
ntob("/tell"), headers=[], host=self.HOST, port=self.PORT,
http_conn=newconn)
if not b:
# The file was closed on the server.
tell_position = BIGFILE_SIZE
else:
tell_position = int(b)
read_so_far = len(body)
# It is difficult for us to force the server to only read
# the bytes that we ask for - there are going to be buffers
# inbetween.
#
# CherryPy will attempt to write as much data as it can to
# the socket, and we don't have a way to determine what that
# size will be. So we make the following assumption - by
# the time we have read in the entire file on the server,
# we will have at least received half of it. If this is not
# the case, then this is an indicator that either:
# - machines that are running this test are using buffer
# sizes greater than half of BIGFILE_SIZE; or
# - streaming is broken.
#
# At the time of writing, we seem to have encountered
# buffer sizes bigger than 512K, so we've increased
# BIGFILE_SIZE to 4MB.
if tell_position >= BIGFILE_SIZE:
if read_so_far < (BIGFILE_SIZE / 2):
self.fail(
"The file should have advanced to position %r, but "
"has already advanced to the end of the file. It "
"may not be streamed as intended, or at the wrong "
"chunk size (64k)" % read_so_far)
elif tell_position < read_so_far:
self.fail(
"The file should have advanced to position %r, but has "
"only advanced to position %r. It may not be streamed "
"as intended, or at the wrong chunk size (64k)" %
(read_so_far, tell_position))
if body != ntob("x" * BIGFILE_SIZE):
self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
(BIGFILE_SIZE, body[:50], len(body)))
conn.close()
def test_file_stream_deadlock(self):
if cherrypy.server.protocol_version != "HTTP/1.1":
return self.skip()
self.PROTOCOL = "HTTP/1.1"
# Make an initial request but abort early.
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest("GET", "/bigfile", 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)
body = response.fp.read(65536)
if body != ntob("x" * len(body)):
self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
(65536, body[:50], len(body)))
response.close()
conn.close()
# Make a second request, which should fetch the whole file.
self.persistent = False
self.getPage("/bigfile")
if self.body != ntob("x" * BIGFILE_SIZE):
self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
(BIGFILE_SIZE, self.body[:50], len(body)))
def test_error_page_with_serve_file(self):
self.getPage("/404test/yunyeen")
self.assertStatus(404)
self.assertInBody("I couldn't find that thing")
def error_page_404(status, message, traceback, version):
import os.path
return static.serve_file(os.path.join(curdir, 'static', '404.html'),
content_type='text/html')

View File

@ -0,0 +1,428 @@
"""Test the various means of instantiating and invoking tools."""
import gzip
import sys
import unittest
from cherrypy._cpcompat import BytesIO, copyitems, itervalues
from cherrypy._cpcompat import IncompleteRead, ntob, ntou, py3k, xrange
from cherrypy._cpcompat import bytestr, unicodestr
import time
timeout = 0.2
import types
import cherrypy
from cherrypy import tools
europoundUnicode = ntou('\x80\xa3')
# Client-side code #
from cherrypy.test import helper
class ToolTests(helper.CPWebCase):
def setup_server():
# Put check_access in a custom toolbox with its own namespace
myauthtools = cherrypy._cptools.Toolbox("myauth")
def check_access(default=False):
if not getattr(cherrypy.request, "userid", default):
raise cherrypy.HTTPError(401)
myauthtools.check_access = cherrypy.Tool(
'before_request_body', check_access)
def numerify():
def number_it(body):
for chunk in body:
for k, v in cherrypy.request.numerify_map:
chunk = chunk.replace(k, v)
yield chunk
cherrypy.response.body = number_it(cherrypy.response.body)
class NumTool(cherrypy.Tool):
def _setup(self):
def makemap():
m = self._merged_args().get("map", {})
cherrypy.request.numerify_map = copyitems(m)
cherrypy.request.hooks.attach('on_start_resource', makemap)
def critical():
cherrypy.request.error_response = cherrypy.HTTPError(
502).set_response
critical.failsafe = True
cherrypy.request.hooks.attach('on_start_resource', critical)
cherrypy.request.hooks.attach(self._point, self.callable)
tools.numerify = NumTool('before_finalize', numerify)
# It's not mandatory to inherit from cherrypy.Tool.
class NadsatTool:
def __init__(self):
self.ended = {}
self._name = "nadsat"
def nadsat(self):
def nadsat_it_up(body):
for chunk in body:
chunk = chunk.replace(ntob("good"), ntob("horrorshow"))
chunk = chunk.replace(ntob("piece"), ntob("lomtick"))
yield chunk
cherrypy.response.body = nadsat_it_up(cherrypy.response.body)
nadsat.priority = 0
def cleanup(self):
# This runs after the request has been completely written out.
cherrypy.response.body = [ntob("razdrez")]
id = cherrypy.request.params.get("id")
if id:
self.ended[id] = True
cleanup.failsafe = True
def _setup(self):
cherrypy.request.hooks.attach('before_finalize', self.nadsat)
cherrypy.request.hooks.attach('on_end_request', self.cleanup)
tools.nadsat = NadsatTool()
def pipe_body():
cherrypy.request.process_request_body = False
clen = int(cherrypy.request.headers['Content-Length'])
cherrypy.request.body = cherrypy.request.rfile.read(clen)
# Assert that we can use a callable object instead of a function.
class Rotator(object):
def __call__(self, scale):
r = cherrypy.response
r.collapse_body()
if py3k:
r.body = [bytes([(x + scale) % 256 for x in r.body[0]])]
else:
r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]]
cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator())
def stream_handler(next_handler, *args, **kwargs):
assert cherrypy.request.config.get('tools.streamer.arg') == 'arg value'
cherrypy.response.output = o = BytesIO()
try:
response = next_handler(*args, **kwargs)
# Ignore the response and return our accumulated output
# instead.
return o.getvalue()
finally:
o.close()
cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool(
stream_handler)
class Root:
def index(self):
return "Howdy earth!"
index.exposed = True
def tarfile(self):
assert cherrypy.request.config.get('tools.streamer.arg') == 'arg value'
cherrypy.response.output.write(ntob('I am '))
cherrypy.response.output.write(ntob('a tarfile'))
tarfile.exposed = True
tarfile._cp_config = {'tools.streamer.on': True, 'tools.streamer.arg': 'arg value'}
def euro(self):
hooks = list(cherrypy.request.hooks['before_finalize'])
hooks.sort()
cbnames = [x.callback.__name__ for x in hooks]
assert cbnames == ['gzip'], cbnames
priorities = [x.priority for x in hooks]
assert priorities == [80], priorities
yield ntou("Hello,")
yield ntou("world")
yield europoundUnicode
euro.exposed = True
# Bare hooks
def pipe(self):
return cherrypy.request.body
pipe.exposed = True
pipe._cp_config = {'hooks.before_request_body': pipe_body}
# Multiple decorators; include kwargs just for fun.
# Note that rotator must run before gzip.
def decorated_euro(self, *vpath):
yield ntou("Hello,")
yield ntou("world")
yield europoundUnicode
decorated_euro.exposed = True
decorated_euro = tools.gzip(compress_level=6)(decorated_euro)
decorated_euro = tools.rotator(scale=3)(decorated_euro)
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 itervalues(dct):
if isinstance(value, types.FunctionType):
value.exposed = True
setattr(root, name.lower(), cls())
Test = TestType('Test', (object,), {})
# METHOD ONE:
# Declare Tools in _cp_config
class Demo(Test):
_cp_config = {"tools.nadsat.on": True}
def index(self, id=None):
return "A good piece of cherry pie"
def ended(self, id):
return repr(tools.nadsat.ended[id])
def err(self, id=None):
raise ValueError()
def errinstream(self, id=None):
yield "nonconfidential"
raise ValueError()
yield "confidential"
# METHOD TWO: decorator using Tool()
# We support Python 2.3, but the @-deco syntax would look like
# this:
# @tools.check_access()
def restricted(self):
return "Welcome!"
restricted = myauthtools.check_access()(restricted)
userid = restricted
def err_in_onstart(self):
return "success!"
def stream(self, id=None):
for x in xrange(100000000):
yield str(x)
stream._cp_config = {'response.stream': True}
conf = {
# METHOD THREE:
# Declare Tools in detached config
'/demo': {
'tools.numerify.on': True,
'tools.numerify.map': {ntob("pie"): ntob("3.14159")},
},
'/demo/restricted': {
'request.show_tracebacks': False,
},
'/demo/userid': {
'request.show_tracebacks': False,
'myauth.check_access.default': True,
},
'/demo/errinstream': {
'response.stream': True,
},
'/demo/err_in_onstart': {
# Because this isn't a dict, on_start_resource will error.
'tools.numerify.map': "pie->3.14159"
},
# Combined tools
'/euro': {
'tools.gzip.on': True,
'tools.encode.on': True,
},
# Priority specified in config
'/decorated_euro/subpath': {
'tools.gzip.priority': 10,
},
# Handler wrappers
'/tarfile': {'tools.streamer.on': True}
}
app = cherrypy.tree.mount(root, config=conf)
app.request_class.namespaces['myauth'] = myauthtools
if sys.version_info >= (2, 5):
from cherrypy.test import _test_decorators
root.tooldecs = _test_decorators.ToolExamples()
setup_server = staticmethod(setup_server)
def testHookErrors(self):
self.getPage("/demo/?id=1")
# If body is "razdrez", then on_end_request is being called too early.
self.assertBody("A horrorshow lomtick of cherry 3.14159")
# If this fails, then on_end_request isn't being called at all.
time.sleep(0.1)
self.getPage("/demo/ended/1")
self.assertBody("True")
valerr = '\n raise ValueError()\nValueError'
self.getPage("/demo/err?id=3")
# If body is "razdrez", then on_end_request is being called too early.
self.assertErrorPage(502, pattern=valerr)
# If this fails, then on_end_request isn't being called at all.
time.sleep(0.1)
self.getPage("/demo/ended/3")
self.assertBody("True")
# If body is "razdrez", then on_end_request is being called too early.
if (cherrypy.server.protocol_version == "HTTP/1.0" or
getattr(cherrypy.server, "using_apache", False)):
self.getPage("/demo/errinstream?id=5")
# Because this error is raised after the response body has
# started, the status should not change to an error status.
self.assertStatus("200 OK")
self.assertBody("nonconfidential")
else:
# Because this error is raised after the response body has
# started, and because it's chunked output, an error is raised by
# the HTTP client when it encounters incomplete output.
self.assertRaises((ValueError, IncompleteRead), self.getPage,
"/demo/errinstream?id=5")
# If this fails, then on_end_request isn't being called at all.
time.sleep(0.1)
self.getPage("/demo/ended/5")
self.assertBody("True")
# Test the "__call__" technique (compile-time decorator).
self.getPage("/demo/restricted")
self.assertErrorPage(401)
# Test compile-time decorator with kwargs from config.
self.getPage("/demo/userid")
self.assertBody("Welcome!")
def testEndRequestOnDrop(self):
old_timeout = None
try:
httpserver = cherrypy.server.httpserver
old_timeout = httpserver.timeout
except (AttributeError, IndexError):
return self.skip()
try:
httpserver.timeout = timeout
# Test that on_end_request is called even if the client drops.
self.persistent = True
try:
conn = self.HTTP_CONN
conn.putrequest("GET", "/demo/stream?id=9", skip_host=True)
conn.putheader("Host", self.HOST)
conn.endheaders()
# Skip the rest of the request and close the conn. This will
# cause the server's active socket to error, which *should*
# result in the request being aborted, and request.close being
# called all the way up the stack (including WSGI middleware),
# eventually calling our on_end_request hook.
finally:
self.persistent = False
time.sleep(timeout * 2)
# Test that the on_end_request hook was called.
self.getPage("/demo/ended/9")
self.assertBody("True")
finally:
if old_timeout is not None:
httpserver.timeout = old_timeout
def testGuaranteedHooks(self):
# The 'critical' on_start_resource hook is 'failsafe' (guaranteed
# to run even if there are failures in other on_start methods).
# This is NOT true of the other hooks.
# Here, we have set up a failure in NumerifyTool.numerify_map,
# but our 'critical' hook should run and set the error to 502.
self.getPage("/demo/err_in_onstart")
self.assertErrorPage(502)
self.assertInBody(
"AttributeError: 'str' object has no attribute 'items'")
def testCombinedTools(self):
expectedResult = (ntou("Hello,world") +
europoundUnicode).encode('utf-8')
zbuf = BytesIO()
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
zfile.write(expectedResult)
zfile.close()
self.getPage("/euro",
headers=[
("Accept-Encoding", "gzip"),
("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7")])
self.assertInBody(zbuf.getvalue()[:3])
zbuf = BytesIO()
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6)
zfile.write(expectedResult)
zfile.close()
self.getPage("/decorated_euro", headers=[("Accept-Encoding", "gzip")])
self.assertInBody(zbuf.getvalue()[:3])
# This returns a different value because gzip's priority was
# lowered in conf, allowing the rotator to run after gzip.
# Of course, we don't want breakage in production apps,
# but it proves the priority was changed.
self.getPage("/decorated_euro/subpath",
headers=[("Accept-Encoding", "gzip")])
if py3k:
self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()]))
else:
self.assertInBody(''.join([chr((ord(x) + 3) % 256)
for x in zbuf.getvalue()]))
def testBareHooks(self):
content = "bit of a pain in me gulliver"
self.getPage("/pipe",
headers=[("Content-Length", str(len(content))),
("Content-Type", "text/plain")],
method="POST", body=content)
self.assertBody(content)
def testHandlerWrapperTool(self):
self.getPage("/tarfile")
self.assertBody("I am a tarfile")
def testToolWithConfig(self):
if not sys.version_info >= (2, 5):
return self.skip("skipped (Python 2.5+ only)")
self.getPage('/tooldecs/blah')
self.assertHeader('Content-Type', 'application/data')
def testWarnToolOn(self):
# get
try:
cherrypy.tools.numerify.on
except AttributeError:
pass
else:
raise AssertionError("Tool.on did not error as it should have.")
# set
try:
cherrypy.tools.numerify.on = True
except AttributeError:
pass
else:
raise AssertionError("Tool.on did not error as it should have.")
class SessionAuthTest(unittest.TestCase):
def test_login_screen_returns_bytes(self):
"""
login_screen must return bytes even if unicode parameters are passed.
Issue 1132 revealed that login_screen would return unicode if the
username and password were unicode.
"""
sa = cherrypy.lib.cptools.SessionAuth()
res = sa.login_screen(None, username=unicodestr('nobody'),
password=unicodestr('anypass'))
self.assertTrue(isinstance(res, bytestr))

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