mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-01-06 11:09:57 -08:00
a528f052b9
* Bump cherrypy from 18.9.0 to 18.10.0 Bumps [cherrypy](https://github.com/cherrypy/cherrypy) from 18.9.0 to 18.10.0. - [Changelog](https://github.com/cherrypy/cherrypy/blob/main/CHANGES.rst) - [Commits](https://github.com/cherrypy/cherrypy/compare/v18.9.0...v18.10.0) --- updated-dependencies: - dependency-name: cherrypy dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> * Update cherrypy==18.10.0 --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com> [skip ci]
971 lines
34 KiB
Python
971 lines
34 KiB
Python
import sys
|
|
import time
|
|
import collections
|
|
import operator
|
|
from http.cookies import SimpleCookie, CookieError
|
|
|
|
import uuid
|
|
|
|
from more_itertools import consume
|
|
|
|
import cherrypy
|
|
from cherrypy._cpcompat import ntob
|
|
from cherrypy import _cpreqbody
|
|
from cherrypy._cperror import format_exc, bare_error
|
|
from cherrypy.lib import httputil, reprconf, encoding
|
|
|
|
|
|
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):
|
|
"""
|
|
Hooks sort by priority, ascending, such that
|
|
hooks of lower priority are run first.
|
|
"""
|
|
return 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)'
|
|
% (cls.__module__, cls.__name__, self.callback,
|
|
self.failsafe, self.priority,
|
|
', '.join(['%s=%r' % (k, v)
|
|
for k, v in self.kwargs.items()])))
|
|
|
|
|
|
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."""
|
|
self.run_hooks(iter(sorted(self[point])))
|
|
|
|
@classmethod
|
|
def run_hooks(cls, hooks):
|
|
"""Execute the indicated hooks, trapping errors.
|
|
|
|
Hooks with ``.failsafe == True`` are guaranteed to run
|
|
even if others at the same hookpoint fail. In this case,
|
|
log the failure and proceed on to the next hook. The only
|
|
way to stop all processing from one of these hooks is
|
|
to raise a BaseException like SystemExit or
|
|
KeyboardInterrupt and stop the whole server.
|
|
"""
|
|
assert isinstance(hooks, collections.abc.Iterator)
|
|
quiet_errors = (
|
|
cherrypy.HTTPError,
|
|
cherrypy.HTTPRedirect,
|
|
cherrypy.InternalRedirect,
|
|
)
|
|
safe = filter(operator.attrgetter('failsafe'), hooks)
|
|
for hook in hooks:
|
|
try:
|
|
hook()
|
|
except quiet_errors:
|
|
cls.run_hooks(safe)
|
|
raise
|
|
except Exception:
|
|
cherrypy.log(traceback=True, severity=40)
|
|
cls.run_hooks(safe)
|
|
raise
|
|
|
|
def __copy__(self):
|
|
newmap = self.__class__()
|
|
# We can't just use 'update' because we want copies of the
|
|
# mutable values (each is a list) as well.
|
|
for k, v in self.items():
|
|
newmap[k] = v[:]
|
|
return newmap
|
|
copy = __copy__
|
|
|
|
def __repr__(self):
|
|
cls = self.__class__
|
|
return '%s.%s(points=%r)' % (
|
|
cls.__module__,
|
|
cls.__name__,
|
|
list(self)
|
|
)
|
|
|
|
|
|
# Config namespace handlers
|
|
|
|
def hooks_namespace(k, v):
|
|
"""Attach bare hooks declared in config."""
|
|
# Use split again to allow multiple hooks for a single
|
|
# hookpoint per path (e.g. "hooks.before_handler.1").
|
|
# Little-known fact you only get from reading source ;)
|
|
hookpoint = k.split('.', 1)[0]
|
|
if isinstance(v, str):
|
|
v = cherrypy.lib.reprconf.attributes(v)
|
|
if not isinstance(v, Hook):
|
|
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).
|
|
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
|
|
# http://cherrypy.dev/ticket/889
|
|
if k[:8] == 'headers.':
|
|
cherrypy.serving.response.headers[k.split('.', 1)[1]] = v
|
|
else:
|
|
setattr(cherrypy.serving.response, k, v)
|
|
|
|
|
|
def error_page_namespace(k, v):
|
|
"""Attach error pages declared in config."""
|
|
if k != 'default':
|
|
k = int(k)
|
|
cherrypy.serving.request.error_page[k] = v
|
|
|
|
|
|
hookpoints = ['on_start_resource', 'before_request_body',
|
|
'before_handler', 'before_finalize',
|
|
'on_end_resource', 'on_end_request',
|
|
'before_error_response', 'after_error_response']
|
|
|
|
|
|
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 want tools to
|
|
interpret the headers and body, those are elsewhere, mostly in
|
|
Tools). This 'metadata' consists of socket data, transport
|
|
characteristics, and the Request-Line. This 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 by
|
|
the Request-URI.
|
|
|
|
Common methods include GET, HEAD, POST, PUT, and 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
|
|
interpreted by the resource. The query portion of a URI follows the
|
|
path component, and is separated by a '?'. For example, the URI
|
|
'http://www.cherrypy.dev/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).
|
|
If a query string is provided that cannot be decoded with this encoding,
|
|
404 is raised (since technically it's a different URI). If you want
|
|
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
|
|
the client's request message AND the server's level of HTTP
|
|
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
|
|
names (in Title-Case format); however, you may get and set them in
|
|
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). See also:
|
|
httputil.HeaderMap, httputil.HeaderElement.
|
|
"""
|
|
|
|
cookie = SimpleCookie()
|
|
"""See help(Cookie)."""
|
|
|
|
rfile = None
|
|
"""
|
|
If the request included an entity (body), it will be available
|
|
as a stream in this attribute. However, the rfile will normally
|
|
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
|
|
number of bytes specified in request.headers['Content-Length'].
|
|
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', 'PATCH')
|
|
"""
|
|
A sequence of HTTP methods for which CherryPy will automatically
|
|
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'
|
|
or multipart, this will be None. Otherwise, this will be an instance
|
|
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()
|
|
"""
|
|
The object which looks up the 'page handler' callable and collects
|
|
config for the current request based on the path_info, other
|
|
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 to the script_name ('mount point') of the
|
|
application which is handling this request.
|
|
"""
|
|
|
|
login = None
|
|
"""
|
|
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
|
|
produce the response. The discovery of the handler and the arguments
|
|
it will receive are determined by the request.dispatch 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 current
|
|
request.
|
|
|
|
These entries are collected from global config, application config
|
|
(based on request.path_info), and from handler config (exactly how
|
|
is governed by the request.dispatch object in 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'
|
|
resource handler (also, a 'default' handler if path_info ends with
|
|
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, ...]}.
|
|
|
|
Each key is a str naming the hook point, and each value is a list
|
|
of hooks which will be called at that hook point during this request.
|
|
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
|
|
during request processing. This is not used for expected exceptions
|
|
(like NotFound, HTTPError, or HTTPRedirect) which are raised in
|
|
response to expected conditions (those should be customized either
|
|
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
|
|
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.
|
|
|
|
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
|
|
include a traceback in the response body."""
|
|
|
|
show_mismatched_params = True
|
|
"""
|
|
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.
|
|
"""
|
|
|
|
unique_id = None
|
|
"""A lazy object generating and memorizing UUID4 on ``str()`` render."""
|
|
|
|
namespaces = reprconf.NamespaceSet(
|
|
**{'hooks': hooks_namespace,
|
|
'request': request_namespace,
|
|
'response': response_namespace,
|
|
'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".
|
|
"""
|
|
self.local = local_host
|
|
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
|
|
|
|
self.unique_id = LazyUUID4()
|
|
|
|
def close(self):
|
|
"""Run cleanup code.
|
|
|
|
(Core)
|
|
"""
|
|
if not self.closed:
|
|
self.closed = True
|
|
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:
|
|
# request server actual written supported response
|
|
# protocol protocol response protocol feature set
|
|
# a 1.0 1.0 1.0 1.0
|
|
# b 1.0 1.1 1.1 1.0
|
|
# c 1.1 1.0 1.0 1.0
|
|
# d 1.1 1.1 1.1 1.1
|
|
# Notice that, in (b), the response will be "HTTP/1.1" even though
|
|
# the client only understands 1.0. RFC 2616 10.5.6 says we should
|
|
# only return 505 if the _major_ version is different.
|
|
rp = int(req_protocol[5]), int(req_protocol[7])
|
|
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 Exception:
|
|
if self.throw_errors:
|
|
raise
|
|
else:
|
|
# Failure in setup, error handler or finalize. Bypass them.
|
|
# Can't use handle_error because we may not have hooks yet.
|
|
cherrypy.log(traceback=True, severity=40)
|
|
if self.show_tracebacks:
|
|
body = format_exc()
|
|
else:
|
|
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 Exception:
|
|
cherrypy.log.error(traceback=True)
|
|
|
|
return response
|
|
|
|
def respond(self, path_info):
|
|
"""Generate a response for the resource at self.path_info.
|
|
|
|
(Core)
|
|
"""
|
|
try:
|
|
try:
|
|
try:
|
|
self._do_respond(path_info)
|
|
except (cherrypy.HTTPRedirect, cherrypy.HTTPError):
|
|
inst = sys.exc_info()[1]
|
|
inst.set_response()
|
|
self.stage = 'before_finalize (HTTPError)'
|
|
self.hooks.run('before_finalize')
|
|
cherrypy.serving.response.finalize()
|
|
finally:
|
|
self.stage = 'on_end_resource'
|
|
self.hooks.run('on_end_resource')
|
|
except self.throws:
|
|
raise
|
|
except Exception:
|
|
if self.throw_errors:
|
|
raise
|
|
self.handle_error()
|
|
|
|
def _do_respond(self, path_info):
|
|
response = cherrypy.serving.response
|
|
|
|
if self.app is None:
|
|
raise cherrypy.NotFound()
|
|
|
|
self.hooks = self.__class__.hooks.copy()
|
|
self.toolmaps = {}
|
|
|
|
# Get the 'Host' header, so we can HTTPRedirect properly.
|
|
self.stage = 'process_headers'
|
|
self.process_headers()
|
|
|
|
self.stage = 'get_resource'
|
|
self.get_resource(path_info)
|
|
|
|
self.body = _cpreqbody.RequestBody(
|
|
self.rfile, self.headers, request_params=self.params)
|
|
|
|
self.namespaces(self.config)
|
|
|
|
self.stage = 'on_start_resource'
|
|
self.hooks.run('on_start_resource')
|
|
|
|
# Parse the querystring
|
|
self.stage = 'process_query_string'
|
|
self.process_query_string()
|
|
|
|
# Process the body
|
|
if self.process_request_body:
|
|
if self.method not in self.methods_with_bodies:
|
|
self.process_request_body = False
|
|
self.stage = 'before_request_body'
|
|
self.hooks.run('before_request_body')
|
|
if self.process_request_body:
|
|
self.body.process()
|
|
|
|
# Run the handler
|
|
self.stage = 'before_handler'
|
|
self.hooks.run('before_handler')
|
|
if self.handler:
|
|
self.stage = 'handler'
|
|
response.body = self.handler()
|
|
|
|
# Finalize
|
|
self.stage = 'before_finalize'
|
|
self.hooks.run('before_finalize')
|
|
response.finalize()
|
|
|
|
def process_query_string(self):
|
|
"""Parse the query string into Python structures.
|
|
|
|
(Core)
|
|
"""
|
|
try:
|
|
p = httputil.parse_query_string(
|
|
self.query_string, encoding=self.query_string_encoding)
|
|
except UnicodeDecodeError:
|
|
raise cherrypy.HTTPError(
|
|
404, 'The given query string could not be processed. Query '
|
|
'strings for this resource must be encoded with %r.' %
|
|
self.query_string_encoding)
|
|
|
|
self.params.update(p)
|
|
|
|
def process_headers(self):
|
|
"""Parse HTTP header data into Python structures.
|
|
|
|
(Core)
|
|
"""
|
|
# Process the headers into self.headers
|
|
headers = self.headers
|
|
for name, value in self.header_list:
|
|
# Call title() now (and use dict.__method__(headers))
|
|
# so title doesn't have to be called twice.
|
|
name = name.title()
|
|
value = value.strip()
|
|
|
|
headers[name] = httputil.decode_TEXT_maybe(value)
|
|
|
|
# Some clients, notably Konquoror, supply multiple
|
|
# cookies on different lines with the same key. To
|
|
# handle this case, store all cookies in self.cookie.
|
|
if name == 'Cookie':
|
|
try:
|
|
self.cookie.load(value)
|
|
except CookieError as exc:
|
|
raise cherrypy.HTTPError(400, str(exc))
|
|
|
|
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
|
|
# which lacks a Host header field.
|
|
if self.protocol >= (1, 1):
|
|
msg = "HTTP/1.1 requires a 'Host' request header."
|
|
raise cherrypy.HTTPError(400, msg)
|
|
else:
|
|
headers['Host'] = httputil.SanitizedHost(dict.get(headers, 'Host'))
|
|
|
|
host = dict.get(headers, 'Host')
|
|
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() should set self.handler and self.config
|
|
dispatch(path)
|
|
|
|
def handle_error(self):
|
|
"""Handle the last unanticipated exception.
|
|
|
|
(Core)
|
|
"""
|
|
try:
|
|
self.hooks.run('before_error_response')
|
|
if self.error_response:
|
|
self.error_response()
|
|
self.hooks.run('after_error_response')
|
|
cherrypy.serving.response.finalize()
|
|
except cherrypy.HTTPRedirect:
|
|
inst = sys.exc_info()[1]
|
|
inst.set_response()
|
|
cherrypy.serving.response.finalize()
|
|
|
|
|
|
class ResponseBody(object):
|
|
"""The body of the HTTP response (the response entity)."""
|
|
|
|
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 isinstance(value, str):
|
|
raise ValueError(self.unicode_err)
|
|
elif isinstance(value, list):
|
|
# every item in a list must be bytes...
|
|
if any(isinstance(item, str) for item in value):
|
|
raise ValueError(self.unicode_err)
|
|
|
|
obj._body = encoding.prepare_iter(value)
|
|
|
|
|
|
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
|
|
names (in Title-Case format); however, you may get and set them in
|
|
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.
|
|
"""
|
|
|
|
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.
|
|
dict.update(self.headers, {
|
|
'Content-Type': 'text/html',
|
|
'Server': 'CherryPy/' + cherrypy.__version__,
|
|
'Date': httputil.HTTPDate(self.time),
|
|
})
|
|
self.cookie = SimpleCookie()
|
|
|
|
def collapse_body(self):
|
|
"""Collapse self.body to a single string; replace it and return it."""
|
|
new_body = b''.join(self.body)
|
|
self.body = new_body
|
|
return new_body
|
|
|
|
def _flush_body(self):
|
|
"""Discard self.body but consume any generator such that any
|
|
finalization can occur, such as is required by caching.tee_output()."""
|
|
consume(iter(self.body))
|
|
|
|
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') + \
|
|
b' ' + headers.encode(reason)
|
|
|
|
if self.stream:
|
|
# The upshot: wsgiserver will chunk the response if
|
|
# you pop Content-Length (or set it explicitly to None).
|
|
# Note that lib.static sets C-L to the file's st_size.
|
|
if dict.get(headers, 'Content-Length') is None:
|
|
dict.pop(headers, 'Content-Length', None)
|
|
elif code < 200 or code in (204, 205, 304):
|
|
# "All 1xx (informational), 204 (no content),
|
|
# and 304 (not modified) responses MUST NOT
|
|
# include a message-body."
|
|
dict.pop(headers, 'Content-Length', None)
|
|
self._flush_body()
|
|
self.body = b''
|
|
else:
|
|
# Responses which are not streamed should have a Content-Length,
|
|
# but allow user code to set Content-Length if desired.
|
|
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('\r\n'):
|
|
name, value = line.split(': ', 1)
|
|
if isinstance(name, str):
|
|
name = name.encode('ISO-8859-1')
|
|
if isinstance(value, str):
|
|
value = headers.encode(value)
|
|
h.append((name, value))
|
|
|
|
|
|
class LazyUUID4(object):
|
|
def __str__(self):
|
|
"""Return UUID4 and keep it for future calls."""
|
|
return str(self.uuid4)
|
|
|
|
@property
|
|
def uuid4(self):
|
|
"""Provide unique id on per-request basis using UUID4.
|
|
|
|
It's evaluated lazily on render.
|
|
"""
|
|
try:
|
|
self._uuid4
|
|
except AttributeError:
|
|
# evaluate on first access
|
|
self._uuid4 = uuid.uuid4()
|
|
|
|
return self._uuid4
|