Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/trunk/cherrypy/_cprequest.py

Revision 1949 (checked in by fumanchu, 3 weeks ago)

Fix for #800 (ability to override default error template). Many thanks to Scott Chapman for the ideas and Nicolas Grilly for the fix.

  • Property svn:eol-style set to native
Line 
1
2 import Cookie
3 import os
4 import sys
5 import time
6 import types
7
8 import cherrypy
9 from cherrypy import _cpcgifs, _cpconfig
10 from cherrypy._cperror import format_exc, bare_error
11 from cherrypy.lib import http
12
13
14 class Hook(object):
15     """A callback and its metadata: failsafe, priority, and kwargs."""
16    
17     __metaclass__ = cherrypy._AttributeDocstrings
18    
19     callback = None
20     callback__doc = """
21     The bare callable that this Hook object is wrapping, which will
22     be called when the Hook is called."""
23    
24     failsafe = False
25     failsafe__doc = """
26     If True, the callback is guaranteed to run even if other callbacks
27     from the same call point raise exceptions."""
28    
29     priority = 50
30     priority__doc = """
31     Defines the order of execution for a list of Hooks. Priority numbers
32     should be limited to the closed interval [0, 100], but values outside
33     this range are acceptable, as are fractional values."""
34    
35     kwargs = {}
36     kwargs__doc = """
37     A set of keyword arguments that will be passed to the
38     callable on each call."""
39    
40     def __init__(self, callback, failsafe=None, priority=None, **kwargs):
41         self.callback = callback
42        
43         if failsafe is None:
44             failsafe = getattr(callback, "failsafe", False)
45         self.failsafe = failsafe
46        
47         if priority is None:
48             priority = getattr(callback, "priority", 50)
49         self.priority = priority
50        
51         self.kwargs = kwargs
52    
53     def __cmp__(self, other):
54         return cmp(self.priority, other.priority)
55    
56     def __call__(self):
57         """Run self.callback(**self.kwargs)."""
58         return self.callback(**self.kwargs)
59    
60     def __repr__(self):
61         cls = self.__class__
62         return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)"
63                 % (cls.__module__, cls.__name__, self.callback,
64                    self.failsafe, self.priority,
65                    ", ".join(['%s=%r' % (k, v)
66                               for k, v in self.kwargs.iteritems()])))
67
68
69 class HookMap(dict):
70     """A map of call points to lists of callbacks (Hook objects)."""
71    
72     def __new__(cls, points=None):
73         d = dict.__new__(cls)
74         for p in points or []:
75             d[p] = []
76         return d
77    
78     def __init__(self, *a, **kw):
79         pass
80    
81     def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
82         """Append a new Hook made from the supplied arguments."""
83         self[point].append(Hook(callback, failsafe, priority, **kwargs))
84    
85     def run(self, point):
86         """Execute all registered Hooks (callbacks) for the given point."""
87         exc = None
88         hooks = self[point]
89         hooks.sort()
90         for hook in hooks:
91             # Some hooks are guaranteed to run even if others at
92             # the same hookpoint fail. We will still log the failure,
93             # but proceed on to the next hook. The only way
94             # to stop all processing from one of these hooks is
95             # to raise SystemExit and stop the whole server.
96             if exc is None or hook.failsafe:
97                 try:
98                     hook()
99                 except (KeyboardInterrupt, SystemExit):
100                     raise
101                 except (cherrypy.HTTPError, cherrypy.HTTPRedirect,
102                         cherrypy.InternalRedirect):
103                     exc = sys.exc_info()[1]
104                 except:
105                     exc = sys.exc_info()[1]
106                     cherrypy.log(traceback=True, severity=40)
107         if exc:
108             raise
109    
110     def __copy__(self):
111         newmap = self.__class__()
112         # We can't just use 'update' because we want copies of the
113         # mutable values (each is a list) as well.
114         for k, v in self.iteritems():
115             newmap[k] = v[:]
116         return newmap
117     copy = __copy__
118    
119     def __repr__(self):
120         cls = self.__class__
121         return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, self.keys())
122
123
124 # Config namespace handlers
125
126 def hooks_namespace(k, v):
127     """Attach bare hooks declared in config."""
128     # Use split again to allow multiple hooks for a single
129     # hookpoint per path (e.g. "hooks.before_handler.1").
130     # Little-known fact you only get from reading source ;)
131     hookpoint = k.split(".", 1)[0]
132     if isinstance(v, basestring):
133         v = cherrypy.lib.attributes(v)
134     if not isinstance(v, Hook):
135         v = Hook(v)
136     cherrypy.request.hooks[hookpoint].append(v)
137
138 def request_namespace(k, v):
139     """Attach request attributes declared in config."""
140     setattr(cherrypy.request, k, v)
141
142 def response_namespace(k, v):
143     """Attach response attributes declared in config."""
144     setattr(cherrypy.response, k, v)
145
146 def error_page_namespace(k, v):
147     """Attach error pages declared in config."""
148     if k != 'default':
149         k = int(k)
150     cherrypy.request.error_page[k] = v
151
152
153 hookpoints = ['on_start_resource', 'before_request_body',
154               'before_handler', 'before_finalize',
155               'on_end_resource', 'on_end_request',
156               'before_error_response', 'after_error_response']
157
158
159 class Request(object):
160     """An HTTP request.
161     
162     This object represents the metadata of an HTTP request message;
163     that is, it contains attributes which describe the environment
164     in which the request URL, headers, and body were sent (if you
165     want tools to interpret the headers and body, those are elsewhere,
166     mostly in Tools). This 'metadata' consists of socket data,
167     transport characteristics, and the Request-Line. This object
168     also contains data regarding the configuration in effect for
169     the given URL, and the execution plan for generating a response.
170     """
171    
172     __metaclass__ = cherrypy._AttributeDocstrings
173    
174     prev = None
175     prev__doc = """
176     The previous Request object (if any). This should be None
177     unless we are processing an InternalRedirect."""
178    
179     # Conversation/connection attributes
180     local = http.Host("127.0.0.1", 80)
181     local__doc = \
182         "An http.Host(ip, port, hostname) object for the server socket."
183    
184     remote = http.Host("127.0.0.1", 1111)
185     remote__doc = \
186         "An http.Host(ip, port, hostname) object for the client socket."
187    
188     scheme = "http"
189     scheme__doc = """
190     The protocol used between client and server. In most cases,
191     this will be either 'http' or 'https'."""
192    
193     server_protocol = "HTTP/1.1"
194     server_protocol__doc = """
195     The HTTP version for which the HTTP server is at least
196     conditionally compliant."""
197    
198     base = ""
199     base__doc = """The (scheme://host) portion of the requested URL."""
200    
201     # Request-Line attributes
202     request_line = ""
203     request_line__doc = """
204     The complete Request-Line received from the client. This is a
205     single string consisting of the request method, URI, and protocol
206     version (joined by spaces). Any final CRLF is removed."""
207    
208     method = "GET"
209     method__doc = """
210     Indicates the HTTP method to be performed on the resource identified
211     by the Request-URI. Common methods include GET, HEAD, POST, PUT, and
212     DELETE. CherryPy allows any extension method; however, various HTTP
213     servers and gateways may restrict the set of allowable methods.
214     CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
215    
216     query_string = ""
217     query_string__doc = """
218     The query component of the Request-URI, a string of information to be
219     interpreted by the resource. The query portion of a URI follows the
220     path component, and is separated by a '?'. For example, the URI
221     'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
222     'a=3&b=4'."""
223    
224     protocol = (1, 1)
225     protocol__doc = """The HTTP protocol version corresponding to the set
226         of features which should be allowed in the response. If BOTH
227         the client's request message AND the server's level of HTTP
228         compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
229         If either is 1.0, this attribute will be the tuple (1, 0).
230         Lower HTTP protocol versions are not explicitly supported."""
231    
232     params = {}
233     params__doc = """
234     A dict which combines query string (GET) and request entity (POST)
235     variables. This is populated in two stages: GET params are added
236     before the 'on_start_resource' hook, and POST params are added
237     between the 'before_request_body' and 'before_handler' hooks."""
238    
239     # Message attributes
240     header_list = []
241     header_list__doc = """
242     A list of the HTTP request headers as (name, value) tuples.
243     In general, you should use request.headers (a dict) instead."""
244    
245     headers = http.HeaderMap()
246     headers__doc = """
247     A dict-like object containing the request headers. Keys are header
248     names (in Title-Case format); however, you may get and set them in
249     a case-insensitive manner. That is, headers['Content-Type'] and
250     headers['content-type'] refer to the same value. Values are header
251     values (decoded according to RFC 2047 if necessary). See also:
252     http.HeaderMap, http.HeaderElement."""
253    
254     cookie = Cookie.SimpleCookie()
255     cookie__doc = """See help(Cookie)."""
256    
257     rfile = None
258     rfile__doc = """
259     If the request included an entity (body), it will be available
260     as a stream in this attribute. However, the rfile will normally
261     be read for you between the 'before_request_body' hook and the
262     'before_handler' hook, and the resulting string is placed into
263     either request.params or the request.body attribute.
264     
265     You may disable the automatic consumption of the rfile by setting
266     request.process_request_body to False, either in config for the desired
267     path, or in an 'on_start_resource' or 'before_request_body' hook.
268     
269     WARNING: In almost every case, you should not attempt to read from the
270     rfile stream after CherryPy's automatic mechanism has read it. If you
271     turn off the automatic parsing of rfile, you should read exactly the
272     number of bytes specified in request.headers['Content-Length'].
273     Ignoring either of these warnings may result in a hung request thread
274     or in corruption of the next (pipelined) request.
275     """
276    
277     process_request_body = True
278     process_request_body__doc = """
279     If True, the rfile (if any) is automatically read and parsed,
280     and the result placed into request.params or request.body."""
281    
282     methods_with_bodies = ("POST", "PUT")
283     methods_with_bodies__doc = """
284     A sequence of HTTP methods for which CherryPy will automatically
285     attempt to read a body from the rfile."""
286    
287     body = None
288     body__doc = """
289     If the request Content-Type is 'application/x-www-form-urlencoded'
290     or multipart, this will be None. Otherwise, this will contain the
291     request entity body as a string; this value is set between the
292     'before_request_body' and 'before_handler' hooks (assuming that
293     process_request_body is True)."""
294    
295     body_params = None
296     body_params__doc = """
297     If the request Content-Type is 'application/x-www-form-urlencoded' or
298     multipart, this will be a dict of the params pulled from the entity
299     body; that is, it will be the portion of request.params that come
300     from the message body (sometimes called "POST params", although they
301     can be sent with various HTTP method verbs). This value is set between
302     the 'before_request_body' and 'before_handler' hooks (assuming that
303     process_request_body is True)."""
304    
305     # Dispatch attributes
306     dispatch = cherrypy.dispatch.Dispatcher()
307     dispatch__doc = """
308     The object which looks up the 'page handler' callable and collects
309     config for the current request based on the path_info, other
310     request attributes, and the application architecture. The core
311     calls the dispatcher as early as possible, passing it a 'path_info'
312     argument.
313     
314     The default dispatcher discovers the page handler by matching path_info
315     to a hierarchical arrangement of objects, starting at request.app.root.
316     See help(cherrypy.dispatch) for more information."""
317    
318     script_name = ""
319     script_name__doc = """
320     The 'mount point' of the application which is handling this request.
321     
322     This attribute MUST NOT end in a slash. If the script_name refers to
323     the root of the URI, it MUST be an empty string (not "/").
324     """
325    
326     path_info = "/"
327     path_info__doc = """
328     The 'relative path' portion of the Request-URI. This is relative
329     to the script_name ('mount point') of the application which is
330     handling this request."""
331
332     login = None
333     login__doc = """
334     When authentication is used during the request processing this is
335     set to 'False' if it failed and to the 'username' value if it succeeded.
336     The default 'None' implies that no authentication happened."""
337    
338     # Note that cherrypy.url uses "if request.app:" to determine whether
339     # the call is during a real HTTP request or not. So leave this None.
340     app = None
341     app__doc = \
342         """The cherrypy.Application object which is handling this request."""
343    
344     handler = None
345     handler__doc = """
346     The function, method, or other callable which CherryPy will call to
347     produce the response. The discovery of the handler and the arguments
348     it will receive are determined by the request.dispatch object.
349     By default, the handler is discovered by walking a tree of objects
350     starting at request.app.root, and is then passed all HTTP params
351     (from the query string and POST body) as keyword arguments."""
352    
353     toolmaps = {}
354     toolmaps__doc = """
355     A nested dict of all Toolboxes and Tools in effect for this request,
356     of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
357    
358     config = None
359     config__doc = """
360     A flat dict of all configuration entries which apply to the
361     current request. These entries are collected from global config,
362     application config (based on request.path_info), and from handler
363     config (exactly how is governed by the request.dispatch object in
364     effect for this request; by default, handler config can be attached
365     anywhere in the tree between request.app.root and the final handler,
366     and inherits downward)."""
367    
368     is_index = None
369     is_index__doc = """
370     This will be True if the current request is mapped to an 'index'
371     resource handler (also, a 'default' handler if path_info ends with
372     a slash). The value may be used to automatically redirect the
373     user-agent to a 'more canonical' URL which either adds or removes
374     the trailing slash. See cherrypy.tools.trailing_slash."""
375    
376     hooks = HookMap(hookpoints)
377     hooks__doc = """
378     A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
379     Each key is a str naming the hook point, and each value is a list
380     of hooks which will be called at that hook point during this request.
381     The list of hooks is generally populated as early as possible (mostly
382     from Tools specified in config), but may be extended at any time.
383     See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
384    
385     error_response = cherrypy.HTTPError(500).set_response
386     error_response__doc = """
387     The no-arg callable which will handle unexpected, untrapped errors
388     during request processing. This is not used for expected exceptions
389     (like NotFound, HTTPError, or HTTPRedirect) which are raised in
390     response to expected conditions (those should be customized either
391     via request.error_page or by overriding HTTPError.set_response).
392     By default, error_response uses HTTPError(500) to return a generic
393     error response to the user-agent."""
394    
395     error_page = {}
396     error_page__doc = """
397     A dict of {error code: response filename or callable} pairs.
398     
399     The error code must be an int representing a given HTTP error code,
400     or the string 'default', which will be used if no matching entry
401     is found for a given numeric code.
402     
403     If a filename is provided, the file should contain a Python string-
404     formatting template, and can expect by default to receive format
405     values with the mapping keys %(status)s, %(message)s, %(traceback)s,
406     and %(version)s. The set of format mappings can be extended by
407     overriding HTTPError.set_response.
408     
409     If a callable is provided, it will be called by default with keyword
410     arguments 'status', 'message', 'traceback', and 'version', as for a
411     string-formatting template. The callable must return a string which
412     will be set to response.body. It may also override headers or perform
413     any other processing.
414     
415     If no entry is given for an error code, and no 'default' entry exists,
416     a default template will be used.
417     """
418    
419     show_tracebacks = True
420     show_tracebacks__doc = """
421     If True, unexpected errors encountered during request processing will
422     include a traceback in the response body."""
423    
424     throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
425     throws__doc = \
426         """The sequence of exceptions which Request.run does not trap."""
427    
428     throw_errors = False
429     throw_errors__doc = """
430     If True, Request.run will not trap any errors (except HTTPRedirect and
431     HTTPError, which are more properly called 'exceptions', not errors)."""
432    
433     closed = False
434     closed__doc = """
435     True once the close method has been called, False otherwise."""
436    
437     stage = None
438     stage__doc = """
439     A string containing the stage reached in the request-handling process.
440     This is useful when debugging a live server with hung requests."""
441    
442     namespaces = _cpconfig.NamespaceSet(
443         **{"hooks": hooks_namespace,
444            "request": request_namespace,
445            "response": response_namespace,
446            "error_page": error_page_namespace,
447            "tools": cherrypy.tools,
448            })
449    
450     def __init__(self, local_host, remote_host, scheme="http",
451                  server_protocol="HTTP/1.1"):
452         """Populate a new Request object.
453         
454         local_host should be an http.Host object with the server info.
455         remote_host should be an http.Host object with the client info.
456         scheme should be a string, either "http" or "https".
457         """
458         self.local = local_host
459         self.remote = remote_host
460         self.scheme = scheme
461         self.server_protocol = server_protocol
462        
463         self.closed = False
464        
465         # Put a *copy* of the class error_page into self.
466         self.error_page = self.error_page.copy()
467        
468         # Put a *copy* of the class namespaces into self.
469         self.namespaces = self.namespaces.copy()
470        
471         self.stage = None
472    
473     def close(self):
474         """Run cleanup code. (Core)"""
475         if not self.closed:
476             self.closed = True
477             self.stage = 'on_end_request'
478             self.hooks.run('on_end_request')
479             self.stage = 'close'
480    
481     def run(self, method, path, query_string, req_protocol, headers, rfile):
482         """Process the Request. (Core)
483         
484         method, path, query_string, and req_protocol should be pulled directly
485             from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
486         path should be %XX-unquoted, but query_string should not be.
487         headers should be a list of (name, value) tuples.
488         rfile should be a file-like object containing the HTTP request entity.
489         
490         When run() is done, the returned object should have 3 attributes:
491           status, e.g. "200 OK"
492           header_list, a list of (name, value) tuples
493           body, an iterable yielding strings
494         
495         Consumer code (HTTP servers) should then access these response
496         attributes to build the outbound stream.
497         
498         """
499         self.stage = 'run'
500         try:
501             self.error_response = cherrypy.HTTPError(500).set_response
502            
503             self.method = method
504             path = path or "/"
505             self.query_string = query_string or ''
506            
507             # Compare request and server HTTP protocol versions, in case our
508             # server does not support the requested protocol. Limit our output
509             # to min(req, server). We want the following output:
510             #     request    server     actual written   supported response
511             #     protocol   protocol  response protocol    feature set
512             # a     1.0        1.0           1.0                1.0
513             # b     1.0        1.1           1.1                1.0
514             # c     1.1        1.0           1.0                1.0
515             # d     1.1        1.1           1.1                1.1
516             # Notice that, in (b), the response will be "HTTP/1.1" even though
517             # the client only understands 1.0. RFC 2616 10.5.6 says we should
518             # only return 505 if the _major_ version is different.
519             rp = int(req_protocol[5]), int(req_protocol[7])
520             sp = int(self.server_protocol[5]), int(self.server_protocol[7])
521             self.protocol = min(rp, sp)
522            
523             # Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
524             url = path
525             if query_string:
526                 url += '?' + query_string
527             self.request_line = '%s %s %s' % (method, url, req_protocol)
528            
529             self.header_list = list(headers)
530             self.rfile = rfile
531             self.headers = http.HeaderMap()
532             self.cookie = Cookie.SimpleCookie()
533             self.handler = None
534            
535             # path_info should be the path from the
536             # app root (script_name) to the handler.
537             self.script_name = self.app.script_name
538             self.path_info = pi = path[len(self.script_name):]
539            
540             self.stage = 'respond'
541             self.respond(pi)
542            
543         except self.throws:
544             raise
545         except:
546             if self.throw_errors:
547                 raise
548             else:
549                 # Failure in setup, error handler or finalize. Bypass them.
550                 # Can't use handle_error because we may not have hooks yet.
551                 cherrypy.log(traceback=True, severity=40)
552                 if self.show_tracebacks:
553                     body = format_exc()
554                 else:
555                     body = ""
556                 r = bare_error(body)
557                 response = cherrypy.response
558                 response.status, response.header_list, response.body = r
559        
560         if self.method == "HEAD":
561             # HEAD requests MUST NOT return a message-body in the response.
562             cherrypy.response.body = []
563        
564         cherrypy.log.access()
565        
566         if cherrypy.response.timed_out:
567             raise cherrypy.TimeoutError()
568        
569         return cherrypy.response
570    
571     def respond(self, path_info):
572         """Generate a response for the resource at self.path_info. (Core)"""
573         try:
574             try:
575                 try:
576                     if self.app is None:
577                         raise cherrypy.NotFound()
578                    
579                     # Get the 'Host' header, so we can HTTPRedirect properly.
580                     self.stage = 'process_headers'
581                     self.process_headers()
582                    
583                     # Make a copy of the class hooks
584                     self.hooks = self.__class__.hooks.copy()
585                     self.toolmaps = {}
586                     self.stage = 'get_resource'
587                     self.get_resource(path_info)
588                     self.namespaces(self.config)
589                    
590                     self.stage = 'on_start_resource'
591                     self.hooks.run('on_start_resource')
592                    
593                     if self.process_request_body:
594                         if self.method not in self.methods_with_bodies:
595                             self.process_request_body = False
596                    
597                     self.stage = 'before_request_body'
598                     self