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

root/branches/cp3-wsgi-remix/_cprequest.py

Revision 1245 (checked in by dowski, 2 years ago)

Fixed an issue with wrapping plain WSGI callables and removed some pre-remix code.

  • 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 HookMap(object):
16    
17     def __init__(self, points=None, failsafe=None):
18         points = points or []
19         self.callbacks = dict([(point, []) for point in points])
20         self.failsafe = failsafe or []
21    
22     def attach(self, point, callback, conf=None):
23         if not conf:
24             # No point adding a wrapper if there's no conf
25             self.callbacks[point].append(callback)
26         else:
27             def wrapper():
28                 callback(**conf)
29             self.callbacks[point].append(wrapper)
30    
31     def run(self, point):
32         """Execute all registered callbacks for the given point."""
33         if cherrypy.response.timed_out:
34             raise cherrypy.TimeoutError()
35        
36         failsafe = point in self.failsafe
37         for callback in self.callbacks[point]:
38             # Some hookpoints guarantee all callbacks are run even if
39             # others at the same hookpoint fail. We will still log the
40             # failure, but proceed on to the next callback. The only way
41             # to stop all processing from one of these callbacks is
42             # to raise SystemExit and stop the whole server. So, trap
43             # your own errors in these callbacks!
44             if failsafe:
45                 try:
46                     callback()
47                 except (KeyboardInterrupt, SystemExit):
48                     raise
49                 except:
50                     cherrypy.log(traceback=True)
51             else:
52                 callback()
53
54
55 class Request(object):
56     """An HTTP request."""
57    
58     # Conversation/connection attributes
59     local = http.Host("localhost", 80)
60     remote = http.Host("localhost", 1111)
61     scheme = "http"
62     base = ""
63    
64     # Request-Line attributes
65     request_line = ""
66     method = "GET"
67     path = ""
68     query_string = ""
69     protocol = (1, 1)
70     params = {}
71    
72     # Message attributes
73     header_list = []
74     headers = http.HeaderMap()
75     simple_cookie = Cookie.SimpleCookie()
76     rfile = None
77     process_request_body = True
78     body = None
79     body_read = False
80    
81     # Dispatch attributes
82     script_name = ""
83     path_info = "/"
84     app = None
85     handler = None
86     toolmap = {}
87     config = None
88     error_response = cherrypy.HTTPError(500).set_response
89     hookpoints = ['on_start_resource', 'before_request_body',
90                   'before_main', 'before_finalize',
91                   'on_end_resource', 'on_end_request',
92                   'before_error_response', 'after_error_response']
93     hooks = HookMap(hookpoints)
94    
95     def __init__(self, local_host, remote_host, scheme="http"):
96         """Populate a new Request object.
97         
98         local_host should be an http.Host object with the server info.
99         remote_host should be an http.Host object with the client info.
100         scheme should be a string, either "http" or "https".
101         """
102         self.local = local_host
103         self.remote = remote_host
104         self.scheme = scheme
105        
106         self.closed = False
107         self.redirections = []
108    
109     def close(self):
110         if not self.closed:
111             self.closed = True
112             self.hooks.run('on_end_request')
113            
114             s = (self, cherrypy.serving.response)
115             try:
116                 cherrypy.engine.servings.remove(s)
117             except ValueError:
118                 pass
119            
120             cherrypy.serving.__dict__.clear()
121    
122     def run(self, method, path, query_string, protocol, headers, rfile):
123         """Process the Request.
124         
125         method, path, query_string, and protocol should be pulled directly
126             from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
127         path should be %XX-unquoted, but query_string should not be.
128         headers should be a list of (name, value) tuples.
129         rfile should be a file-like object containing the HTTP request entity.
130         
131         When run() is done, the returned object should have 3 attributes:
132           status, e.g. "200 OK"
133           header_list, a list of (name, value) tuples
134           body, an iterable yielding strings
135         
136         Consumer code (HTTP servers) should then access these response
137         attributes to build the outbound stream.
138         
139         """
140         try:
141             self.error_response = cherrypy.HTTPError(500).set_response
142            
143             self.method = method
144             self.path = path or "/"
145             self.query_string = query_string
146             self.protocol = int(protocol[5]), int(protocol[7])
147            
148             # Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
149             url = path
150             if query_string:
151                 url += '?' + query_string
152             self.request_line = '%s %s %s' % (method, url, protocol)
153            
154             self.header_list = list(headers)
155             self.rfile = rfile
156             self.headers = http.HeaderMap()
157             self.simple_cookie = Cookie.SimpleCookie()
158             self.handler = None
159            
160             # Get the 'Host' header, so we can do HTTPRedirects properly.
161             self.process_headers()
162            
163             self.script_name = self.wsgi_environ.get('SCRIPT_NAME', '')
164            
165             # path_info should be the path from the
166             # app root (script_name) to the handler.
167             self.path_info = self.wsgi_environ.get('PATH_INFO', '')
168            
169             # Loop to allow for InternalRedirect.
170             pi = self.path_info
171             while True:
172                 try:
173                     self.respond(pi)
174                     break
175                 except cherrypy.InternalRedirect, ir:
176                     pi = ir.path
177                     if (pi in self.redirections and
178                         not cherrypy.config.get("recursive_redirect")):
179                         raise RuntimeError("InternalRedirect visited the "
180                                            "same URL twice: %s" % repr(pi))
181                     self.redirections.append(pi)
182         except (KeyboardInterrupt, SystemExit):
183             raise
184         except cherrypy.TimeoutError:
185             raise
186         except:
187             if cherrypy.config.get("throw_errors", False):
188                 raise
189             self.handle_error(sys.exc_info())
190        
191         if self.method == "HEAD":
192             # HEAD requests MUST NOT return a message-body in the response.
193             cherrypy.response.body = []
194        
195         log_access = cherrypy.config.get("log_access", cherrypy.log_access)
196         if log_access:
197             log_access()
198        
199         return cherrypy.response
200    
201     def respond(self, path_info):
202         """Generate a response for the resource at self.path_info."""
203         try:
204             try:
205                 if cherrypy.response.timed_out:
206                     raise cherrypy.TimeoutError()
207                
208                 self.hooks = HookMap(self.hookpoints)
209                 self.hooks.failsafe = ['on_start_resource', 'on_end_resource',
210                                        'on_end_request']
211                
212                 self.get_resource(path_info)
213                 self.tool_up()
214                 self.hooks.run('on_start_resource')
215                
216                 if self.process_request_body and not self.body_read:
217                     # Check path-specific methods_with_bodies.
218                     meths = self.config.get("methods_with_bodies", ("POST", "PUT"))
219                     self.process_request_body = self.method in meths
220                    
221                     # Prepare the SizeCheckWrapper for the request body
222                     mbs = int(self.config.get('server.max_request_body_size',
223                                               100 * 1024 * 1024))
224                     if mbs > 0:
225                         self.rfile = http.SizeCheckWrapper(self.rfile, mbs)
226                
227                 self.hooks.run('before_request_body')
228                 if self.process_request_body:
229                     self.process_body()
230                
231                 self.hooks.run('before_main')
232                 if self.handler:
233                     self.handler()
234                 self.hooks.run('before_finalize')
235                 cherrypy.response.finalize()
236             except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst:
237                 inst.set_response()
238                 self.hooks.run('before_finalize')
239                 cherrypy.response.finalize()
240         finally:
241             self.hooks.run('on_end_resource')
242    
243     def process_headers(self):
244         self.params = http.parse_query_string(self.query_string)
245        
246         # Process the headers into self.headers
247         headers = self.headers
248         for name, value in self.header_list:
249             # Call title() now (and use dict.__method__(headers))
250             # so title doesn't have to be called twice.
251             name = name.title()
252             value = value.strip()
253            
254             # Warning: if there is more than one header entry for cookies (AFAIK,
255             # only Konqueror does that), only the last one will remain in headers
256             # (but they will be correctly stored in request.simple_cookie).
257             if "=?" in value:
258                 dict.__setitem__(headers, name, http.decode_TEXT(value))
259             else:
260                 dict.__setitem__(headers, name, value)
261            
262             # Handle cookies differently because on Konqueror, multiple
263             # cookies come on different lines with the same key
264             if name == 'Cookie':
265                 self.simple_cookie.load(value)
266        
267         if not dict.__contains__(headers, 'Host'):
268             # All Internet-based HTTP/1.1 servers MUST respond with a 400
269             # (Bad Request) status code to any HTTP/1.1 request message
270             # which lacks a Host header field.
271             if self.protocol >= (1, 1):
272                 msg = "HTTP/1.1 requires a 'Host' request header."
273                 raise cherrypy.HTTPError(400, msg)
274         host = dict.__getitem__(headers, 'Host')
275         if not host:
276             host = self.local.name or self.local.ip
277         self.base = "%s://%s" % (self.scheme, host)
278    
279     def get_resource(self, path):
280         """Find and call a dispatcher (which sets self.handler and .config)."""
281         dispatch = default_dispatch
282         # First, see if there is a custom dispatch at this URI. Custom
283         # dispatchers can only be specified in app.conf, not in _cp_config
284         # (since custom dispatchers may not even have an app.root).
285         trail = path
286         while trail:
287             nodeconf = self.app.conf.get(trail, {})
288             d = nodeconf.get("dispatch")
289             if d:
290                 dispatch = d
291                 break
292            
293             lastslash = trail.rfind("/")
294             if lastslash == -1:
295                 break
296             elif lastslash == 0 and trail != "/":
297                 trail = "/"
298             else:
299                 trail = trail[:lastslash]
300        
301         # dispatch() should set self.handler and self.config
302         dispatch(path)
303    
304     def tool_up(self):
305         """Populate self.toolmap and set up each tool."""
306         # Get all 'tools.*' config entries as a {toolname: {k: v}} dict.
307         self.toolmap = tm = {}
308         reqconf = self.config
309         for k in reqconf:
310             atoms = k.split(".", 2)
311             namespace = atoms[0]
312             if namespace == "tools":
313                 toolname = atoms[1]
314                 bucket = tm.setdefault(toolname, {})
315                 bucket[atoms[2]] = reqconf[k]
316             elif namespace == "hooks":
317                 # Attach bare hooks declared in config.
318                 hookpoint = atoms[1]
319                 v = reqconf[k]
320                 if isinstance(v, basestring):
321                     v = cherrypy.lib.attributes(v)
322                 self.hooks.attach(hookpoint, v)
323        
324         # Run tool._setup(conf) for each tool in the new toolmap.
325         tools = cherrypy.tools
326         for toolname in tm:
327             if tm[toolname].get("on", False):
328                 tool = getattr(tools, toolname)
329                 tool._setup()
330    
331     def _get_browser_url(self):
332         url = self.base + self.path
333         if self.query_string:
334             url += '?' + self.query_string
335         return url
336     browser_url = property(_get_browser_url,
337                           doc="The URL as entered in a browser (read-only).")
338    
339     def process_body(self):
340         """Convert request.rfile into request.params (or request.body)."""
341         # Guard against re-reading body (e.g. on InternalRedirect)
342         if self.body_read:
343             return
344         self.body_read = True
345        
346         # FieldStorage only recognizes POST, so fake it.
347         methenv = {'REQUEST_METHOD': "POST"}
348         try:
349             forms = _cpcgifs.FieldStorage(fp=self.rfile,
350                                           headers=self.headers,
351                                           environ=methenv,
352                                           keep_blank_values=1)
353         except http.MaxSizeExceeded:
354             # Post data is too big
355             raise cherrypy.HTTPError(413)
356        
357         if forms.file:
358             # request body was a content-type other than form params.
359             self.body = forms.file
360         else:
361             self.params.update(http.params_from_CGI_form(forms))
362    
363     def handle_error(self, exc):
364         response = cherrypy.response
365         try:
366             self.hooks.run("before_error_response")
367             if self.error_response:
368                 self.error_response()
369             self.hooks.run("after_error_response")
370             response.finalize()
371             return
372         except cherrypy.HTTPRedirect, inst:
373             try:
374                 inst.set_response()
375                 response.finalize()
376                 return
377             except (KeyboardInterrupt, SystemExit):
378                 raise
379             except:
380                 # Fall through to the second error handler
381                 pass
382         except (KeyboardInterrupt, SystemExit):
383             raise
384         except:
385             # Fall through to the second error handler
386             pass
387        
388         # Failure in error handler or finalize. Bypass them.
389         if cherrypy.config.get('show_tracebacks', False):
390             dbltrace = ("\n===First Error===\n\n%s"
391                         "\n\n===Second Error===\n\n%s\n\n")
392             body = dbltrace % (format_exc(exc), format_exc())
393         else:
394             body = ""
395         r = bare_error(body)
396         response.status, response.header_list, response.body = r
397
398
399 class PageHandler(object):
400     """Callable which sets response.body."""
401    
402     def __init__(self, callable, *args, **kwargs):
403         self.callable = callable
404         self.args = args
405         self.kwargs = kwargs
406    
407     def __call__(self):
408         cherrypy.response.body = self.callable(*self.args, **self.kwargs)
409
410
411 class LateParamPageHandler(PageHandler):
412     """When passing cherrypy.request.params to the page handler, we don't
413     want to capture that dict too early; we want to give tools like the
414     decoding tool a chance to modify the params dict in-between the lookup
415     of the handler and the actual calling of the handler. This subclass
416     takes that into account, and allows request.params to be 'bound late'
417     (it's more complicated than that, but that's the effect).
418     """
419    
420     def _get_kwargs(self):
421         kwargs = cherrypy.request.params.copy()
422         if self._kwargs:
423             kwargs.update(self._kwargs)
424         return kwargs
425    
426     def _set_kwargs(self, kwargs):
427         self._kwargs = kwargs
428    
429     kwargs = property(_get_kwargs, _set_kwargs,
430                       doc='page handler kwargs (with '
431                       'cherrypy.request.params copied in)')
432
433
434 class Dispatcher(object):
435    
436     def __call__(self, path_info):
437         """Set handler and config for the current request."""
438         request = cherrypy.request
439         func, vpath = self.find_handler(path_info)
440        
441         # Decode any leftover %2F in the virtual_path atoms.
442         vpath = [x.replace("%2F", "/") for x in vpath]
443        
444         if func:
445             request.handler = LateParamPageHandler(func, *vpath)
446         else:
447             request.handler = cherrypy.NotFound()
448    
449     def find_handler(self, path):
450         """Find the appropriate page handler for the given path."""
451         request = cherrypy.request
452         app = request.app
453         root = app.root
454        
455         # Get config for the root object/path.
456         environments = cherrypy.config.environments
457         curpath = ""
458         nodeconf = {}
459         if hasattr(root, "_cp_config"):
460             nodeconf.update(root._cp_config)
461         if 'environment' in nodeconf:
462             env = environments[nodeconf['environment']]
463             for k in env:
464                 if k not in nodeconf:
465                     nodeconf[k] = env[k]
466         if "/" in app.conf:
467             nodeconf.update(app.conf["/"])
468         object_trail = [('root', root, nodeconf, curpath)]
469        
470         node = root
471         names = [x for x in path.strip('/').split('/') if x] + ['index']
472         for name in names:
473             # map to legal Python identifiers (replace '.' with '_')
474             objname = name.replace('.', '_')
475            
476             nodeconf = {}
477             node = getattr(node, objname, None)
478             if node is not None:
479                 # Get _cp_config attached to this node.
480                 if hasattr(node, "_cp_config"):
481                     nodeconf.update(node._cp_config)
482                    
483                     # Resolve "environment" entries. This must be done node-by-node
484                     # so that a child's "environment" can override concrete settings
485                     # of a parent. However, concrete settings in this node will
486                     # override "environment" settings in the same node.
487                     if