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

root/tags/cherrypy-2.1.0-rc1/cherrypy/_cphttptools.py

Revision 650 (checked in by mikerobi, 3 years ago)

implimented changes needed to re-close ticket:288

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