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