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

root/branches/cherrypy-2.x/cherrypy/_cphttptools.py

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

2.x backport of RFC-2047 header encoding/decoding (see [1166] et al).

  • 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 _cputil, _cpcgifs
11 from cherrypy.filters import applyFilters
12 from cherrypy.lib import cptools, httptools, profiler
13
14
15 class Request(object):
16     """An HTTP request."""
17    
18     is_index = None
19    
20     def __init__(self, remoteAddr, remotePort, remoteHost, scheme="http"):
21         """Populate a new Request object.
22         
23         remoteAddr should be the client IP address
24         remotePort should be the client Port
25         remoteHost: should be the client's host name. If not available
26             (because no reverse DNS lookup is performed), the client
27             IP should be provided.
28         scheme should be a string, either "http" or "https".
29         """
30         self.remote_addr  = remoteAddr
31         self.remote_port  = remotePort
32         self.remote_host  = remoteHost
33         # backward compatibility
34         self.remoteAddr = remoteAddr
35         self.remotePort = remotePort
36         self.remoteHost = remoteHost
37        
38         self.scheme = scheme
39         self.execute_main = True
40         self.closed = False
41    
42     def close(self):
43         if not self.closed:
44             self.closed = True
45             applyFilters('on_end_request', failsafe=True)
46             cherrypy.serving.__dict__.clear()
47    
48     def run(self, requestLine, headers, rfile):
49         """Process the Request.
50         
51         requestLine should be of the form "GET /path HTTP/1.0".
52         headers should be a list of (name, value) tuples.
53         rfile should be a file-like object containing the HTTP request entity.
54         
55         When run() is done, the returned object should have 3 attributes:
56           status, e.g. "200 OK"
57           headers, a list of (name, value) tuples
58           body, an iterable yielding strings
59         
60         Consumer code (HTTP servers) should then access these response
61         attributes to build the outbound stream.
62         
63         """
64         self.requestLine = requestLine.strip()
65         self.header_list = list(headers)
66         self.rfile = rfile
67        
68         self.headers = httptools.HeaderMap()
69         self.headerMap = self.headers # Backward compatibility
70         self.simple_cookie = Cookie.SimpleCookie()
71         self.simpleCookie = self.simple_cookie # Backward compatibility
72        
73         # Set up the profiler if requested.
74         conf = cherrypy.config.get
75         if conf("profiling.on", False):
76             p = getattr(cherrypy, "profiler", None)
77             if p is None:
78                 ppath = conf("profiling.path", "")
79                 p = cherrypy.profiler = profiler.Profiler(ppath)
80             cherrypy.profiler.run(self._run)
81         else:
82             self._run()
83        
84         if self.method == "HEAD":
85             # HEAD requests MUST NOT return a message-body in the response.
86             cherrypy.response.body = []
87        
88         _cputil.get_special_attribute("_cp_log_access", "_cpLogAccess")()
89        
90         return cherrypy.response
91    
92     def _run(self):
93        
94         try:
95             # This has to be done very early in the request process,
96             # because request.object_path is used for config lookups
97             # right away.
98             self.processRequestLine()
99            
100             try:
101                 applyFilters('on_start_resource', failsafe=True)
102                
103                 try:
104                     self.processHeaders()
105                    
106                     applyFilters('before_request_body')
107                     if self.processRequestBody:
108                         # Prepare the SizeCheckWrapper for the request body
109                         mbs = int(cherrypy.config.get('server.max_request_body_size',
110                                                       100 * 1024 * 1024))
111                         if mbs > 0:
112                             self.rfile = httptools.SizeCheckWrapper(self.rfile, mbs)
113                        
114                         self.processBody()
115                    
116                     # Loop to allow for InternalRedirect.
117                     while True:
118                         try:
119                             applyFilters('before_main')
120                             if self.execute_main:
121                                 self.main()
122                             break
123                         except cherrypy.InternalRedirect, ir:
124                             self.object_path = ir.path
125                    
126                     applyFilters('before_finalize')
127                     cherrypy.response.finalize()
128                 except cherrypy.RequestHandled:
129                     pass
130                 except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst:
131                     # For an HTTPRedirect or HTTPError (including NotFound),
132                     # we don't go through the regular mechanism:
133                     # we return the redirect or error page immediately
134                     inst.set_response()
135                     applyFilters('before_finalize')
136                     cherrypy.response.finalize()
137             finally:
138                 applyFilters('on_end_resource', failsafe=True)
139         except (KeyboardInterrupt, SystemExit):
140             raise
141         except:
142             if cherrypy.config.get("server.throw_errors", False):
143                 raise
144             cherrypy.response.handleError(sys.exc_info())
145    
146     def processRequestLine(self):
147         rl = self.requestLine
148         method, path, qs, proto = httptools.parseRequestLine(rl)
149         if path == "*":
150             path = "global"
151        
152         self.method = method
153         self.processRequestBody = method in ("POST", "PUT")
154        
155         self.path = path
156         self.query_string = qs
157         self.queryString = qs # Backward compatibility
158         self.protocol = proto
159        
160         # Change object_path in filters to change
161         # the object that will get rendered
162         self.object_path = path
163        
164         # Compare request and server HTTP versions, in case our server does
165         # not support the requested version. We can't tell the server what
166         # version number to write in the response, so we limit our output
167         # to min(req, server). We want the following output:
168         #     request    server     actual written   supported response
169         #     version    version   response version  feature set (resp.v)
170         # a     1.0        1.0           1.0                1.0
171         # b     1.0        1.1           1.1                1.0
172         # c     1.1        1.0           1.0                1.0
173         # d     1.1        1.1           1.1                1.1
174         # Notice that, in (b), the response will be "HTTP/1.1" even though
175         # the client only understands 1.0. RFC 2616 10.5.6 says we should
176         # only return 505 if the _major_ version is different.
177        
178         # cherrypy.request.version == request.protocol in a Version instance.
179         self.version = httptools.Version.from_http(self.protocol)
180         server_v = cherrypy.config.get("server.protocol_version", "HTTP/1.0")
181         server_v = httptools.Version.from_http(server_v)
182        
183         # cherrypy.response.version should be used to determine whether or
184         # not to include a given HTTP/1.1 feature in the response content.
185         cherrypy.response.version = min(self.version, server_v)
186    
187     def processHeaders(self):
188        
189         self.params = httptools.parseQueryString(self.query_string)
190         self.paramMap = self.params # Backward compatibility
191        
192         # Process the headers into self.headers
193         for name, value in self.header_list:
194             value = value.strip()
195             # Warning: if there is more than one header entry for cookies (AFAIK,
196             # only Konqueror does that), only the last one will remain in headers
197             # (but they will be correctly stored in request.simple_cookie).
198             self.headers[name] = httptools.decode_TEXT(value)
199            
200             # Handle cookies differently because on Konqueror, multiple
201             # cookies come on different lines with the same key
202             if name.title() == 'Cookie':
203                 self.simple_cookie.load(value)
204        
205         # Save original values (in case they get modified by filters)
206         # This feature is deprecated in 2.2 and will be removed in 2.3.
207         self._original_params = self.params.copy()
208        
209         if self.version >= "1.1":
210             # All Internet-based HTTP/1.1 servers MUST respond with a 400
211             # (Bad Request) status code to any HTTP/1.1 request message
212             # which lacks a Host header field.
213             if not self.headers.has_key("Host"):
214                 msg = "HTTP/1.1 requires a 'Host' request header."
215                 raise cherrypy.HTTPError(400, msg)
216         self.base = "%s://%s" % (self.scheme, self.headers.get('Host', ''))
217    
218     def _get_original_params(self):
219         # This feature is deprecated in 2.2 and will be removed in 2.3.
220         return self._original_params
221     original_params = property(_get_original_params,
222                         doc="Deprecated. A copy of the original params.")
223    
224     def _get_browser_url(self):
225         url = self.base + self.path
226         if self.query_string:
227             url += '?' + self.query_string
228         return url
229     browser_url = property(_get_browser_url,
230                           doc="The URL as entered in a browser (read-only).")
231     browserUrl = browser_url # Backward compatibility
232    
233     def processBody(self):
234         # FieldStorage only recognizes POST, so fake it.
235         methenv = {'REQUEST_METHOD': "POST"}
236         try:
237             forms = _cpcgifs.FieldStorage(fp=self.rfile,
238                                           headers=self.headers,
239                                           environ=methenv,
240                                           keep_blank_values=1)
241         except httptools.MaxSizeExceeded:
242             # Post data is too big
243             raise cherrypy.HTTPError(413)
244        
245         if forms.file:
246             # request body was a content-type other than form params.
247             self.body = forms.file
248         else:
249             self.params.update(httptools.paramsFromCGIForm(forms))
250    
251     def main(self, path=None):
252         """Obtain and set cherrypy.response.body from a page handler."""
253         if path is None:
254             path = self.object_path
255        
256         page_handler, object_path, virtual_path = self.mapPathToObject(path)
257        
258         # Decode any leftover %2F in the virtual_path atoms.
259         virtual_path = [x.replace("%2F", "/") for x in virtual_path]
260        
261         # Remove "root" from object_path and join it to get object_path
262         self.object_path = '/' + '/'.join(object_path[1:])
263         try:
264             body = page_handler(*virtual_path, **self.params)
265         except Exception, x:
266             if hasattr(x, "args"):
267                 x.args = x.args + (page_handler,)
268             raise
269         cherrypy.response.body = body
270    
271     def mapPathToObject(self, objectpath):
272         """For path, return the corresponding exposed callable (or raise NotFound).
273         
274         path should be a "relative" URL path, like "/app/a/b/c". Leading and
275         trailing slashes are ignored.
276         
277         Traverse path:
278         for /a/b?arg=val, we'll try:
279           root.a.b.index -> redirect to /a/b/?arg=val
280           root.a.b.default(arg='val') -> redirect to /a/b/?arg=val
281           root.a.b(arg='val')
282           root.a.default('b', arg='val')
283           root.default('a', 'b', arg='val')
284         
285         The target method must have an ".exposed = True" attribute.
286         """
287        
288         objectTrail = _cputil.get_object_trail(objectpath)
289         names = [name for name, candidate in objectTrail]
290        
291         # Try successive objects (reverse order)
292         mounted_app_roots = cherrypy.tree.mount_points.values()
293         for i in xrange(len(objectTrail) - 1, -1, -1):
294            
295             name, candidate = objectTrail[i]
296            
297             # Try a "default" method on the current leaf.
298             defhandler = getattr(candidate, "default", None)
299             if callable(defhandler) and getattr(defhandler, 'exposed', False):
300                 # See http://www.cherrypy.org/ticket/613
301                 self.is_index = objectpath.endswith("/")
302                 return defhandler, names[:i+1] + ["default"], names[i+1:-1]
303            
304             # Uncomment the next line to restrict positional params to "default".
305             # if i < len(objectTrail) - 2: continue
306            
307             # Try the current leaf.
308             if callable(candidate) and getattr(candidate, 'exposed', False):
309                 if i == len(objectTrail) - 1:
310                     # We found the extra ".index". Check if the original path
311                     # had a trailing slash (otherwise, do a redirect).
312                     self.is_index = True
313                     if not objectpath.endswith('/'):
314                         atoms = self.browser_url.split("?", 1)
315                         newUrl = atoms.pop(0) + '/'
316                         if atoms:
317                             newUrl += "?" + atoms[0]
318                         raise cherrypy.HTTPRedirect(newUrl)
319                 self.is_index = False
320                 return candidate, names[:i+1], names[i+1:-1]
321            
322             if candidate in mounted_app_roots:
323                 break
324        
325         # We didn't find anything
326         raise cherrypy.NotFound(objectpath)
327
328
329 class Body(object):
330     """The body of the HTTP response (the response entity)."""
331    
332     def __get__(self, obj, objclass=None):
333         if obj is None:
334             # When calling on the class instead of an instance...
335             return self
336         else:
337             return obj._body
338    
339     def __set__(self, obj, value):
340         # Convert the given value to an iterable object.
341         if isinstance(value, types.FileType):
342             value = cptools.fileGenerator(value)
343         elif isinstance(value, types.GeneratorType):
344             value = flattener(value)
345         elif isinstance(value, basestring):
346             # strings get wrapped in a list because iterating over a single
347             # item list is much faster than iterating over every character
348             # in a long string.
349             value = [value]
350         elif value is None:
351             value = []
352         obj._body = value
353
354
355 def flattener(input):
356     """Yield the given input, recursively iterating over each result (if needed)."""
357     for x in input:
358         if not isinstance(x, types.GeneratorType):
359             yield x
360         else:
361             for y in flattener(x):
362                 yield y
363
364
365 class Response(object):
366     """An HTTP Response."""
367    
368     body = Body()
369    
370     def __init__(self):
371         self.status = None
372         self.header_list = None
373         self.body = None
374         self.time = time.time()
375        
376         self.headers = httptools.HeaderMap()
377         self.headerMap = self.headers # Backward compatibility
378         content_type = cherrypy.config.get('server.default_content_type', 'text/html')
379         self.headers.update({
380             "Content-Type": content_type,
381             "Server": "CherryPy/" + cherrypy.__version__,
382             "Date": httptools.HTTPDate(time.gmtime(self.time)),
383             "Set-Cookie": [],
384             "Content-Length": None
385         })
386         self.simple_cookie = Cookie.SimpleCookie()
387         self.simpleCookie = self.simple_cookie # Backward compatibility
388    
389     def collapse_body(self):
390         newbody = ''.join([chunk for chunk in self.body])
391         self.body = newbody
392         return newbody
393    
394     def finalize(self):
395         """Transform headers (and cookies) into cherrypy.response.header_list."""
396        
397         try:
398             code, reason, _ = httptools.validStatus(self.status)
399         except ValueError, x:
400             raise cherrypy.HTTPError(500, x.args[0])
401        
402         self.status = "%s %s" % (code, reason)
403        
404         stream = cherrypy.config.get("stream_response", False)
405         # OPTIONS requests MUST include a Content-Length of 0 if no body.
406         # Just punt and figure Content-Length for all OPTIONS requests.
407         if cherrypy.request.method == "OPTIONS":
408             stream = False
409        
410         if stream:
411             try:
412                 del self.headers['Content-Length']
413             except KeyError:
414                 pass
415         elif code < 200 or code in (204, 304):
416             # "All 1xx (informational), 204 (no content),
417             # and 304 (not modified) responses MUST NOT
418             # include a message-body."
419             self.headers.pop('Content-Length', None)
420             self.body = ""
421         else:
422             # Responses which are not streamed should have a Content-Length,
423             # but allow user code to set Content-Length if desired.
424             if self.headers.get('Content-Length') is None:
425                 content = self.collapse_body()
426                 self.headers['Content-Length'] = len(content)
427        
428         # Transform our header dict into a sorted list of tuples.
429         self.header_list = self.headers.sorted_list(protocol=self.version)
430        
431         cookie = self.simple_cookie.output()
432         if cookie:
433             for line in cookie.split("\n"):
434                 name, value = line.split(": ", 1)
435                 self.header_list.append((name, value))
436    
437     dbltrace = "\n===First Error===\n\n%s\n\n===Second Error===\n\n%s\n\n"
438    
439     def handleError(self, exc):
440         """Set status, headers, and body when an unanticipated error occurs."""
441         try:
442             applyFilters('before_error_response')
443            
444             # _cp_on_error will probably change self.body.
445             # It may also change the headers, etc.
446             _cputil.get_special_attribute('_cp_on_error', '_cpOnError')()
447            
448             self.finalize()
449            
450             applyFilters('after_error_response')
451             return
452         except cherrypy.HTTPRedirect, inst:
453             try:
454                 inst.set_response()
455                 self.finalize()
456                 return
457             except (KeyboardInterrupt, SystemExit):
458                 raise
459             except:
460                 # Fall through to the second error handler
461                 pass
462         except (KeyboardInterrupt, SystemExit):
463             raise
464         except:
465             # Fall through to the second error handler
466             pass
467        
468         # Failure in _cp_on_error, error filter, or finalize.
469         # Bypass them all.
470         if cherrypy.config.get('server.show_tracebacks', False):
471             body = self.dbltrace % (_cputil.formatExc(exc),
472                                     _cputil.formatExc())
473         else:
474             body = ""
475         self.setBareError(body)
476    
477     def setBareError(self, body=None):
478         self.status, self.header_list, self.body = _cputil.bareError(body)
479
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets