1import sys
2import time
3import collections
4import operator
5from http.cookies import SimpleCookie, CookieError
6
7import uuid
8
9from more_itertools import consume
10
11import cherrypy
12from cherrypy._cpcompat import ntob
13from cherrypy import _cpreqbody
14from cherrypy._cperror import format_exc, bare_error
15from cherrypy.lib import httputil, reprconf, encoding
16
17
18class Hook(object):
19
20    """A callback and its metadata: failsafe, priority, and kwargs."""
21
22    callback = None
23    """
24    The bare callable that this Hook object is wrapping, which will
25    be called when the Hook is called."""
26
27    failsafe = False
28    """
29    If True, the callback is guaranteed to run even if other callbacks
30    from the same call point raise exceptions."""
31
32    priority = 50
33    """
34    Defines the order of execution for a list of Hooks. Priority numbers
35    should be limited to the closed interval [0, 100], but values outside
36    this range are acceptable, as are fractional values."""
37
38    kwargs = {}
39    """
40    A set of keyword arguments that will be passed to the
41    callable on each call."""
42
43    def __init__(self, callback, failsafe=None, priority=None, **kwargs):
44        self.callback = callback
45
46        if failsafe is None:
47            failsafe = getattr(callback, 'failsafe', False)
48        self.failsafe = failsafe
49
50        if priority is None:
51            priority = getattr(callback, 'priority', 50)
52        self.priority = priority
53
54        self.kwargs = kwargs
55
56    def __lt__(self, other):
57        """
58        Hooks sort by priority, ascending, such that
59        hooks of lower priority are run first.
60        """
61        return self.priority < other.priority
62
63    def __call__(self):
64        """Run self.callback(**self.kwargs)."""
65        return self.callback(**self.kwargs)
66
67    def __repr__(self):
68        cls = self.__class__
69        return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)'
70                % (cls.__module__, cls.__name__, self.callback,
71                   self.failsafe, self.priority,
72                   ', '.join(['%s=%r' % (k, v)
73                              for k, v in self.kwargs.items()])))
74
75
76class HookMap(dict):
77
78    """A map of call points to lists of callbacks (Hook objects)."""
79
80    def __new__(cls, points=None):
81        d = dict.__new__(cls)
82        for p in points or []:
83            d[p] = []
84        return d
85
86    def __init__(self, *a, **kw):
87        pass
88
89    def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
90        """Append a new Hook made from the supplied arguments."""
91        self[point].append(Hook(callback, failsafe, priority, **kwargs))
92
93    def run(self, point):
94        """Execute all registered Hooks (callbacks) for the given point."""
95        self.run_hooks(iter(sorted(self[point])))
96
97    @classmethod
98    def run_hooks(cls, hooks):
99        """Execute the indicated hooks, trapping errors.
100
101        Hooks with ``.failsafe == True`` are guaranteed to run
102        even if others at the same hookpoint fail. In this case,
103        log the failure and proceed on to the next hook. The only
104        way to stop all processing from one of these hooks is
105        to raise a BaseException like SystemExit or
106        KeyboardInterrupt and stop the whole server.
107        """
108        assert isinstance(hooks, collections.abc.Iterator)
109        quiet_errors = (
110            cherrypy.HTTPError,
111            cherrypy.HTTPRedirect,
112            cherrypy.InternalRedirect,
113        )
114        safe = filter(operator.attrgetter('failsafe'), hooks)
115        for hook in hooks:
116            try:
117                hook()
118            except quiet_errors:
119                cls.run_hooks(safe)
120                raise
121            except Exception:
122                cherrypy.log(traceback=True, severity=40)
123                cls.run_hooks(safe)
124                raise
125
126    def __copy__(self):
127        newmap = self.__class__()
128        # We can't just use 'update' because we want copies of the
129        # mutable values (each is a list) as well.
130        for k, v in self.items():
131            newmap[k] = v[:]
132        return newmap
133    copy = __copy__
134
135    def __repr__(self):
136        cls = self.__class__
137        return '%s.%s(points=%r)' % (
138            cls.__module__,
139            cls.__name__,
140            list(self)
141        )
142
143
144# Config namespace handlers
145
146def hooks_namespace(k, v):
147    """Attach bare hooks declared in config."""
148    # Use split again to allow multiple hooks for a single
149    # hookpoint per path (e.g. "hooks.before_handler.1").
150    # Little-known fact you only get from reading source ;)
151    hookpoint = k.split('.', 1)[0]
152    if isinstance(v, str):
153        v = cherrypy.lib.reprconf.attributes(v)
154    if not isinstance(v, Hook):
155        v = Hook(v)
156    cherrypy.serving.request.hooks[hookpoint].append(v)
157
158
159def request_namespace(k, v):
160    """Attach request attributes declared in config."""
161    # Provides config entries to set request.body attrs (like
162    # attempt_charsets).
163    if k[:5] == 'body.':
164        setattr(cherrypy.serving.request.body, k[5:], v)
165    else:
166        setattr(cherrypy.serving.request, k, v)
167
168
169def response_namespace(k, v):
170    """Attach response attributes declared in config."""
171    # Provides config entries to set default response headers
172    # http://cherrypy.org/ticket/889
173    if k[:8] == 'headers.':
174        cherrypy.serving.response.headers[k.split('.', 1)[1]] = v
175    else:
176        setattr(cherrypy.serving.response, k, v)
177
178
179def error_page_namespace(k, v):
180    """Attach error pages declared in config."""
181    if k != 'default':
182        k = int(k)
183    cherrypy.serving.request.error_page[k] = v
184
185
186hookpoints = ['on_start_resource', 'before_request_body',
187              'before_handler', 'before_finalize',
188              'on_end_resource', 'on_end_request',
189              'before_error_response', 'after_error_response']
190
191
192class Request(object):
193
194    """An HTTP request.
195
196    This object represents the metadata of an HTTP request message;
197    that is, it contains attributes which describe the environment
198    in which the request URL, headers, and body were sent (if you
199    want tools to interpret the headers and body, those are elsewhere,
200    mostly in Tools). This 'metadata' consists of socket data,
201    transport characteristics, and the Request-Line. This object
202    also contains data regarding the configuration in effect for
203    the given URL, and the execution plan for generating a response.
204    """
205
206    prev = None
207    """
208    The previous Request object (if any). This should be None
209    unless we are processing an InternalRedirect."""
210
211    # Conversation/connection attributes
212    local = httputil.Host('127.0.0.1', 80)
213    'An httputil.Host(ip, port, hostname) object for the server socket.'
214
215    remote = httputil.Host('127.0.0.1', 1111)
216    'An httputil.Host(ip, port, hostname) object for the client socket.'
217
218    scheme = 'http'
219    """
220    The protocol used between client and server. In most cases,
221    this will be either 'http' or 'https'."""
222
223    server_protocol = 'HTTP/1.1'
224    """
225    The HTTP version for which the HTTP server is at least
226    conditionally compliant."""
227
228    base = ''
229    """The (scheme://host) portion of the requested URL.
230    In some cases (e.g. when proxying via mod_rewrite), this may contain
231    path segments which cherrypy.url uses when constructing url's, but
232    which otherwise are ignored by CherryPy. Regardless, this value
233    MUST NOT end in a slash."""
234
235    # Request-Line attributes
236    request_line = ''
237    """
238    The complete Request-Line received from the client. This is a
239    single string consisting of the request method, URI, and protocol
240    version (joined by spaces). Any final CRLF is removed."""
241
242    method = 'GET'
243    """
244    Indicates the HTTP method to be performed on the resource identified
245    by the Request-URI. Common methods include GET, HEAD, POST, PUT, and
246    DELETE. CherryPy allows any extension method; however, various HTTP
247    servers and gateways may restrict the set of allowable methods.
248    CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
249
250    query_string = ''
251    """
252    The query component of the Request-URI, a string of information to be
253    interpreted by the resource. The query portion of a URI follows the
254    path component, and is separated by a '?'. For example, the URI
255    'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
256    'a=3&b=4'."""
257
258    query_string_encoding = 'utf8'
259    """
260    The encoding expected for query string arguments after % HEX HEX decoding).
261    If a query string is provided that cannot be decoded with this encoding,
262    404 is raised (since technically it's a different URI). If you want
263    arbitrary encodings to not error, set this to 'Latin-1'; you can then
264    encode back to bytes and re-decode to whatever encoding you like later.
265    """
266
267    protocol = (1, 1)
268    """The HTTP protocol version corresponding to the set
269    of features which should be allowed in the response. If BOTH
270    the client's request message AND the server's level of HTTP
271    compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
272    If either is 1.0, this attribute will be the tuple (1, 0).
273    Lower HTTP protocol versions are not explicitly supported."""
274
275    params = {}
276    """
277    A dict which combines query string (GET) and request entity (POST)
278    variables. This is populated in two stages: GET params are added
279    before the 'on_start_resource' hook, and POST params are added
280    between the 'before_request_body' and 'before_handler' hooks."""
281
282    # Message attributes
283    header_list = []
284    """
285    A list of the HTTP request headers as (name, value) tuples.
286    In general, you should use request.headers (a dict) instead."""
287
288    headers = httputil.HeaderMap()
289    """
290    A dict-like object containing the request headers. Keys are header
291    names (in Title-Case format); however, you may get and set them in
292    a case-insensitive manner. That is, headers['Content-Type'] and
293    headers['content-type'] refer to the same value. Values are header
294    values (decoded according to :rfc:`2047` if necessary). See also:
295    httputil.HeaderMap, httputil.HeaderElement."""
296
297    cookie = SimpleCookie()
298    """See help(Cookie)."""
299
300    rfile = None
301    """
302    If the request included an entity (body), it will be available
303    as a stream in this attribute. However, the rfile will normally
304    be read for you between the 'before_request_body' hook and the
305    'before_handler' hook, and the resulting string is placed into
306    either request.params or the request.body attribute.
307
308    You may disable the automatic consumption of the rfile by setting
309    request.process_request_body to False, either in config for the desired
310    path, or in an 'on_start_resource' or 'before_request_body' hook.
311
312    WARNING: In almost every case, you should not attempt to read from the
313    rfile stream after CherryPy's automatic mechanism has read it. If you
314    turn off the automatic parsing of rfile, you should read exactly the
315    number of bytes specified in request.headers['Content-Length'].
316    Ignoring either of these warnings may result in a hung request thread
317    or in corruption of the next (pipelined) request.
318    """
319
320    process_request_body = True
321    """
322    If True, the rfile (if any) is automatically read and parsed,
323    and the result placed into request.params or request.body."""
324
325    methods_with_bodies = ('POST', 'PUT', 'PATCH')
326    """
327    A sequence of HTTP methods for which CherryPy will automatically
328    attempt to read a body from the rfile. If you are going to change
329    this property, modify it on the configuration (recommended)
330    or on the "hook point" `on_start_resource`.
331    """
332
333    body = None
334    """
335    If the request Content-Type is 'application/x-www-form-urlencoded'
336    or multipart, this will be None. Otherwise, this will be an instance
337    of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you
338    can .read()); this value is set between the 'before_request_body' and
339    'before_handler' hooks (assuming that process_request_body is True)."""
340
341    # Dispatch attributes
342    dispatch = cherrypy.dispatch.Dispatcher()
343    """
344    The object which looks up the 'page handler' callable and collects
345    config for the current request based on the path_info, other
346    request attributes, and the application architecture. The core
347    calls the dispatcher as early as possible, passing it a 'path_info'
348    argument.
349
350    The default dispatcher discovers the page handler by matching path_info
351    to a hierarchical arrangement of objects, starting at request.app.root.
352    See help(cherrypy.dispatch) for more information."""
353
354    script_name = ''
355    """
356    The 'mount point' of the application which is handling this request.
357
358    This attribute MUST NOT end in a slash. If the script_name refers to
359    the root of the URI, it MUST be an empty string (not "/").
360    """
361
362    path_info = '/'
363    """
364    The 'relative path' portion of the Request-URI. This is relative
365    to the script_name ('mount point') of the application which is
366    handling this request."""
367
368    login = None
369    """
370    When authentication is used during the request processing this is
371    set to 'False' if it failed and to the 'username' value if it succeeded.
372    The default 'None' implies that no authentication happened."""
373
374    # Note that cherrypy.url uses "if request.app:" to determine whether
375    # the call is during a real HTTP request or not. So leave this None.
376    app = None
377    """The cherrypy.Application object which is handling this request."""
378
379    handler = None
380    """
381    The function, method, or other callable which CherryPy will call to
382    produce the response. The discovery of the handler and the arguments
383    it will receive are determined by the request.dispatch object.
384    By default, the handler is discovered by walking a tree of objects
385    starting at request.app.root, and is then passed all HTTP params
386    (from the query string and POST body) as keyword arguments."""
387
388    toolmaps = {}
389    """
390    A nested dict of all Toolboxes and Tools in effect for this request,
391    of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
392
393    config = None
394    """
395    A flat dict of all configuration entries which apply to the
396    current request. These entries are collected from global config,
397    application config (based on request.path_info), and from handler
398    config (exactly how is governed by the request.dispatch object in
399    effect for this request; by default, handler config can be attached
400    anywhere in the tree between request.app.root and the final handler,
401    and inherits downward)."""
402
403    is_index = None
404    """
405    This will be True if the current request is mapped to an 'index'
406    resource handler (also, a 'default' handler if path_info ends with
407    a slash). The value may be used to automatically redirect the
408    user-agent to a 'more canonical' URL which either adds or removes
409    the trailing slash. See cherrypy.tools.trailing_slash."""
410
411    hooks = HookMap(hookpoints)
412    """
413    A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
414    Each key is a str naming the hook point, and each value is a list
415    of hooks which will be called at that hook point during this request.
416    The list of hooks is generally populated as early as possible (mostly
417    from Tools specified in config), but may be extended at any time.
418    See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
419
420    error_response = cherrypy.HTTPError(500).set_response
421    """
422    The no-arg callable which will handle unexpected, untrapped errors
423    during request processing. This is not used for expected exceptions
424    (like NotFound, HTTPError, or HTTPRedirect) which are raised in
425    response to expected conditions (those should be customized either
426    via request.error_page or by overriding HTTPError.set_response).
427    By default, error_response uses HTTPError(500) to return a generic
428    error response to the user-agent."""
429
430    error_page = {}
431    """
432    A dict of {error code: response filename or callable} pairs.
433
434    The error code must be an int representing a given HTTP error code,
435    or the string 'default', which will be used if no matching entry
436    is found for a given numeric code.
437
438    If a filename is provided, the file should contain a Python string-
439    formatting template, and can expect by default to receive format
440    values with the mapping keys %(status)s, %(message)s, %(traceback)s,
441    and %(version)s. The set of format mappings can be extended by
442    overriding HTTPError.set_response.
443
444    If a callable is provided, it will be called by default with keyword
445    arguments 'status', 'message', 'traceback', and 'version', as for a
446    string-formatting template. The callable must return a string or
447    iterable of strings which will be set to response.body. It may also
448    override headers or perform any other processing.
449
450    If no entry is given for an error code, and no 'default' entry exists,
451    a default template will be used.
452    """
453
454    show_tracebacks = True
455    """
456    If True, unexpected errors encountered during request processing will
457    include a traceback in the response body."""
458
459    show_mismatched_params = True
460    """
461    If True, mismatched parameters encountered during PageHandler invocation
462    processing will be included in the response body."""
463
464    throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
465    """The sequence of exceptions which Request.run does not trap."""
466
467    throw_errors = False
468    """
469    If True, Request.run will not trap any errors (except HTTPRedirect and
470    HTTPError, which are more properly called 'exceptions', not errors)."""
471
472    closed = False
473    """True once the close method has been called, False otherwise."""
474
475    stage = None
476    """
477    A string containing the stage reached in the request-handling process.
478    This is useful when debugging a live server with hung requests."""
479
480    unique_id = None
481    """A lazy object generating and memorizing UUID4 on ``str()`` render."""
482
483    namespaces = reprconf.NamespaceSet(
484        **{'hooks': hooks_namespace,
485           'request': request_namespace,
486           'response': response_namespace,
487           'error_page': error_page_namespace,
488           'tools': cherrypy.tools,
489           })
490
491    def __init__(self, local_host, remote_host, scheme='http',
492                 server_protocol='HTTP/1.1'):
493        """Populate a new Request object.
494
495        local_host should be an httputil.Host object with the server info.
496        remote_host should be an httputil.Host object with the client info.
497        scheme should be a string, either "http" or "https".
498        """
499        self.local = local_host
500        self.remote = remote_host
501        self.scheme = scheme
502        self.server_protocol = server_protocol
503
504        self.closed = False
505
506        # Put a *copy* of the class error_page into self.
507        self.error_page = self.error_page.copy()
508
509        # Put a *copy* of the class namespaces into self.
510        self.namespaces = self.namespaces.copy()
511
512        self.stage = None
513
514        self.unique_id = LazyUUID4()
515
516    def close(self):
517        """Run cleanup code. (Core)"""
518        if not self.closed:
519            self.closed = True
520            self.stage = 'on_end_request'
521            self.hooks.run('on_end_request')
522            self.stage = 'close'
523
524    def run(self, method, path, query_string, req_protocol, headers, rfile):
525        r"""Process the Request. (Core)
526
527        method, path, query_string, and req_protocol should be pulled directly
528        from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
529
530        path
531            This should be %XX-unquoted, but query_string should not be.
532
533            When using Python 2, they both MUST be byte strings,
534            not unicode strings.
535
536            When using Python 3, they both MUST be unicode strings,
537            not byte strings, and preferably not bytes \x00-\xFF
538            disguised as unicode.
539
540        headers
541            A list of (name, value) tuples.
542
543        rfile
544            A file-like object containing the HTTP request entity.
545
546        When run() is done, the returned object should have 3 attributes:
547
548          * status, e.g. "200 OK"
549          * header_list, a list of (name, value) tuples
550          * body, an iterable yielding strings
551
552        Consumer code (HTTP servers) should then access these response
553        attributes to build the outbound stream.
554
555        """
556        response = cherrypy.serving.response
557        self.stage = 'run'
558        try:
559            self.error_response = cherrypy.HTTPError(500).set_response
560
561            self.method = method
562            path = path or '/'
563            self.query_string = query_string or ''
564            self.params = {}
565
566            # Compare request and server HTTP protocol versions, in case our
567            # server does not support the requested protocol. Limit our output
568            # to min(req, server). We want the following output:
569            #     request    server     actual written   supported response
570            #     protocol   protocol  response protocol    feature set
571            # a     1.0        1.0           1.0                1.0
572            # b     1.0        1.1           1.1                1.0
573            # c     1.1        1.0           1.0                1.0
574            # d     1.1        1.1           1.1                1.1
575            # Notice that, in (b), the response will be "HTTP/1.1" even though
576            # the client only understands 1.0. RFC 2616 10.5.6 says we should
577            # only return 505 if the _major_ version is different.
578            rp = int(req_protocol[5]), int(req_protocol[7])
579            sp = int(self.server_protocol[5]), int(self.server_protocol[7])
580            self.protocol = min(rp, sp)
581            response.headers.protocol = self.protocol
582
583            # Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
584            url = path
585            if query_string:
586                url += '?' + query_string
587            self.request_line = '%s %s %s' % (method, url, req_protocol)
588
589            self.header_list = list(headers)
590            self.headers = httputil.HeaderMap()
591
592            self.rfile = rfile
593            self.body = None
594
595            self.cookie = SimpleCookie()
596            self.handler = None
597
598            # path_info should be the path from the
599            # app root (script_name) to the handler.
600            self.script_name = self.app.script_name
601            self.path_info = pi = path[len(self.script_name):]
602
603            self.stage = 'respond'
604            self.respond(pi)
605
606        except self.throws:
607            raise
608        except Exception:
609            if self.throw_errors:
610                raise
611            else:
612                # Failure in setup, error handler or finalize. Bypass them.
613                # Can't use handle_error because we may not have hooks yet.
614                cherrypy.log(traceback=True, severity=40)
615                if self.show_tracebacks:
616                    body = format_exc()
617                else:
618                    body = ''
619                r = bare_error(body)
620                response.output_status, response.header_list, response.body = r
621
622        if self.method == 'HEAD':
623            # HEAD requests MUST NOT return a message-body in the response.
624            response.body = []
625
626        try:
627            cherrypy.log.access()
628        except Exception:
629            cherrypy.log.error(traceback=True)
630
631        return response
632
633    def respond(self, path_info):
634        """Generate a response for the resource at self.path_info. (Core)"""
635        try:
636            try:
637                try:
638                    self._do_respond(path_info)
639                except (cherrypy.HTTPRedirect, cherrypy.HTTPError):
640                    inst = sys.exc_info()[1]
641                    inst.set_response()
642                    self.stage = 'before_finalize (HTTPError)'
643                    self.hooks.run('before_finalize')
644                    cherrypy.serving.response.finalize()
645            finally:
646                self.stage = 'on_end_resource'
647                self.hooks.run('on_end_resource')
648        except self.throws:
649            raise
650        except Exception:
651            if self.throw_errors:
652                raise
653            self.handle_error()
654
655    def _do_respond(self, path_info):
656        response = cherrypy.serving.response
657
658        if self.app is None:
659            raise cherrypy.NotFound()
660
661        self.hooks = self.__class__.hooks.copy()
662        self.toolmaps = {}
663
664        # Get the 'Host' header, so we can HTTPRedirect properly.
665        self.stage = 'process_headers'
666        self.process_headers()
667
668        self.stage = 'get_resource'
669        self.get_resource(path_info)
670
671        self.body = _cpreqbody.RequestBody(
672            self.rfile, self.headers, request_params=self.params)
673
674        self.namespaces(self.config)
675
676        self.stage = 'on_start_resource'
677        self.hooks.run('on_start_resource')
678
679        # Parse the querystring
680        self.stage = 'process_query_string'
681        self.process_query_string()
682
683        # Process the body
684        if self.process_request_body:
685            if self.method not in self.methods_with_bodies:
686                self.process_request_body = False
687        self.stage = 'before_request_body'
688        self.hooks.run('before_request_body')
689        if self.process_request_body:
690            self.body.process()
691
692        # Run the handler
693        self.stage = 'before_handler'
694        self.hooks.run('before_handler')
695        if self.handler:
696            self.stage = 'handler'
697            response.body = self.handler()
698
699        # Finalize
700        self.stage = 'before_finalize'
701        self.hooks.run('before_finalize')
702        response.finalize()
703
704    def process_query_string(self):
705        """Parse the query string into Python structures. (Core)"""
706        try:
707            p = httputil.parse_query_string(
708                self.query_string, encoding=self.query_string_encoding)
709        except UnicodeDecodeError:
710            raise cherrypy.HTTPError(
711                404, 'The given query string could not be processed. Query '
712                'strings for this resource must be encoded with %r.' %
713                self.query_string_encoding)
714
715        self.params.update(p)
716
717    def process_headers(self):
718        """Parse HTTP header data into Python structures. (Core)"""
719        # Process the headers into self.headers
720        headers = self.headers
721        for name, value in self.header_list:
722            # Call title() now (and use dict.__method__(headers))
723            # so title doesn't have to be called twice.
724            name = name.title()
725            value = value.strip()
726
727            headers[name] = httputil.decode_TEXT_maybe(value)
728
729            # Some clients, notably Konquoror, supply multiple
730            # cookies on different lines with the same key. To
731            # handle this case, store all cookies in self.cookie.
732            if name == 'Cookie':
733                try:
734                    self.cookie.load(value)
735                except CookieError as exc:
736                    raise cherrypy.HTTPError(400, str(exc))
737
738        if not dict.__contains__(headers, 'Host'):
739            # All Internet-based HTTP/1.1 servers MUST respond with a 400
740            # (Bad Request) status code to any HTTP/1.1 request message
741            # which lacks a Host header field.
742            if self.protocol >= (1, 1):
743                msg = "HTTP/1.1 requires a 'Host' request header."
744                raise cherrypy.HTTPError(400, msg)
745        host = dict.get(headers, 'Host')
746        if not host:
747            host = self.local.name or self.local.ip
748        self.base = '%s://%s' % (self.scheme, host)
749
750    def get_resource(self, path):
751        """Call a dispatcher (which sets self.handler and .config). (Core)"""
752        # First, see if there is a custom dispatch at this URI. Custom
753        # dispatchers can only be specified in app.config, not in _cp_config
754        # (since custom dispatchers may not even have an app.root).
755        dispatch = self.app.find_config(
756            path, 'request.dispatch', self.dispatch)
757
758        # dispatch() should set self.handler and self.config
759        dispatch(path)
760
761    def handle_error(self):
762        """Handle the last unanticipated exception. (Core)"""
763        try:
764            self.hooks.run('before_error_response')
765            if self.error_response:
766                self.error_response()
767            self.hooks.run('after_error_response')
768            cherrypy.serving.response.finalize()
769        except cherrypy.HTTPRedirect:
770            inst = sys.exc_info()[1]
771            inst.set_response()
772            cherrypy.serving.response.finalize()
773
774
775class ResponseBody(object):
776
777    """The body of the HTTP response (the response entity)."""
778
779    unicode_err = ('Page handlers MUST return bytes. Use tools.encode '
780                   'if you wish to return unicode.')
781
782    def __get__(self, obj, objclass=None):
783        if obj is None:
784            # When calling on the class instead of an instance...
785            return self
786        else:
787            return obj._body
788
789    def __set__(self, obj, value):
790        # Convert the given value to an iterable object.
791        if isinstance(value, str):
792            raise ValueError(self.unicode_err)
793        elif isinstance(value, list):
794            # every item in a list must be bytes...
795            if any(isinstance(item, str) for item in value):
796                raise ValueError(self.unicode_err)
797
798        obj._body = encoding.prepare_iter(value)
799
800
801class Response(object):
802
803    """An HTTP Response, including status, headers, and body."""
804
805    status = ''
806    """The HTTP Status-Code and Reason-Phrase."""
807
808    header_list = []
809    """
810    A list of the HTTP response headers as (name, value) tuples.
811    In general, you should use response.headers (a dict) instead. This
812    attribute is generated from response.headers and is not valid until
813    after the finalize phase."""
814
815    headers = httputil.HeaderMap()
816    """
817    A dict-like object containing the response headers. Keys are header
818    names (in Title-Case format); however, you may get and set them in
819    a case-insensitive manner. That is, headers['Content-Type'] and
820    headers['content-type'] refer to the same value. Values are header
821    values (decoded according to :rfc:`2047` if necessary).
822
823    .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement`
824    """
825
826    cookie = SimpleCookie()
827    """See help(Cookie)."""
828
829    body = ResponseBody()
830    """The body (entity) of the HTTP response."""
831
832    time = None
833    """The value of time.time() when created. Use in HTTP dates."""
834
835    stream = False
836    """If False, buffer the response body."""
837
838    def __init__(self):
839        self.status = None
840        self.header_list = None
841        self._body = []
842        self.time = time.time()
843
844        self.headers = httputil.HeaderMap()
845        # Since we know all our keys are titled strings, we can
846        # bypass HeaderMap.update and get a big speed boost.
847        dict.update(self.headers, {
848            'Content-Type': 'text/html',
849            'Server': 'CherryPy/' + cherrypy.__version__,
850            'Date': httputil.HTTPDate(self.time),
851        })
852        self.cookie = SimpleCookie()
853
854    def collapse_body(self):
855        """Collapse self.body to a single string; replace it and return it."""
856        new_body = b''.join(self.body)
857        self.body = new_body
858        return new_body
859
860    def _flush_body(self):
861        """
862        Discard self.body but consume any generator such that
863        any finalization can occur, such as is required by
864        caching.tee_output().
865        """
866        consume(iter(self.body))
867
868    def finalize(self):
869        """Transform headers (and cookies) into self.header_list. (Core)"""
870        try:
871            code, reason, _ = httputil.valid_status(self.status)
872        except ValueError:
873            raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
874
875        headers = self.headers
876
877        self.status = '%s %s' % (code, reason)
878        self.output_status = ntob(str(code), 'ascii') + \
879            b' ' + headers.encode(reason)
880
881        if self.stream:
882            # The upshot: wsgiserver will chunk the response if
883            # you pop Content-Length (or set it explicitly to None).
884            # Note that lib.static sets C-L to the file's st_size.
885            if dict.get(headers, 'Content-Length') is None:
886                dict.pop(headers, 'Content-Length', None)
887        elif code < 200 or code in (204, 205, 304):
888            # "All 1xx (informational), 204 (no content),
889            # and 304 (not modified) responses MUST NOT
890            # include a message-body."
891            dict.pop(headers, 'Content-Length', None)
892            self._flush_body()
893            self.body = b''
894        else:
895            # Responses which are not streamed should have a Content-Length,
896            # but allow user code to set Content-Length if desired.
897            if dict.get(headers, 'Content-Length') is None:
898                content = self.collapse_body()
899                dict.__setitem__(headers, 'Content-Length', len(content))
900
901        # Transform our header dict into a list of tuples.
902        self.header_list = h = headers.output()
903
904        cookie = self.cookie.output()
905        if cookie:
906            for line in cookie.split('\r\n'):
907                name, value = line.split(': ', 1)
908                if isinstance(name, str):
909                    name = name.encode('ISO-8859-1')
910                if isinstance(value, str):
911                    value = headers.encode(value)
912                h.append((name, value))
913
914
915class LazyUUID4(object):
916    def __str__(self):
917        """Return UUID4 and keep it for future calls."""
918        return str(self.uuid4)
919
920    @property
921    def uuid4(self):
922        """Provide unique id on per-request basis using UUID4.
923
924        It's evaluated lazily on render.
925        """
926        try:
927            self._uuid4
928        except AttributeError:
929            # evaluate on first access
930            self._uuid4 = uuid.uuid4()
931
932        return self._uuid4
933