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
10 from binascii import b2a_base64
11 from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou, reversed, sorted
12 from cherrypy._cpcompat import basestring, bytestr, iteritems, nativestr, unicodestr, unquote_qs
13 response_codes = BaseHTTPRequestHandler.responses.copy()
14
15
16 response_codes[500] = ('Internal Server Error',
17 'The server encountered an unexpected condition '
18 'which prevented it from fulfilling the request.')
19 response_codes[503] = ('Service Unavailable',
20 'The server is currently unable to handle the '
21 'request due to a temporary overloading or '
22 'maintenance of the server.')
23
24 import re
25 import urllib
26
27
28
30 """Return the given path \*atoms, joined into a single URL.
31
32 This will correctly join a SCRIPT_NAME and PATH_INFO into the
33 original URL, even if either atom is blank.
34 """
35 url = "/".join([x for x in atoms if x])
36 while "//" in url:
37 url = url.replace("//", "/")
38
39 return url or "/"
40
42 """Return the given path *atoms, joined into a single URL.
43
44 This will correctly join a SCRIPT_NAME and PATH_INFO into the
45 original URL, even if either atom is blank.
46 """
47 url = ntob("/").join([x for x in atoms if x])
48 while ntob("//") in url:
49 url = url.replace(ntob("//"), ntob("/"))
50
51 return url or ntob("/")
52
54 """Return a protocol tuple from the given 'HTTP/x.y' string."""
55 return int(protocol_str[5]), int(protocol_str[7])
56
58 """Return a list of (start, stop) indices from a Range header, or None.
59
60 Each (start, stop) tuple will be composed of two ints, which are suitable
61 for use in a slicing operation. That is, the header "Range: bytes=3-6",
62 if applied against a Python string, is requesting resource[3:7]. This
63 function will return the list [(3, 7)].
64
65 If this function returns an empty list, you should return HTTP 416.
66 """
67
68 if not headervalue:
69 return None
70
71 result = []
72 bytesunit, byteranges = headervalue.split("=", 1)
73 for brange in byteranges.split(","):
74 start, stop = [x.strip() for x in brange.split("-", 1)]
75 if start:
76 if not stop:
77 stop = content_length - 1
78 start, stop = int(start), int(stop)
79 if start >= content_length:
80
81
82
83
84
85
86
87
88 continue
89 if stop < start:
90
91
92
93
94
95
96 return None
97 result.append((start, stop + 1))
98 else:
99 if not stop:
100
101 return None
102
103 result.append((content_length - int(stop), content_length))
104
105 return result
106
107
109 """An element (with parameters) from an HTTP header's element list."""
110
116
118 return cmp(self.value, other.value)
119
121 return self.value < other.value
122
124 p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)]
125 return "%s%s" % (self.value, "".join(p))
126
129
132
134 """Transform 'token;key=val' to ('token', {'key': 'val'})."""
135
136
137 atoms = [x.strip() for x in elementstr.split(";") if x.strip()]
138 if not atoms:
139 initial_value = ''
140 else:
141 initial_value = atoms.pop(0).strip()
142 params = {}
143 for atom in atoms:
144 atom = [x.strip() for x in atom.split("=", 1) if x.strip()]
145 key = atom.pop(0)
146 if atom:
147 val = atom[0]
148 else:
149 val = ""
150 params[key] = val
151 return initial_value, params
152 parse = staticmethod(parse)
153
155 """Construct an instance from a string of the form 'token;key=val'."""
156 ival, params = cls.parse(elementstr)
157 return cls(ival, params)
158 from_str = classmethod(from_str)
159
160
161 q_separator = re.compile(r'; *q *=')
162
164 """An element (with parameters) from an Accept* header's element list.
165
166 AcceptElement objects are comparable; the more-preferred object will be
167 "less than" the less-preferred object. They are also therefore sortable;
168 if you sort a list of AcceptElement objects, they will be listed in
169 priority order; the most preferred value will be first. Yes, it should
170 have been the other way around, but it's too late to fix now.
171 """
172
188 from_str = classmethod(from_str)
189
191 val = self.params.get("q", "1")
192 if isinstance(val, HeaderElement):
193 val = val.value
194 return float(val)
195 qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
196
202
208
209
211 """Return a sorted HeaderElement list from a comma-separated header string."""
212 if not fieldvalue:
213 return []
214
215 result = []
216 for element in fieldvalue.split(","):
217 if fieldname.startswith("Accept") or fieldname == 'TE':
218 hv = AcceptElement.from_str(element)
219 else:
220 hv = HeaderElement.from_str(element)
221 result.append(hv)
222
223 return list(reversed(sorted(result)))
224
225 -def decode_TEXT(value):
226 r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr")."""
227 try:
228
229 from email.header import decode_header
230 except ImportError:
231 from email.Header import decode_header
232 atoms = decode_header(value)
233 decodedvalue = ""
234 for atom, charset in atoms:
235 if charset is not None:
236 atom = atom.decode(charset)
237 decodedvalue += atom
238 return decodedvalue
239
241 """Return legal HTTP status Code, Reason-phrase and Message.
242
243 The status arg must be an int, or a str that begins with an int.
244
245 If status is an int, or a str and no reason-phrase is supplied,
246 a default reason-phrase will be provided.
247 """
248
249 if not status:
250 status = 200
251
252 status = str(status)
253 parts = status.split(" ", 1)
254 if len(parts) == 1:
255
256 code, = parts
257 reason = None
258 else:
259 code, reason = parts
260 reason = reason.strip()
261
262 try:
263 code = int(code)
264 except ValueError:
265 raise ValueError("Illegal response status from server "
266 "(%s is non-numeric)." % repr(code))
267
268 if code < 100 or code > 599:
269 raise ValueError("Illegal response status from server "
270 "(%s is out of range)." % repr(code))
271
272 if code not in response_codes:
273
274 default_reason, message = "", ""
275 else:
276 default_reason, message = response_codes[code]
277
278 if reason is None:
279 reason = default_reason
280
281 return code, reason, message
282
283
284
285
286
287
288 -def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
289 """Parse a query given as a string argument.
290
291 Arguments:
292
293 qs: URL-encoded query string to be parsed
294
295 keep_blank_values: flag indicating whether blank values in
296 URL encoded queries should be treated as blank strings. A
297 true value indicates that blanks should be retained as blank
298 strings. The default false value indicates that blank values
299 are to be ignored and treated as if they were not included.
300
301 strict_parsing: flag indicating what to do with parsing errors. If
302 false (the default), errors are silently ignored. If true,
303 errors raise a ValueError exception.
304
305 Returns a dict, as G-d intended.
306 """
307 pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
308 d = {}
309 for name_value in pairs:
310 if not name_value and not strict_parsing:
311 continue
312 nv = name_value.split('=', 1)
313 if len(nv) != 2:
314 if strict_parsing:
315 raise ValueError("bad query field: %r" % (name_value,))
316
317 if keep_blank_values:
318 nv.append('')
319 else:
320 continue
321 if len(nv[1]) or keep_blank_values:
322 name = unquote_qs(nv[0], encoding)
323 value = unquote_qs(nv[1], encoding)
324 if name in d:
325 if not isinstance(d[name], list):
326 d[name] = [d[name]]
327 d[name].append(value)
328 else:
329 d[name] = value
330 return d
331
332
333 image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
334
336 """Build a params dictionary from a query_string.
337
338 Duplicate key/value pairs in the provided query_string will be
339 returned as {'key': [val1, val2, ...]}. Single key/values will
340 be returned as strings: {'key': 'value'}.
341 """
342 if image_map_pattern.match(query_string):
343
344
345 pm = query_string.split(",")
346 pm = {'x': int(pm[0]), 'y': int(pm[1])}
347 else:
348 pm = _parse_qs(query_string, keep_blank_values, encoding=encoding)
349 return pm
350
351
353 """A case-insensitive dict subclass.
354
355 Each key is changed on entry to str(key).title().
356 """
357
360
363
366
369
370 - def get(self, key, default=None):
372
373 if hasattr({}, 'has_key'):
376
380
382 newdict = cls()
383 for k in seq:
384 newdict[str(k).title()] = value
385 return newdict
386 fromkeys = classmethod(fromkeys)
387
389 key = str(key).title()
390 try:
391 return self[key]
392 except KeyError:
393 self[key] = x
394 return x
395
396 - def pop(self, key, default):
398
399
400
401
402
403
404
405 if nativestr == bytestr:
406 header_translate_table = ''.join([chr(i) for i in xrange(256)])
407 header_translate_deletechars = ''.join([chr(i) for i in xrange(32)]) + chr(127)
408 else:
409 header_translate_table = None
410 header_translate_deletechars = bytes(range(32)) + bytes([127])
411
412
414 """A dict subclass for HTTP request and response headers.
415
416 Each key is changed on entry to str(key).title(). This allows headers
417 to be case-insensitive and avoid duplicates.
418
419 Values are header values (decoded according to :rfc:`2047` if necessary).
420 """
421
422 protocol=(1, 1)
423 encodings = ["ISO-8859-1"]
424
425
426
427
428
429
430 use_rfc_2047 = True
431
433 """Return a sorted list of HeaderElements for the given header."""
434 key = str(key).title()
435 value = self.get(key)
436 return header_elements(key, value)
437
439 """Return a sorted list of HeaderElement.value for the given header."""
440 return [e.value for e in self.elements(key)]
441
462
464 """Return the given header name or value, encoded for HTTP output."""
465 for enc in self.encodings:
466 try:
467 return v.encode(enc)
468 except UnicodeEncodeError:
469 continue
470
471 if self.protocol == (1, 1) and self.use_rfc_2047:
472
473
474
475
476
477 v = b2a_base64(v.encode('utf-8'))
478 return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
479
480 raise ValueError("Could not encode header part %r using "
481 "any of the encodings %r." %
482 (v, self.encodings))
483
484
486 """An internet address.
487
488 name
489 Should be the client's host name. If not available (because no DNS
490 lookup is performed), the IP address should be used instead.
491
492 """
493
494 ip = "0.0.0.0"
495 port = 80
496 name = "unknown.tld"
497
498 - def __init__(self, ip, port, name=None):
504
506 return "httputil.Host(%r, %r, %r)" % (self.ip, self.port, self.name)
507