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

root/tags/cherrypy-3.0.0/cherrypy/_cprequest.py

Revision 1470 (checked in by fumanchu, 2 years ago)

Fix for #614 (VirtualHost? and staticdir tool still don't play well together).

  • Property svn:eol-style set to native
Line 
1 """CherryPy core request/response handling."""
2
3 import Cookie
4 import os
5 import sys
6 import time
7 import types
8
9 import cherrypy
10 from cherrypy import _cpcgifs
11 from cherrypy._cperror import format_exc, bare_error
12 from cherrypy.lib import http
13
14
15 class Hook(object):
16     """A callback and its metadata: failsafe, priority, and kwargs.
17     
18     failsafe: If True, the callback is guaranteed to run even if other
19         callbacks from the same call point raise exceptions.
20     priority: Defines the order of execution for a list of Hooks.
21         Defaults to 50. Priority numbers should be limited to the
22         closed interval [0, 100], but values outside this range are
23         acceptable, as are fractional values.
24     """
25    
26     def __init__(self, callback, failsafe=None, priority=None, **kwargs):
27         self.callback = callback
28        
29         if failsafe is None:
30             failsafe = getattr(callback, "failsafe", False)
31         self.failsafe = failsafe
32        
33         if priority is None:
34             priority = getattr(callback, "priority", 50)
35         self.priority = priority
36        
37         self.kwargs = kwargs
38    
39     def __cmp__(self, other):
40         return cmp(self.priority, other.priority)
41    
42     def __call__(self):
43         """Run self.callback(**self.kwargs)."""
44         return self.callback(**self.kwargs)
45
46
47 class HookMap(dict):
48     """A map of call points to lists of callbacks (Hook objects)."""
49    
50     def __new__(cls, points=None):
51         d = dict.__new__(cls)
52         for p in points or []:
53             d[p] = []
54         return d
55    
56     def __init__(self, *a, **kw):
57         pass
58    
59     def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
60         """Append a new Hook made from the supplied arguments."""
61         self[point].append(Hook(callback, failsafe, priority, **kwargs))
62    
63     def run(self, point):
64         """Execute all registered Hooks (callbacks) for the given point."""
65         exc = None
66         hooks = self[point]
67         hooks.sort()
68         for hook in hooks:
69             # Some hooks are guaranteed to run even if others at
70             # the same hookpoint fail. We will still log the failure,
71             # but proceed on to the next hook. The only way
72             # to stop all processing from one of these hooks is
73             # to raise SystemExit and stop the whole server.
74             if exc is None or hook.failsafe:
75                 try:
76                     hook()
77                 except (KeyboardInterrupt, SystemExit):
78                     raise
79                 except (cherrypy.HTTPError, cherrypy.HTTPRedirect,
80                         cherrypy.InternalRedirect):
81                     exc = sys.exc_info()[1]
82                 except:
83                     exc = sys.exc_info()[1]
84                     cherrypy.log(traceback=True)
85         if exc:
86             raise
87    
88     def __copy__(self):
89         newmap = self.__class__()
90         # We can't just use 'update' because we want copies of the
91         # mutable values (each is a list) as well.
92         for k, v in self.iteritems():
93             newmap[k] = v[:]
94         return newmap
95     copy = __copy__
96
97
98 # Config namespace handlers
99
100 def hooks_namespace(k, v):
101     """Attach bare hooks declared in config."""
102     # Use split again to allow multiple hooks for a single
103     # hookpoint per path (e.g. "hooks.before_handler.1").
104     # Little-known fact you only get from reading source ;)
105     hookpoint = k.split(".", 1)[0]
106     if isinstance(v, basestring):
107         v = cherrypy.lib.attributes(v)
108     if not isinstance(v, Hook):
109         v = Hook(v)
110     cherrypy.request.hooks[hookpoint].append(v)
111
112 def request_namespace(k, v):
113     """Attach request attributes declared in config."""
114     setattr(cherrypy.request, k, v)
115
116 def response_namespace(k, v):
117     """Attach response attributes declared in config."""
118     setattr(cherrypy.response, k, v)
119
120 def error_page_namespace(k, v):
121     """Attach error pages declared in config."""
122     cherrypy.request.error_page[int(k)] = v
123
124
125 hookpoints = ['on_start_resource', 'before_request_body',
126               'before_handler', 'before_finalize',
127               'on_end_resource', 'on_end_request',
128               'before_error_response', 'after_error_response']
129
130
131 class Request(object):
132     """An HTTP request."""
133    
134     prev = None
135    
136     # Conversation/connection attributes
137     local = http.Host("localhost", 80)
138     remote = http.Host("localhost", 1111)
139     scheme = "http"
140     server_protocol = "HTTP/1.1"
141     base = ""
142    
143     # Request-Line attributes
144     request_line = ""
145     method = "GET"
146     query_string = ""
147     protocol = (1, 1)
148     params = {}
149    
150     # Message attributes
151     header_list = []
152     headers = http.HeaderMap()
153     cookie = Cookie.SimpleCookie()
154     rfile = None
155     process_request_body = True
156     methods_with_bodies = ("POST", "PUT")
157     body = None
158    
159     # Dispatch attributes
160     dispatch = cherrypy.dispatch.Dispatcher()
161     script_name = ""
162     path_info = "/"
163     app = None
164     handler = None
165     toolmaps = {}
166     config = None
167     is_index = None
168    
169     hooks = HookMap(hookpoints)
170    
171     error_response = cherrypy.HTTPError(500).set_response
172     error_page = {}
173     show_tracebacks = True
174     throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
175     throw_errors = False
176    
177     namespaces = {"hooks": hooks_namespace,
178                   "request": request_namespace,
179                   "response": response_namespace,
180                   "error_page": error_page_namespace,
181                   # "tools": See _cptools.Toolbox
182                   }
183    
184     def __init__(self, local_host, remote_host, scheme="http",
185                  server_protocol="HTTP/1.1"):
186         """Populate a new Request object.
187         
188         local_host should be an http.Host object with the server info.
189         remote_host should be an http.Host object with the client info.
190         scheme should be a string, either "http" or "https".
191         """
192         self.local = local_host
193         self.remote = remote_host
194         self.scheme = scheme
195         self.server_protocol = server_protocol
196        
197         self.closed = False
198        
199         # Put a *copy* of the class error_page into self.
200         self.error_page = self.error_page.copy()
201        
202         # Put a *copy* of the class namespaces into self.
203         self.namespaces = self.namespaces.copy()
204    
205     def close(self):
206         if not self.closed:
207             self.closed = True
208             self.hooks.run('on_end_request')
209            
210             s = (self, cherrypy._serving.response)
211             try:
212                 cherrypy.engine.servings.remove(s)
213             except ValueError:
214                 pass
215            
216             cherrypy._serving.__dict__.clear()
217    
218     def run(self, method, path, query_string, req_protocol, headers, rfile):
219         """Process the Request.
220         
221         method, path, query_string, and req_protocol should be pulled directly
222             from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
223         path should be %XX-unquoted, but query_string should not be.
224         headers should be a list of (name, value) tuples.
225         rfile should be a file-like object containing the HTTP request entity.
226         
227         When run() is done, the returned object should have 3 attributes:
228           status, e.g. "200 OK"
229           header_list, a list of (name, value) tuples
230           body, an iterable yielding strings
231         
232         Consumer code (HTTP servers) should then access these response
233         attributes to build the outbound stream.
234         
235         """
236        
237         try:
238             self.error_response = cherrypy.HTTPError(500).set_response
239            
240             self.method = method
241             path = path or "/"
242             self.query_string = query_string or ''
243            
244             # Compare request and server HTTP protocol versions, in case our
245             # server does not support the requested protocol. Limit our output
246             # to min(req, server). We want the following output:
247             #     request    server     actual written   supported response
248             #     protocol   protocol  response protocol    feature set
249             # a     1.0        1.0           1.0                1.0
250             # b     1.0        1.1           1.1                1.0
251             # c     1.1        1.0           1.0                1.0
252             # d     1.1        1.1           1.1                1.1
253             # Notice that, in (b), the response will be "HTTP/1.1" even though
254             # the client only understands 1.0. RFC 2616 10.5.6 says we should
255             # only return 505 if the _major_ version is different.
256             rp = int(req_protocol[5]), int(req_protocol[7])
257             sp = int(self.server_protocol[5]), int(self.server_protocol[7])
258             self.protocol = min(rp, sp)
259            
260             # Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
261             url = path
262             if query_string:
263                 url += '?' + query_string
264             self.request_line = '%s %s %s' % (method, url, req_protocol)
265            
266             self.header_list = list(headers)
267             self.rfile = rfile
268             self.headers = http.HeaderMap()
269             self.cookie = Cookie.SimpleCookie()
270             self.handler = None
271            
272             # path_info should be the path from the
273             # app root (script_name) to the handler.
274             self.script_name = self.app.script_name
275             self.path_info = pi = path[len(self.script_name.rstrip("/")):]
276            
277             self.respond(pi)
278            
279         except self.throws:
280             raise
281         except:
282             if self.throw_errors:
283                 raise
284             else:
285                 # Failure in setup, error handler or finalize. Bypass them.
286                 # Can't use handle_error because we may not have hooks yet.
287                 cherrypy.log(traceback=True)
288                 if self.show_tracebacks:
289                     body = format_exc()
290                 else:
291                     body = ""
292                 r = bare_error(body)
293                 response = cherrypy.response
294                 response.status, response.header_list, response.body = r
295        
296         if self.method == "HEAD":
297             # HEAD requests MUST NOT return a message-body in the response.
298             cherrypy.response.body = []
299        
300         cherrypy.log.access()
301        
302         if cherrypy.response.timed_out:
303             raise cherrypy.TimeoutError()
304        
305         return cherrypy.response
306    
307     def respond(self, path_info):
308         """Generate a response for the resource at self.path_info."""
309         try:
310             try:
311                 try:
312                     if self.app is None:
313                         raise cherrypy.NotFound()
314                    
315                     # Get the 'Host' header, so we can do HTTPRedirects properly.
316                     self.process_headers()
317                    
318                     # Make a copy of the class hooks
319                     self.hooks = self.__class__.hooks.copy()
320                     self.toolmaps = {}
321                     self.get_resource(path_info)
322                     cherrypy._cpconfig._call_namespaces(self.config, self.namespaces)
323                    
324                     self.hooks.run('on_start_resource')
325                    
326                     if self.process_request_body:
327                         if self.method not in self.methods_with_bodies:
328                             self.process_request_body = False
329                        
330                         if self.process_request_body:
331                             # Prepare the SizeCheckWrapper for the request body
332                             mbs = cherrypy.server.max_request_body_size
333                             if mbs > 0:
334                                 self.rfile = http.SizeCheckWrapper(self.rfile, mbs)
335                    
336                     self.hooks.run('before_request_body')
337                     if self.process_request_body:
338                         self.process_body()
339                    
340                     self.hooks.run('before_handler')
341                     if self.handler:
342                         cherrypy.response.body = self.handler()
343                     self.hooks.run('before_finalize')
344                     cherrypy.response.finalize()
345                 except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst:
346                     inst.set_response()
347                     self.hooks.run('before_finalize')
348                     cherrypy.response.finalize()
349             finally:
350                 self.hooks.run('on_end_resource')
351         except self.throws:
352             raise
353         except:
354             if self.throw_errors:
355                 raise
356             self.handle_error(sys.exc_info())
357    
358     def process_headers(self):
359         self.params = http.parse_query_string(self.query_string)
360        
361         # Process the headers into self.headers
362         headers = self.headers
363         for name, value in self.header_list:
364             # Call title() now (and use dict.__method__(headers))
365             # so title doesn't have to be called twice.
366             name = name.title()
367             value = value.strip()
368            
369             # Warning: if there is more than one header entry for cookies (AFAIK,
370             # only Konqueror does that), only the last one will remain in headers
371             # (but they will be correctly stored in request.cookie).
372             if "=?" in value:
373                 dict.__setitem__(headers, name, http.decode_TEXT(value))
374             else:
375                 dict.__setitem__(headers, name, value)
376            
377             # Handle cookies differently because on Konqueror, multiple
378             # cookies come on different lines with the same key
379             if name == 'Cookie':
380                 self.cookie.load(value)
381        
382         if not dict.__contains__(headers, 'Host'):
383             # All Internet-based HTTP/1.1 servers MUST respond with a 400
384             # (Bad Request) status code to any HTTP/1.1 request message
385             # which lacks a Host header field.
386             if self.protocol >= (1, 1):
387                 msg = "HTTP/1.1 requires a 'Host' request header."
388                 raise cherrypy.HTTPError(400, msg)
389         host = dict.__getitem__(headers, 'Host')
390         if not host:
391             host = self.local.name or self.local.ip
392         self.base = "%s://%s" % (self.scheme, host)
393    
394     def get_resource(self, path):
395         """Find and call a dispatcher (which sets self.handler and .config)."""
396         dispatch = self.dispatch
397         # First, see if there is a custom dispatch at this URI. Custom
398         # dispatchers can only be specified in app.config, not in _cp_config
399         # (since custom dispatchers may not even have an app.root).
400         trail = path
401         while trail:
402             nodeconf = self.app.config.get(trail, {})
403            
404             d = nodeconf.get("request.dispatch")
405             if d:
406                 dispatch = d
407                 break
408            
409             lastslash = trail.rfind("/")
410             if lastslash == -1:
411                 break
412             elif lastslash == 0 and trail != "/":
413                 trail = "/"
414             else:
415                 trail = trail[:lastslash]
416        
417         # dispatch() should set self.handler and self.config
418         dispatch(path)
419    
420     def process_body(self):
421         """Convert request.rfile into request.params (or request.body)."""
422         # FieldStorage only recognizes POST, so fake it.
423         methenv = {'REQUEST_METHOD': "POST"}
424         try:
425             forms = _cpcgifs.FieldStorage(fp=self.rfile,
426                                           headers=self.headers,
427                                           environ=methenv,
428                                           keep_blank_values=1)
429         except http.MaxSizeExceeded:
430             # Post data is too big
431             raise cherrypy.HTTPError(413)
432        
433         # Note that, if headers['Content-Type'] is multipart/*,
434         # then forms.file will not exist; instead, each form[key]
435         # item will be its own file object, and will be handled
436         # by params_from_CGI_form.
437         if forms.file:
438             # request body was a content-type other than form params.
439             self.body = forms.file
440         else:
441             self.params.update(http.params_from_CGI_form(forms))
442    
443     def handle_error(self, exc):
444         try:
445             self.hooks.run("before_error_response")
446             if self.error_response:
447                 self.error_response()
448             self.hooks.run("after_error_response")
449             cherrypy.response.finalize()
450         except cherrypy.HTTPRedirect, inst:
451             inst.set_response()
452             cherrypy.response.finalize()
453
454
455 def file_generator(input, chunkSize=65536):
456     """Yield the given input (a file object) in chunks (default 64k)."""
457     chunk = input.read(chunkSize)
458     while chunk:
459         yield chunk
460         chunk = input.read(chunkSize)
461     input.close()
462
463
464 class Body(object):
465     """The body of the HTTP response (the response entity)."""
466    
467     def __get__(self, obj, objclass=None):
468         if obj is None:
469             # When calling on the class instead of an instance...
470             return self
471         else:
472             return obj._body
473    
474     def __set__(self, obj, value):
475         # Convert the given value to an iterable object.
476         if isinstance(value, basestring):
477             # strings get wrapped in a list because iterating over a single
478             # item list is much faster than iterating over every character
479             # in a long string.
480             if value:
481                 value = [value]
482             else:
483                 # [''] doesn't evaluate to False, so replace it with [].
484                 value = []
485         elif isinstance(value, types.FileType):
486             value = file_generator(value)
487         elif value is None:
488             value = []
489         obj._body = value
490
491
492 class Response(object):
493     """An HTTP Response."""
494    
495     # Class attributes for dev-time introspection.
496     status = ""
497     header_list = []
498     headers = http.HeaderMap()
499     cookie = Cookie.SimpleCookie()
500     body = Body()
501     time = None
502     timeout = 300
503     timed_out = False
504     stream = False
505    
506     def __init__(self):
507         self.status = None
508         self.header_list = None
509         self._body = []
510         self.time = time.time()
511        
512</