Package cherrypy :: Package lib :: Module http
[hide private]
[frames] | no frames]

Source Code for Module cherrypy.lib.http

  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)
456