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

root/trunk/cherrypy/lib/http.py

Revision 2026 (checked in by fumanchu, 3 months ago)

Doc tweaks.

  • 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     
256     Duplicate key/value pairs in the provided query_string will be
257     returned as {'key': [val1, val2, ...]}. Single key/values will
258     be returned as strings: {'key': 'value'}.
259     """
260     if image_map_pattern.match(query_string):
261         # Server-side image map. Map the coords to 'x' and 'y'
262         # (like CGI::Request does).
263         pm = query_string.split(",")
264         pm = {'x': int(pm[0]), 'y': int(pm[1])}
265     else:
266         pm = cgi.parse_qs(query_string, keep_blank_values)
267         for key, val in pm.items():
268             if len(val) == 1:
269                 pm[key] = val[0]
270     return pm
271
272 def params_from_CGI_form(form):
273     params = {}
274     for key in form.keys():
275         value_list = form[key]
276         if isinstance(value_list, list):
277             params[key] = []
278             for item in value_list:
279                 if item.filename is not None:
280                     value = item # It's a file upload
281                 else:
282                     value = item.value # It's a regular field
283                 params[key].append(value)
284         else:
285             if value_list.filename is not None:
286                 value = value_list # It's a file upload
287             else:
288                 value = value_list.value # It's a regular field
289             params[key] = value
290     return params
291
292
293 class CaseInsensitiveDict(dict):
294     """A case-insensitive dict subclass.
295     
296     Each key is changed on entry to str(key).title().
297     """
298    
299     def __getitem__(self, key):
300         return dict.__getitem__(self, str(key).title())
301    
302     def __setitem__(self, key, value):
303         dict.__setitem__(self, str(key).title(), value)
304    
305     def __delitem__(self, key):
306         dict.__delitem__(self, str(key).title())
307    
308     def __contains__(self, key):
309         return dict.__contains__(self, str(key).title())
310    
311     def get(self, key, default=None):
312         return dict.get(self, str(key).title(), default)
313    
314     def has_key(self, key):
315         return dict.has_key(self, str(key).title())
316    
317     def update(self, E):
318         for k in E.keys():
319             self[str(k).title()] = E[k]
320    
321     def fromkeys(cls, seq, value=None):
322         newdict = cls()
323         for k in seq:
324             newdict[str(k).title()] = value
325         return newdict
326     fromkeys = classmethod(fromkeys)
327    
328     def setdefault(self, key, x=None):
329         key = str(key).title()
330         try:
331             return self[key]
332         except KeyError:
333             self[key] = x
334             return x
335    
336     def pop(self, key, default):
337         return dict.pop(self, str(key).title(), default)
338
339
340 class HeaderMap(CaseInsensitiveDict):
341     """A dict subclass for HTTP request and response headers.
342     
343     Each key is changed on entry to str(key).title(). This allows headers
344     to be case-insensitive and avoid duplicates.
345     
346     Values are header values (decoded according to RFC 2047 if necessary).
347     """
348    
349     def elements(self, key):
350         """Return a list of HeaderElements for the given header (or None)."""
351         key = str(key).title()
352         h = self.get(key)
353         if h is None:
354             return []
355         return header_elements(key, h)
356    
357     def output(self, protocol=(1, 1)):
358         """Transform self into a list of (name, value) tuples."""
359         header_list = []
360         for key, v in self.iteritems():
361             if isinstance(v, unicode):
362                 # HTTP/1.0 says, "Words of *TEXT may contain octets
363                 # from character sets other than US-ASCII." and
364                 # "Recipients of header field TEXT containing octets
365                 # outside the US-ASCII character set may assume that
366                 # they represent ISO-8859-1 characters."
367                 try:
368                     v = v.encode("iso-8859-1")
369                 except UnicodeEncodeError:
370                     if protocol >= (1, 1):
371                         # Encode RFC-2047 TEXT
372                         # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
373                         from email.Header import Header
374                         v = Header(v, 'utf-8').encode()
375                     else:
376                         raise
377             else:
378                 # This coercion should not take any time at all
379                 # if value is already of type "str".
380                 v = str(v)
381             header_list.append((key, v))
382         return header_list
383
384
385
386 class Host(object):
387     """An internet address.
388     
389     name should be the client's host name. If not available (because no DNS
390         lookup is performed), the IP address should be used instead.
391     """
392    
393     ip = "0.0.0.0"
394     port = 80
395     name = "unknown.tld"
396    
397     def __init__(self, ip, port, name=None):
398         self.ip = ip
399         self.port = port
400         if name is None:
401             name = ip
402         self.name = name
403    
404     def __repr__(self):
405         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