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

root/tags/cherrypy-3.0.0/cherrypy/lib/http.py

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

Fixed serious buglet in SizeCheckWrapper? (allowed unmonitored read).

  • Property svn:eol-style set to native
Line 
1 """HTTP library functions."""
2
3 # This module contains functions for building an HTTP application
4 # framework: any one, not just one whose name starts with "Ch". ;) If you
5 # reference any modules from some popular framework inside *this* module,
6 # FuManChu will personally hang you up by your thumbs and submit you
7 # to a public caning.
8
9 from BaseHTTPServer import BaseHTTPRequestHandler
10 response_codes = BaseHTTPRequestHandler.responses.copy()
11
12 # From http://www.cherrypy.org/ticket/361
13 response_codes[500] = ('Internal Server Error',
14                       'The server encountered an unexpected condition '
15                       'which prevented it from fulfilling the request.')
16 response_codes[503] = ('Service Unavailable',
17                       'The server is currently unable to handle the '
18                       'request due to a temporary overloading or '
19                       'maintenance of the server.')
20
21
22 import cgi
23 from email.Header import Header, decode_header
24 import re
25 import rfc822
26 HTTPDate = rfc822.formatdate
27 import time
28
29
30 def urljoin(*atoms):
31     url = "/".join(atoms)
32     while "//" in url:
33         url = url.replace("//", "/")
34     return url
35
36 def protocol_from_http(protocol_str):
37     """Return a protocol tuple from the given 'HTTP/x.y' string."""
38     return int(protocol_str[5]), int(protocol_str[7])
39
40 def get_ranges(headervalue, content_length):
41     """Return a list of (start, stop) indices from a Range header, or None.
42     
43     Each (start, stop) tuple will be composed of two ints, which are suitable
44     for use in a slicing operation. That is, the header "Range: bytes=3-6",
45     if applied against a Python string, is requesting resource[3:7]. This
46     function will return the list [(3, 7)].
47     
48     If this function return an empty list, you should return HTTP 416.
49     """
50    
51     if not headervalue:
52         return None
53    
54     result = []
55     bytesunit, byteranges = headervalue.split("=", 1)
56     for brange in byteranges.split(","):
57         start, stop = [x.strip() for x in brange.split("-", 1)]
58         if start:
59             if not stop:
60                 stop = content_length - 1
61             start, stop = map(int, (start, stop))
62             if start >= content_length:
63                 # From rfc 2616 sec 14.16:
64                 # "If the server receives a request (other than one
65                 # including an If-Range request-header field) with an
66                 # unsatisfiable Range request-header field (that is,
67                 # all of whose byte-range-spec values have a first-byte-pos
68                 # value greater than the current length of the selected
69                 # resource), it SHOULD return a response code of 416
70                 # (Requested range not satisfiable)."
71                 continue
72             if stop < start:
73                 # From rfc 2616 sec 14.16:
74                 # "If the server ignores a byte-range-spec because it
75                 # is syntactically invalid, the server SHOULD treat
76                 # the request as if the invalid Range header field
77                 # did not exist. (Normally, this means return a 200
78                 # response containing the full entity)."
79                 return None
80             result.append((start, stop + 1))
81         else:
82             if not stop:
83                 # See rfc quote above.
84                 return None
85             # Negative subscript (last N bytes)
86             result.append((content_length - int(stop), content_length))
87    
88     return result
89
90
91 class HeaderElement(object):
92     """An element (with parameters) from an HTTP header's element list."""
93    
94     def __init__(self, value, params=None):
95         self.value = value
96         if params is None:
97             params = {}
98         self.params = params
99    
100     def __unicode__(self):
101         p = [";%s=%s" % (k, v) for k, v in self.params.iteritems()]
102         return u"%s%s" % (self.value, "".join(p))
103    
104     def __str__(self):
105         return str(self.__unicode__())
106    
107     def parse(elementstr):
108         """Transform 'token;key=val' to ('token', {'key': 'val'})."""
109         # Split the element into a value and parameters. The 'value' may
110         # be of the form, "token=token", but we don't split that here.
111         atoms = [x.strip() for x in elementstr.split(";")]
112         initial_value = atoms.pop(0).strip()
113         params = {}
114         for atom in atoms:
115             atom = [x.strip() for x in atom.split("=", 1) if x.strip()]
116             key = atom.pop(0)
117             if atom:
118                 val = atom[0]
119             else:
120                 val = ""
121             params[key] = val
122         return initial_value, params
123     parse = staticmethod(parse)
124    
125     def from_str(cls, elementstr):
126         """Construct an instance from a string of the form 'token;key=val'."""
127         ival, params = cls.parse(elementstr)
128         return cls(ival, params)
129     from_str = classmethod(from_str)
130
131
132 q_separator = re.compile(r'; *q *=')
133
134 class AcceptElement(HeaderElement):
135     """An element (with parameters) from an Accept-* header's element list."""
136    
137     def from_str(cls, elementstr):
138         qvalue = None
139         # The first "q" parameter (if any) separates the initial
140         # parameter(s) (if any) from the accept-params.
141         atoms = q_separator.split(elementstr, 1)
142         initial_value = atoms.pop(0).strip()
143         if atoms:
144             # The qvalue for an Accept header can have extensions. The other
145             # headers cannot, but it's easier to parse them as if they did.
146             qvalue = HeaderElement.from_str(atoms[0].strip())
147        
148         ival, params = cls.parse(initial_value)
149         if qvalue is not None:
150             params["q"] = qvalue
151         return cls(ival, params)
152     from_str = classmethod(from_str)
153    
154     def qvalue(self):
155         val = self.params.get("q", "1")
156         if isinstance(val, HeaderElement):
157             val = val.value
158         return float(val)
159     qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
160    
161     def __cmp__(self, other):
162         # If you sort a list of AcceptElement objects, they will be listed
163         # in priority order; the most preferred value will be first.
164         diff = cmp(other.qvalue, self.qvalue)
165         if diff == 0:
166             diff = cmp(str(other), str(self))
167         return diff
168
169
170 def header_elements(fieldname, fieldvalue):
171     """Return a HeaderElement list from a comma-separated header str."""
172    
173     if not fieldvalue:
174         return None
175     headername = fieldname.lower()
176    
177     result = []
178     for element in fieldvalue.split(","):
179         if headername.startswith("accept") or headername == 'te':
180             hv = AcceptElement.from_str(element)
181         else:
182             hv = HeaderElement.from_str(element)
183         result.append(hv)
184    
185     result.sort()
186     return result
187
188 def decode_TEXT(value):
189     """Decode RFC-2047 TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> u"f\xfcr")."""
190     atoms = decode_header(value)
191     decodedvalue = ""
192     for atom, charset in atoms:
193         if charset is not None:
194             atom = atom.decode(charset)
195         decodedvalue += atom
196     return decodedvalue
197
198 def valid_status(status):
199     """Return legal HTTP status Code, Reason-phrase and Message.
200     
201     The status arg must be an int, or a str that begins with an int.
202     
203     If status is an int, or a str and  no reason-phrase is supplied,
204     a default reason-phrase will be provided.
205     """
206    
207     if not status:
208         status = 200
209    
210     status = str(status)
211     parts = status.split(" ", 1)
212     if len(parts) == 1:
213         # No reason supplied.
214         code, = parts
215         reason = None
216     else:
217         code, reason = parts
218         reason = reason.strip()
219    
220     try:
221         code = int(code)
222     except ValueError:
223         raise ValueError("Illegal response status from server "
224                          "(%s is non-numeric)." % repr(code))
225    
226     if code < 100 or code > 599:
227         raise ValueError("Illegal response status from server "
228                          "(%s is out of range)." % repr(code))
229    
230     if code not in response_codes:
231         # code is unknown but not illegal
232         default_reason, message = "", ""
233     else:
234         default_reason, message = response_codes[code]
235    
236     if reason is None:
237         reason = default_reason
238    
239     return code, reason, message
240
241
242 image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
243
244 def parse_query_string(query_string, keep_blank_values=True):
245     """Build a params dictionary from a query_string."""
246     if image_map_pattern.match(query_string):
247         # Server-side image map. Map the coords to 'x' and 'y'
248         # (like CGI::Request does).
249         pm = query_string.split(",")
250         pm = {'x': int(pm[0]), 'y': int(pm[1])}
251     else:
252         pm = cgi.parse_qs(query_string, keep_blank_values)
253         for key, val in pm.items():
254             if len(val) == 1:
255                 pm[key] = val[0]
256     return pm
257
258 def params_from_CGI_form(form):
259     params = {}
260     for key in form.keys():
261         value_list = form[key]
262         if isinstance(value_list, list):
263             params[key] = []
264             for item in value_list:
265                 if item.filename is not None:
266                     value = item # It's a file upload
267                 else:
268                     value = item.value # It's a regular field
269                 params[key].append(value)
270         else:
271             if value_list.filename is not None:
272                 value = value_list # It's a file upload
273             else:
274                 value = value_list.value # It's a regular field
275             params[key] = value
276     return params
277
278
279 class CaseInsensitiveDict(dict):
280     """A case-insensitive dict subclass.
281     
282     Each key is changed on entry to str(key).title().
283     """
284    
285     def __getitem__(self, key):
286         return dict.__getitem__(self, str(key).title())
287    
288     def __setitem__(self, key, value):
289         dict.__setitem__(self, str(key).title(), value)
290    
291     def __delitem__(self, key):
292         dict.__delitem__(self, str(key).title())
293    
294     def __contains__(self, key):
295         return dict.__contains__(self, str(key).title())
296    
297     def get(self, key, default=None):
298         return dict.get(self, str(key).title(), default)
299    
300     def has_key(self, key):
301         return dict.has_key(self, str(key).title())
302    
303     def update(self, E):
304         for k in E.keys():
305             self[str(k).title()] = E[k]
306    
307     def fromkeys(cls, seq, value=None):
308         newdict = cls()
309         for k in seq:
310             newdict[str(k).title()] = value
311         return newdict
312     fromkeys = classmethod(fromkeys)
313    
314     def setdefault(self, key, x=None):
315         key = str(key).title()
316         try:
317             return self[key]
318         except KeyError:
319             self[key] = x
320             return x
321    
322     def pop(self, key, default):
323         return dict.pop(self, str(key).title(), default)
324
325
326 class HeaderMap(CaseInsensitiveDict):
327     """A dict subclass for HTTP request and response headers.
328     
329     Each key is changed on entry to str(key).title(). This allows headers
330     to be case-insensitive and avoid duplicates.
331     """
332    
333     def elements(self, key):
334         """Return a list of HeaderElements for the given header (or None)."""
335         key = str(key).title()
336         h = self.get(key)
337         if h is None:
338             return []
339         return header_elements(key, h)
340    
341     def output(self, protocol=(1, 1)):
342         """Transform self into a list of (name, value) tuples."""
343         header_list = []
344         for key, v in self.iteritems():
345             if isinstance(v, unicode):
346                 # HTTP/1.0 says, "Words of *TEXT may contain octets
347                 # from character sets other than US-ASCII." and
348                 # "Recipients of header field TEXT containing octets
349                 # outside the US-ASCII character set may assume that
350                 # they represent ISO-8859-1 characters."
351                 try:
352                     v = v.encode("iso-8859-1")
353                 except UnicodeEncodeError:
354                     if protocol >= (1, 1):
355                         # Encode RFC-2047 TEXT
356                         # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
357                         v = Header(v, 'utf-8').encode()
358                     else:
359                         raise
360             else:
361                 # This coercion should not take any time at all
362                 # if value is already of type "str".
363                 v = str(v)
364             header_list.append((key, v))
365         return header_list
366
367
368 class MaxSizeExceeded(Exception):
369     pass
370
371 class SizeCheckWrapper(object):
372     """Wraps a file-like object, raising MaxSizeExceeded if too large."""
373    
374     def __init__(self, rfile, maxlen):
375         self.rfile = rfile
376         self.maxlen = maxlen
377         self.bytes_read = 0
378    
379     def _check_length(self):
380         if self.maxlen and self.bytes_read > self.maxlen:
381             raise MaxSizeExceeded()
382    
383     def read(self, size = None):
384         data = self.rfile.read(size)
385         self.bytes_read += len(data)
386         self._check_length()
387         return data
388    
389     def readline(self, size = None):
390         if size is not None:
391             data = self.rfile.readline(size)
392             self.bytes_read += len(data)
393             self._check_length()
394             return data
395        
396         # User didn't specify a size ...
397         # We read the line in chunks to make sure it's not a 100MB line !
398         res = []
399         while True:
400             data = self.rfile.readline(256)
401             self.bytes_read += len(data)
402             self._check_length()
403             res.append(data)
404             # See http://www.cherrypy.org/ticket/421
405             if len(data) < 256 or data[-1:] == "\n":
406                 return ''.join(res)
407    
408     def readlines(self, sizehint = 0):
409         # Shamelessly stolen from StringIO
410         total = 0
411         lines = []
412         line = self.readline()
413         while line:
414             lines.append(line)
415             total += len(line)
416             if 0 < sizehint <= total:
417                 break
418             line = self.readline()
419         return lines
420    
421     def close(self):
422         self.rfile.close()
423    
424     def __iter__(self):
425         return self
426    
427     def next(self):
428         data = self.rfile.next()
429         self.bytes_read += len(data)
430         self._check_length()
431         return data
432
433
434 class Host(object):
435     """An internet address.
436     
437     name should be the client's host name. If not available (because no DNS
438         lookup is performed), the IP address should be used instead.
439     """
440    
441     ip = "0.0.0.0"
442     port = 80
443     name = "unknown.tld"
444    
445     def __init__(self, ip, port, name=None):
446         self.ip = ip
447         self.port = port
448         if name is None:
449             name = ip
450         self.name = name
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets