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

root/branches/cherrypy-2.1/cherrypy/_cphttptools.py

Revision 737 (checked in by rdelon, 3 years ago)

request.browserUrl was missing the queryString

Line 
1 """
2 Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
3 All rights reserved.
4
5 Redistribution and use in source and binary forms, with or without modification,
6 are permitted provided that the following conditions are met:
7
8     * Redistributions of source code must retain the above copyright notice,
9       this list of conditions and the following disclaimer.
10     * Redistributions in binary form must reproduce the above copyright notice,
11       this list of conditions and the following disclaimer in the documentation
12       and/or other materials provided with the distribution.
13     * Neither the name of the CherryPy Team nor the names of its contributors
14       may be used to endorse or promote products derived from this software
15       without specific prior written permission.
16
17 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
21 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
23 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
24 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
25 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27 """
28
29 """
30 Common Service Code for CherryPy
31 """
32
33 import cgi
34
35 import Cookie
36 import os
37 import re
38 import sys
39 import types
40 import urllib
41 from urlparse import urlparse
42
43 import cherrypy
44 from cherrypy import _cputil, _cpcgifs, _cpwsgiserver, _cperror
45 from cherrypy.lib import cptools
46
47
48 class Version(object):
49    
50     """A version, such as "2.1 beta 3", which can be compared atom-by-atom.
51     
52     If a string is provided to the constructor, it will be split on word
53     boundaries; that is, "1.4.13 beta 9" -> ["1", "4", "13", "beta", "9"].
54     
55     Comparisons are performed atom-by-atom, numerically if both atoms are
56     numeric. Therefore, "2.12" is greater than "2.4", and "3.0 beta" is
57     greater than "3.0 alpha" (only because "b" > "a"). If an atom is
58     provided in one Version and not another, the longer Version is
59     greater than the shorter, that is: "4.8 alpha" > "4.8".
60     """
61    
62     def __init__(self, atoms):
63         """A Version object. A str argument will be split on word boundaries."""
64         if isinstance(atoms, basestring):
65             self.atoms = re.split(r'\W', atoms)
66         else:
67             self.atoms = [str(x) for x in atoms]
68    
69     def from_http(cls, version_str):
70         """Return a Version object from the given 'HTTP/x.y' string."""
71         return cls(version_str[5:])
72     from_http = classmethod(from_http)
73    
74     def to_http(self):
75         """Return a 'HTTP/x.y' string for this Version object."""
76         return "HTTP/%s.%s" % tuple(self.atoms[:2])
77    
78     def __str__(self):
79         return ".".join([str(x) for x in self.atoms])
80    
81     def __cmp__(self, other):
82         cls = self.__class__
83         if not isinstance(other, cls):
84             # Try to coerce other to a Version instance.
85             other = cls(other)
86        
87         index = 0
88         while index < len(self.atoms) and index < len(other.atoms):
89             mine, theirs = self.atoms[index], other.atoms[index]
90             if mine.isdigit() and theirs.isdigit():
91                 mine, theirs = int(mine), int(theirs)
92             if mine < theirs:
93                 return -1
94             if mine > theirs:
95                 return 1
96             index += 1
97         if index < len(other.atoms):
98             return -1
99         if index < len(self.atoms):
100             return 1
101         return 0
102
103
104 class KeyTitlingDict(dict):
105    
106     """A dict subclass which changes each key to str(key).title()
107     
108     This allows headers to be case-insensitive and avoid duplicates.
109     """
110    
111     def __getitem__(self, key):
112         return dict.__getitem__(self, str(key).title())
113    
114     def __setitem__(self, key, value):
115         dict.__setitem__(self, str(key).title(), value)
116    
117     def __delitem__(self, key):
118         dict.__delitem__(self, str(key).title())
119    
120     def __contains__(self, item):
121         return dict.__contains__(self, str(item).title())
122    
123     def get(self, key, default=None):
124         return dict.get(self, str(key).title(), default)
125    
126     def has_key(self, key):
127         return dict.has_key(self, str(key).title())
128    
129     def update(self, E):
130         for k in E.keys():
131             self[str(k).title()] = E[k]
132    
133     def fromkeys(cls, seq, value=None):
134         newdict = cls()
135         for k in seq:
136             newdict[str(k).title()] = value
137         return newdict
138     fromkeys = classmethod(fromkeys)
139    
140     def setdefault(self, key, x=None):
141         key = str(key).title()
142         try:
143             return self[key]
144         except KeyError:
145             self[key] = x
146             return x
147    
148     def pop(self, key, default):
149         return dict.pop(self, str(key).title(), default)
150
151
152 class Request(object):
153    
154     """Process an HTTP request and set cherrypy.response attributes."""
155    
156     def __init__(self, clientAddress, remoteHost, requestLine, headers,
157                  rfile, scheme="http"):
158         """Populate a new Request object.
159         
160         clientAddress should be a tuple of client IP address, client Port
161         remoteHost should be string of the client's IP address.
162         requestLine should be of the form "GET /path HTTP/1.0".
163         headers should be a list of (name, value) tuples.
164         rfile should be a file-like object containing the HTTP request
165             entity.
166         scheme should be a string, either "http" or "https".
167         
168         When __init__ is done, cherrypy.response should have 3 attributes:
169           status, e.g. "200 OK"
170           headers, a list of (name, value) tuples
171           body, an iterable yielding strings
172         
173         Consumer code (HTTP servers) should then access these response
174         attributes to build the outbound stream.
175         
176         """
177        
178         request = cherrypy.request
179         request.method = ""
180         request.requestLine = requestLine.strip()
181         self.parseFirstLine()
182        
183         # Prepare cherrypy.request variables
184         request.remoteAddr = clientAddress[0]
185         request.remotePort = clientAddress[1]
186         request.remoteHost = remoteHost
187         request.paramList = [] # Only used for Xml-Rpc
188         request.headers = headers
189         request.headerMap = KeyTitlingDict()
190         request.simpleCookie = Cookie.SimpleCookie()
191         request.rfile = rfile
192         request.scheme = scheme
193        
194         # Prepare cherrypy.response variables
195         cherrypy.response.status = None
196         cherrypy.response.headers = None
197         cherrypy.response.body = None
198        
199         cherrypy.response.headerMap = KeyTitlingDict()
200         cherrypy.response.headerMap.update({
201             "Content-Type": "text/html",
202             "Server": "CherryPy/" + cherrypy.__version__,
203             "Date": cptools.HTTPDate(),
204             "Set-Cookie": [],
205             "Content-Length": None
206         })
207         cherrypy.response.simpleCookie = Cookie.SimpleCookie()
208        
209         self.run()
210        
211         if request.method == "HEAD":
212             # HEAD requests MUST NOT return a message-body in the response.
213             cherrypy.response.body = []
214        
215         _cputil.getSpecialAttribute("_cpLogAccess")()
216    
217     def parseFirstLine(self):
218         # This has to be done very early in the request process,
219         # because request.path is used for config lookups right away.
220         request = cherrypy.request
221        
222         # Parse first line
223         request.method, path, request.protocol = request.requestLine.split()
224         request.processRequestBody = request.method in ("POST", "PUT")
225        
226         # separate the queryString, or set it to "" if not found
227         if "?" in path:
228             path, request.queryString = path.split("?", 1)
229         else:
230             path, request.queryString = path, ""
231        
232         # Unquote the path (e.g. "/this%20path" -> "this path").
233         # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
234         # Note that cgi.parse_qs will decode the querystring for us.
235         path = urllib.unquote(path)
236        
237         if path == "*":
238             # "...the request does not apply to a particular resource,
239             # but to the server itself". See
240             # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
241             path = "global"
242         elif not path.startswith("/"):
243             # path is an absolute path (including "http://host.domain.tld");
244             # convert it to a relative path, so configMap lookups work. This
245             # default method assumes all hosts are valid for this server.
246             scheme, location, p, pm, q, f = urlparse(path)
247             path = path[len(scheme + "://" + location):]
248        
249         # Save original value (in case it gets modified by filters)
250         request.path = request.originalPath = path
251        
252         # Change objectPath in filters to change
253         # the object that will get rendered
254         request.objectPath = None
255    
256     def run(self):
257         """Process the Request."""
258         try:
259             try:
260                 applyFilters('onStartResource')
261                
262                 try:
263                     self.processRequestHeaders()
264                    
265                     applyFilters('beforeRequestBody')
266                     if cherrypy.request.processRequestBody:
267                         self.processRequestBody()
268                    
269                     applyFilters('beforeMain')
270                     if cherrypy.response.body is None:
271                         main()
272                    
273                     applyFilters('beforeFinalize')
274                     finalize()
275                 except cherrypy.RequestHandled:
276                     pass
277                 except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst:
278                     # For an HTTPRedirect or HTTPError (including NotFound),
279                     # we don't go through the regular mechanism:
280                     # we return the redirect or error page immediately
281                     inst.set_response()
282                     applyFilters('beforeFinalize')
283                     finalize()
284             finally:
285                 applyFilters('onEndResource')
286         except (KeyboardInterrupt, SystemExit):
287             raise
288         except:
289             handleError(sys.exc_info())
290    
291     def processRequestHeaders(self):
292         request = cherrypy.request
293        
294         # Compare request and server HTTP versions, in case our server does
295         # not support the requested version. We can't tell the server what
296         # version number to write in the response, so we limit our output
297         # to min(req, server). We want the following output:
298         #     request    server     actual written   supported response
299         #     version    version   response version  feature set (resp.v)
300         # a     1.0        1.0           1.0                1.0
301         # b     1.0        1.1           1.1                1.0
302         # c     1.1        1.0           1.0                1.0
303         # d     1.1        1.1           1.1                1.1
304         # Notice that, in (b), the response will be "HTTP/1.1" even though
305         # the client only understands 1.0. RFC 2616 10.5.6 says we should
306         # only return 505 if the _major_ version is different.
307         request_v = Version.from_http(request.protocol)
308         server_v = cherrypy.config.get("server.protocolVersion", "HTTP/1.0")
309         server_v = Version.from_http(server_v)
310         # cherrypy.response.version should be used to determine whether or
311         # not to include a given HTTP/1.1 feature in the response content.
312         cherrypy.response.version = min(request_v, server_v)
313         # cherrypy.request.version == request.protocol in a Version instance.
314         cherrypy.request.version = request_v
315        
316         # build a paramMap dictionary from queryString
317         if re.match(r"[0-9]+,[0-9]+", request.queryString):
318             # Server-side image map. Map the coords to 'x' and 'y'
319             # (like CGI::Request does).
320             pm = request.queryString.split(",")
321             pm = {'x': int(pm[0]), 'y': int(pm[1])}
322         else:
323             pm = cgi.parse_qs(request.queryString, keep_blank_values=True)
324             for key, val in pm.items():
325                 if len(val) == 1:
326                     pm[key] = val[0]
327         request.paramMap = pm
328        
329         # Process the headers into request.headerMap
330         for name, value in request.headers:
331             value = value.strip()
332             # Warning: if there is more than one header entry for cookies (AFAIK,
333             # only Konqueror does that), only the last one will remain in headerMap
334             # (but they will be correctly stored in request.simpleCookie).
335             request.headerMap[name] = value
336            
337             # Handle cookies differently because on Konqueror, multiple
338             # cookies come on different lines with the same key
339             if name.title() == 'Cookie':
340                 request.simpleCookie.load(value)
341        
342         # Write a message to the error.log only if there is no access.log.
343         # This is only here for backwards-compatibility (with the time
344         # before the access.log existed), and should be removed in CP 2.2.
345         fname = cherrypy.config.get('server.logAccessFile', '')
346         if not fname:
347             msg = "%s - %s" % (request.remoteAddr, request.requestLine)
348             cherrypy.log(msg, "HTTP")
349        
350         # Save original values (in case they get modified by filters)
351         request.originalParamMap = request.paramMap
352         request.originalParamList = request.paramList
353        
354         if cherrypy.response.version >= "1.1":
355             # All Internet-based HTTP/1.1 servers MUST respond with a 400
356             # (Bad Request) status code to any HTTP/1.1 request message
357             # which lacks a Host header field.
358             if not request.headerMap.has_key("Host"):
359                 msg = "HTTP/1.1 requires a 'Host' request header."
360                 raise cherrypy.HTTPError(400, msg)
361         request.base = "%s://%s" % (request.scheme, request.headerMap.get('Host', ''))
362         request.browserUrl = request.base + request.path
363         if request.queryString:
364             request.browserUrl += '?' + request.queryString
365    
366     def processRequestBody(self):
367         request = cherrypy.request
368        
369         # Create a copy of headerMap with lowercase keys because
370         # FieldStorage doesn't work otherwise
371         lowerHeaderMap = {}
372         for key, value in request.headerMap.items():
373             lowerHeaderMap[key.lower()] = value
374        
375         # FieldStorage only recognizes POST, so fake it.
376         methenv = {'REQUEST_METHOD': "POST"}
377         try:
378             forms = _cpcgifs.FieldStorage(fp=request.rfile,
379                                       headers=lowerHeaderMap,
380                                       environ=methenv,
381                                       keep_blank_values=1)
382         except _cpwsgiserver.MaxSizeExceeded:
383             # Post data is too big
384             raise _cperror.HTTPError(413)
385        
386         if forms.file:
387             # request body was a content-type other than form params.
388             cherrypy.request.body = forms.file
389         else:
390             for key in forms.keys():
391                 valueList = forms[key]
392                 if isinstance(valueList, list):
393                     request.paramMap[key] = []
394                     for item in valueList:
395                         if item.filename is not None:
396                             value = item # It's a file upload
397                         else:
398                             value = item.value # It's a regular field
399                         request.paramMap[key].append(value)
400                 else:
401                     if valueList.filename is not None:
402                         value = valueList # It's a file upload
403                     else:
404                         value = valueList.value # It's a regular field
405                     request.paramMap[key] = value
406
407
408 # Error handling
409
410 dbltrace = """
411 =====First Error=====
412
413 %s
414
415 =====Second Error=====
416
417 %s
418
419 """
420
421 def handleError(exc):
422     """Set status, headers, and body when an unanticipated error occurs."""
423     try:
424         applyFilters('beforeErrorResponse')
425        
426         # _cpOnError will probably change cherrypy.response.body.
427         # It may also change the headerMap, etc.
428         _cputil.getSpecialAttribute('_cpOnError')()
429        
430         finalize()
431        
432         applyFilters('afterErrorResponse')
433         return
434     except (cherrypy.HTTPRedirect, cherrypy.HTTPError), inst:
435         try:
436             inst.set_response()
437             finalize()
438             return
439         except (KeyboardInterrupt, SystemExit):
440             raise
441         except:
442             # Fall through to the second error handler
443             pass
444     except (KeyboardInterrupt, SystemExit):
445         raise
446     except:
447         # Fall through to the second error handler
448         pass
449    
450     # Failure in _cpOnError, error filter, or finalize.
451     # Bypass them all.
452     defaultOn = (cherrypy.config.get('server.environment') == 'development')
453     if cherrypy.config.get('server.showTracebacks', defaultOn):
454         body = dbltrace % (_cputil.formatExc(exc), _cputil.formatExc())
455     else:
456         body = ""
457     response = cherrypy.response
458     response.status, response.headers, response.body = bareError(body)
459
460 def bareError(extrabody=None):
461     """Produce status, headers, body for a critical error.
462     
463     Returns a triple without calling any other questionable functions,
464     so it should be as error-free as possible. Call it from an HTTP server
465     if you get errors after Request() is done.
466     
467     If extrabody is None, a friendly but rather unhelpful error message
468     is set in the body. If extrabody is a string, it will be appended
469     as-is to the body.
470     """
471    
472     # The whole point of this function is to be a last line-of-defense
473     # in handling errors. That is, it must not raise any errors itself;
474     # it cannot be allowed to fail. Therefore, don't add to it!
475     # In particular, don't call any other CP functions.
476
477     body = "Unrecoverable error in the server."
478     if extrabody is not None:
479         body += "\n" + extrabody
480    
481     return ("500 Internal Server Error",
482         [('Content-Type', 'text/plain'),
483          ('Content-Length', str(len(body)))],
484         [body])
485
486
487
488 # Response functions
489
490 def main(path=None):
491     """Obtain and set cherrypy.response.body from a page handler."""
492     if path is None:
493         path = cherrypy.request.objectPath or cherrypy.request.path
494    
495     while True:
496         try:
497             page_handler, object_path, virtual_path = mapPathToObject(path)
498            
499             # Remove "root" from object_path and join it to get objectPath
500             cherrypy.request.objectPath = '/' + '/'.join(object_path[1:])
501             args = virtual_path + cherrypy.request.paramList
502             body = page_handler(*args, **cherrypy.request.paramMap)
503             cherrypy.response.body = iterable(body)
504             return
505         except cherrypy.InternalRedirect, x:
506             # Try again with the new path
507             path = x.path
508
509 def iterable(body):
510     """Convert the given body to an iterable object."""
511     if isinstance(body, types.FileType):
512         body = cptools.fileGenerator(body)
513     elif isinstance(body, types.GeneratorType):
514         body = flattener(body)
515     elif isinstance(body, basestring):
516         # strings get wrapped in a list because iterating over a single
517         # item list is much faster than iterating over every character
518         # in a long string.
519