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