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

root/trunk/cherrypy/lib/http.py

Revision 1994 (checked in by fumanchu, 6 days ago)

Fix for #803 (run CP under Google App Engine).

  • 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 import re
24 from rfc822 import formatdate as HTTPDate
25
26
27 def urljoin(*atoms):
28     """Return the given path *atoms, joined into a single URL.
29     
30     This will correctly join a SCRIPT_NAME and PATH_INFO into the
31     original URL, even if either atom is blank.
32     """
33     url = "/".join([x for x in atoms if x])
34     while "//" in url:
35         url = url.replace("//", "/")
36     # Special-case the final url of "", and return "/" instead.
37     return url or "/"
38
39 def protocol_from_http(protocol_str):
40     """Return a protocol tuple from the given 'HTTP/x.y' string."""
41     return int(protocol_str[5]), int(protocol_str[7])
42
43 def get_ranges(headervalue, content_length):
44     """Return a list of (start, stop) indices from a Range header, or None.
45     
46     Each (start, stop) tuple will be composed of two ints, which are suitable
47     for use in a slicing operation. That is, the header "Range: bytes=3-6",
48     if applied against a Python string, is requesting resource[3:7]. This
49     function will return the list [(3, 7)].
50     
51     If this function returns an empty list, you should return HTTP 416.
52     """
53    
54     if not headervalue:
55         return None
56    
57     result = []
58     bytesunit, byteranges = headervalue.split("=", 1)
59     for brange in byteranges.split(","):
60         start, stop = [x.strip() for x in brange.split("-", 1)]
61         if start:
62             if not stop:
63                 stop = content_length - 1
64             start, stop = map(int, (start, stop))
65             if start >= content_length:
66                 # From rfc 2616 sec 14.16:
67                 # "If the server receives a request (other than one
68                 # including an If-Range request-header field) with an
69                 # unsatisfiable Range request-header field (that is,
70                 # all of whose byte-range-spec values have a first-byte-pos
71                 # value greater than the current length of the selected
72                 # resource), it SHOULD return a response code of 416
73                 # (Requested range not satisfiable)."
74                 continue
75             if stop < start:
76                 # From rfc 2616 sec 14.16:
77                 # "If the server ignores a byte-range-spec because it
78                 # is syntactically invalid, the server SHOULD treat
79                 # the request as if the invalid Range header field
80                 # did not exist. (Normally, this means return a 200
81                 # response containing the full entity)."
82                 return None
83             result.append((start, stop + 1))
84         else:
85             if not stop:
86                 # See rfc quote above.
87                 return None
88             # Negative subscript (last N bytes)
89             result.append((content_length - int(stop), content_length))
90    
91     return result
92
93
94 class HeaderElement(object):
95     """An element (with parameters) from an HTTP header's element list."""
96    
97     def __init__(self, value, params=None):
98         self.value = value
99         if params is None:
100             params = {}
101         self.params = params
102    
103     def __unicode__(self):
104         p = [";%s=%s" % (k, v) for k, v in self.params.iteritems()]
105         return u"%s%s" % (self.value, "".join(p))
106    
107     def __str__(self):
108         return str(self.__unicode__())
109    
110     def parse(elementstr):
111         """Transform 'token;key=val' to ('token', {'key': 'val'})."""
112         # Split the element into a value and parameters. The 'value' may
113         # be of the form, "token=token", but we don't split that here.
114         atoms = [x.strip() for x in elementstr.split(";") if x.strip()]
115         initial_value = atoms.pop(0).strip()
116         params = {}
117         for atom in atoms:
118             atom = [x.strip() for x in atom.split("=", 1) if x.strip()]
119             key = atom.pop(0)
120             if atom:
121                 val = atom[0]
122             else:
123                 val = ""
124             params[key] = val
125         return initial_value, params
126     parse = staticmethod(parse)
127    
128     def from_str(cls, elementstr):
129         """Construct an instance from a string of the form 'token;key=val'."""
130         ival, params = cls.parse(elementstr)
131         return cls(ival, params)
132     from_str = classmethod(from_str)
133
134
135 q_separator = re.compile(r'; *q *=')
136
137 class AcceptElement(HeaderElement):
138     """An element (with parameters) from an Accept* header's element list.
139     
140     AcceptElement objects are comparable; the more-preferred object will be
141     "less than" the less-preferred object. They are also therefore sortable;
142     if you sort a list of AcceptElement objects, they will be listed in
143     priority order; the most preferred value will be first. Yes, it should
144     have been the other way around, but it's too late to fix now.
145     """
146    
147     def from_str(cls, elementstr):
148         qvalue = None
149         # The first "q" parameter (if any) separates the initial
150         # media-range parameter(s) (if any) from the accept-params.
151         atoms = q_separator.split(elementstr, 1)
152         media_range = atoms.pop(0).strip()
153         if atoms:
154             # The qvalue for an Accept header can have extensions. The other
155             # headers cannot, but it's easier to parse them as if they did.
156             qvalue = HeaderElement.from_str(atoms[0].strip())
157        
158         media_type, params = cls.parse(media_range)
159         if qvalue is not None:
160             params["q"] = qvalue
161         return cls(media_type, params)
162     from_str = classmethod(from_str)
163    
164     def qvalue(self):
165         val = self.params.get("q", "1")
166         if isinstance(val, HeaderElement):
167             val = val.value
168         return float(val)
169     qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
170    
171     def __cmp__(self, other):
172         diff = cmp(other.qvalue, self.qvalue)
173         if diff == 0:
174             diff = cmp(str(other), str(self))
175         return diff
176
177
178 def header_elements(fieldname, fieldvalue):
179     """Return a HeaderElement list from a comma-separated header str."""
180    
181     if not fieldvalue:
182         return None
183     headername = fieldname.lower()
184    
185     result = []
186     for element in fieldvalue.split(","):
187         if headername.startswith("accept") or headername == 'te':
188             hv = AcceptElement.from_str(element)
189         else:
190             hv = HeaderElement.from_str(element)
191         result.append(hv)
192    
193     result.sort()
194     return result
195
196 def decode_TEXT(value):
197     """Decode RFC-2047 TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> u"f\xfcr")."""
198     from email.Header import decode_header
199     atoms = decode_header(value)
200     decodedvalue = ""
201     for atom, charset in atoms:
202         if charset is not None:
203             atom = atom.decode(charset)
204         decodedvalue += atom
205     return decodedvalue
206
207 def valid_status(status):
208     """Return legal HTTP status Code, Reason-phrase and Message.
209     
210     The status arg must be an int, or a str that begins with an int.
211     
212     If status is an int, or a str and  no reason-phrase is supplied,
213     a default reason-phrase will be provided.
214     """
215    
216     if not status:
217         status = 200
218    
219     status = str(status)
220     parts = status.split(" ", 1)
221     if len(parts) == 1:
222         # No reason supplied.
223         code, = parts
224         reason = None
225     else:
226         code, reason = parts
227         reason = reason.strip()
228    
229     try:
230         code = int(code)
231     except ValueError:
232         raise ValueError("Illegal response status from server "
233                          "(%s is non-numeric)." % repr(code))
234    
235     if code < 100 or code > 599:
236         raise ValueError("Illegal response status from server "
237                          "(%s is out of range)." % repr(code))
238    
239     if code not in response_codes:
240         # code is unknown but not illegal
241         default_reason, message = "", ""
242     else:
243         default_reason, message = response_codes[code]
244    
245     if reason is None:
246         reason = default_reason
247    
248     return code, reason, message
249
250
251 image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
252
253 def parse_query_string(query_string, keep_blank_values=True):
254     """Build a params dictionary from a query_string."""
255     if image_map_pattern.match(query_string):
256         # Server-side image map. Map the coords to 'x' and 'y'
257         # (like CGI::Request does).
258         pm = query_string.split(",")
259         pm = {'x': int(pm[0]), 'y': int(pm[1])}
260     else:
261         pm = cgi.parse_qs(query_string, keep_blank_values)
262         for key, val in pm.items():
263             if len(val) == 1:
264                 pm[key] = val[0]
265     return pm
266
267 def params_from_CGI_form(form):
268     params = {}
269     for key in form.keys():
270         value_list = form[key]
271         if isinstance(value_list, list):
272             params[key] = []
273             for item in value_list:
274                 if item.filename is not None:
275                     value = item # It's a file upload
276                 else:
277                     value = item.value # It's a regular field
278                 params[key].append(value)
279         else:
280             if value_list.filename is not None:
281                 value = value_list # It's a file upload
282             else:
283                 value = value_list.value # It's a regular field
284             params[key] = value
285     return params
286
287
288 class CaseInsensitiveDict(dict):
289     """A case-insensitive dict subclass.
290     
291     Each key is changed on entry to str(key).title().
292     """
293    
294     def __getitem__(self, key):
295         return dict.__getitem__(self, str(key).title())
296    
297     def __setitem__(self, key, value):
298         dict.__setitem__(self, str(key).title(), value)
299    
300     def __delitem__(self, key):
301         dict.__delitem__(self, str(key).title())
302    
303     def __contains__(self, key):
304         return dict.__contains__(self, str(key).title())
305    
306     def get(self, key, default=None):
307         return dict.get(self, str(key).title(), default)
308    
309     def has_key(self, key):
310         return dict.has_key(self, str(key).title())
311    
312     def update(self, E):
313         for k in E.keys():
314             self[str(k).title()] = E[k]
315    
316     def fromkeys(cls, seq, value=None):
317         newdict = cls()
318         for k in seq:
319             newdict[str(k).title()] = value
320         return newdict
321     fromkeys = classmethod(fromkeys)
322    
323     def setdefault(self, key, x=None):
324         key = str(key).title()
325         try:
326             return self[key]
327         except KeyError:
328             self[key] = x
329             return x
330    
331     def pop(self, key, default):
332         return dict.pop(self, str(key).title(), default)
333
334
335 class HeaderMap(CaseInsensitiveDict):
336     """A dict subclass for HTTP request and response headers.
337     
338     Each key is changed on entry to str(key).title(). This allows headers
339     to be case-insensitive and avoid duplicates.
340     
341     Values are header values (decoded according to RFC 2047 if necessary).
342     """
343    
344     def elements(self, key):
345         """Return a list of HeaderElements for the given header (or None)."""
346         key = str(key).title()
347         h = self.get(key)
348         if h is None:
349             return []
350         return header_elements(key, h)
351    
352     def output(self, protocol=(1, 1)):
353         """Transform self into a list of (name, value) tuples."""
354         header_list = []
355         for key, v in self.iteritems():
356             if isinstance(v, unicode):
357                 # HTTP/1.0 says, "Words of *TEXT may contain octets
358                 # from character sets other than US-ASCII." and
359                 # "Recipients of header field TEXT containing octets
360                 # outside the US-ASCII character set may assume that
361                 # they represent ISO-8859-1 characters."
362                 try:
363                     v = v.encode("iso-8859-1")
364                 except UnicodeEncodeError:
365                     if protocol >= (1, 1):
366                         # Encode RFC-2047 TEXT
367                         # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
368                         from email.Header import Header
369                         v = Header(v, 'utf-8').encode()
370                     else:
371                         raise
372             else:
373                 # This coercion should not take any time at all
374                 # if value is already of type "str".
375                 v = str(v)
376             header_list.append((key, v))
377         return header_list
378
379
380
381 class Host(object):
382     """An internet address.
383     
384     name should be the client's host name. If not available (because no DNS
385         lookup is performed), the IP address should be used instead.
386     """
387    
388     ip = "0.0.0.0"
389     port = 80
390     name = "unknown.tld"
391    
392     def __init__(self, ip, port, name=None):
393         self.ip = ip
394         self.port = port
395         if name is None:
396             name = ip
397         self.name = name
398    
399     def __repr__(self):
400         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