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

root/branches/cp3-wsgi-remix/lib/http.py

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

Lots of mixedCase to lower_with_underscores.

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

Hosted by WebFaction

Log in as guest/cpguest to create tickets