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

root/branches/cherrypy-3.0.x/cherrypy/lib/http.py

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

Docstring updates.

  • 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     Values are header values (decoded according to RFC 2047 if necessary).
333     """
334    
335     def elements(self, key):
336         """Return a list of HeaderElements for the given header (or None)."""
337         key = str(key).title()
338         h = self.get(key)
339         if h is None:
340             return []
341         return header_elements(key, h)
342    
343     def output(self, protocol=(1, 1)):
344         """Transform self into a list of (name, value) tuples."""
345         header_list = []
346         for key, v in self.iteritems():
347             if isinstance(v, unicode):
348                 # HTTP/1.0 says, "Words of *TEXT may contain octets
349                 # from character sets other than US-ASCII." and
350                 # "Recipients of header field TEXT containing octets
351                 # outside the US-ASCII character set may assume that
352                 # they represent ISO-8859-1 characters."
353                 try:
354                     v = v.encode("iso-8859-1")
355                 except UnicodeEncodeError:
356                     if protocol >= (1, 1):
357                         # Encode RFC-2047 TEXT
358                         # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
359                         v = Header(v, 'utf-8').encode()
360                     else:
361                         raise
362             else:
363                 # This coercion should not take any time at all
364                 # if value is already of type "str".
365                 v = str(v)
366             header_list.append((key, v))
367         return header_list
368
369
370 class MaxSizeExceeded(Exception):
371     pass
372
373 class SizeCheckWrapper(object):
374     """Wraps a file-like object, raising MaxSizeExceeded if too large."""
375    
376     def __init__(self, rfile, maxlen):
377         self.rfile = rfile
378         self.maxlen = maxlen
379         self.bytes_read = 0
380    
381     def _check_length(self):
382         if self.maxlen and self.bytes_read > self.maxlen:
383             raise MaxSizeExceeded()
384    
385     def read(self, size = None):
386         data = self.rfile.read(size)
387         self.bytes_read += len(data)
388         self._check_length()
389         return data
390    
391     def readline(self, size = None):
392         if size is not None:
393             data = self.rfile.readline(size)
394             self.bytes_read += len(data)
395             self._check_length()
396             return data
397        
398         # User didn't specify a size ...
399         # We read the line in chunks to make sure it's not a 100MB line !
400         res = []
401         while True:
402             data = self.rfile.readline(256)
403             self.bytes_read += len(data)
404             self._check_length()
405             res.append(data)
406             # See http://www.cherrypy.org/ticket/421
407             if len(data) < 256 or data[-1:] == "\n":
408                 return ''.join(res)
409    
410     def readlines(self, sizehint = 0):
411         # Shamelessly stolen from StringIO
412         total = 0
413         lines = []
414         line = self.readline()
415         while line:
416             lines.append(line)
417             total += len(line)
418             if 0 < sizehint <= total:
419                 break
420             line = self.readline()
421         return lines
422    
423     def close(self):
424         self.rfile.close()
425    
426     def __iter__(self):
427         return self
428    
429     def next(self):
430         data = self.rfile.next()
431         self.bytes_read += len(data)
432         self._check_length()
433         return data
434
435
436 class Host(object):
437     """An internet address.
438     
439     name should be the client's host name. If not available (because no DNS
440         lookup is performed), the IP address should be used instead.
441     """
442    
443     ip = "0.0.0.0"
444     port = 80
445     name = "unknown.tld"
446    
447     def __init__(self, ip, port, name=None):
448         self.ip = ip
449         self.port = port
450         if name is None:
451             name = ip
452         self.name = name
453    
454     def __repr__(self):
455         return "http.Host(%r, %r, %r)" % (self.ip, self.port, self.name)
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets