| Index: third_party/cherrypy/lib/httputil.py
|
| ===================================================================
|
| --- third_party/cherrypy/lib/httputil.py (revision 0)
|
| +++ third_party/cherrypy/lib/httputil.py (revision 0)
|
| @@ -0,0 +1,506 @@
|
| +"""HTTP library functions.
|
| +
|
| +This module contains functions for building an HTTP application
|
| +framework: any one, not just one whose name starts with "Ch". ;) If you
|
| +reference any modules from some popular framework inside *this* module,
|
| +FuManChu will personally hang you up by your thumbs and submit you
|
| +to a public caning.
|
| +"""
|
| +
|
| +from binascii import b2a_base64
|
| +from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou, reversed, sorted
|
| +from cherrypy._cpcompat import basestring, bytestr, iteritems, nativestr, unicodestr, unquote_qs
|
| +response_codes = BaseHTTPRequestHandler.responses.copy()
|
| +
|
| +# From http://www.cherrypy.org/ticket/361
|
| +response_codes[500] = ('Internal Server Error',
|
| + 'The server encountered an unexpected condition '
|
| + 'which prevented it from fulfilling the request.')
|
| +response_codes[503] = ('Service Unavailable',
|
| + 'The server is currently unable to handle the '
|
| + 'request due to a temporary overloading or '
|
| + 'maintenance of the server.')
|
| +
|
| +import re
|
| +import urllib
|
| +
|
| +
|
| +
|
| +def urljoin(*atoms):
|
| + """Return the given path \*atoms, joined into a single URL.
|
| +
|
| + This will correctly join a SCRIPT_NAME and PATH_INFO into the
|
| + original URL, even if either atom is blank.
|
| + """
|
| + url = "/".join([x for x in atoms if x])
|
| + while "//" in url:
|
| + url = url.replace("//", "/")
|
| + # Special-case the final url of "", and return "/" instead.
|
| + return url or "/"
|
| +
|
| +def urljoin_bytes(*atoms):
|
| + """Return the given path *atoms, joined into a single URL.
|
| +
|
| + This will correctly join a SCRIPT_NAME and PATH_INFO into the
|
| + original URL, even if either atom is blank.
|
| + """
|
| + url = ntob("/").join([x for x in atoms if x])
|
| + while ntob("//") in url:
|
| + url = url.replace(ntob("//"), ntob("/"))
|
| + # Special-case the final url of "", and return "/" instead.
|
| + return url or ntob("/")
|
| +
|
| +def protocol_from_http(protocol_str):
|
| + """Return a protocol tuple from the given 'HTTP/x.y' string."""
|
| + return int(protocol_str[5]), int(protocol_str[7])
|
| +
|
| +def get_ranges(headervalue, content_length):
|
| + """Return a list of (start, stop) indices from a Range header, or None.
|
| +
|
| + Each (start, stop) tuple will be composed of two ints, which are suitable
|
| + for use in a slicing operation. That is, the header "Range: bytes=3-6",
|
| + if applied against a Python string, is requesting resource[3:7]. This
|
| + function will return the list [(3, 7)].
|
| +
|
| + If this function returns an empty list, you should return HTTP 416.
|
| + """
|
| +
|
| + if not headervalue:
|
| + return None
|
| +
|
| + result = []
|
| + bytesunit, byteranges = headervalue.split("=", 1)
|
| + for brange in byteranges.split(","):
|
| + start, stop = [x.strip() for x in brange.split("-", 1)]
|
| + if start:
|
| + if not stop:
|
| + stop = content_length - 1
|
| + start, stop = int(start), int(stop)
|
| + if start >= content_length:
|
| + # From rfc 2616 sec 14.16:
|
| + # "If the server receives a request (other than one
|
| + # including an If-Range request-header field) with an
|
| + # unsatisfiable Range request-header field (that is,
|
| + # all of whose byte-range-spec values have a first-byte-pos
|
| + # value greater than the current length of the selected
|
| + # resource), it SHOULD return a response code of 416
|
| + # (Requested range not satisfiable)."
|
| + continue
|
| + if stop < start:
|
| + # From rfc 2616 sec 14.16:
|
| + # "If the server ignores a byte-range-spec because it
|
| + # is syntactically invalid, the server SHOULD treat
|
| + # the request as if the invalid Range header field
|
| + # did not exist. (Normally, this means return a 200
|
| + # response containing the full entity)."
|
| + return None
|
| + result.append((start, stop + 1))
|
| + else:
|
| + if not stop:
|
| + # See rfc quote above.
|
| + return None
|
| + # Negative subscript (last N bytes)
|
| + result.append((content_length - int(stop), content_length))
|
| +
|
| + return result
|
| +
|
| +
|
| +class HeaderElement(object):
|
| + """An element (with parameters) from an HTTP header's element list."""
|
| +
|
| + def __init__(self, value, params=None):
|
| + self.value = value
|
| + if params is None:
|
| + params = {}
|
| + self.params = params
|
| +
|
| + def __cmp__(self, other):
|
| + return cmp(self.value, other.value)
|
| +
|
| + def __lt__(self, other):
|
| + return self.value < other.value
|
| +
|
| + def __str__(self):
|
| + p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)]
|
| + return "%s%s" % (self.value, "".join(p))
|
| +
|
| + def __bytes__(self):
|
| + return ntob(self.__str__())
|
| +
|
| + def __unicode__(self):
|
| + return ntou(self.__str__())
|
| +
|
| + def parse(elementstr):
|
| + """Transform 'token;key=val' to ('token', {'key': 'val'})."""
|
| + # Split the element into a value and parameters. The 'value' may
|
| + # be of the form, "token=token", but we don't split that here.
|
| + atoms = [x.strip() for x in elementstr.split(";") if x.strip()]
|
| + if not atoms:
|
| + initial_value = ''
|
| + else:
|
| + initial_value = atoms.pop(0).strip()
|
| + params = {}
|
| + for atom in atoms:
|
| + atom = [x.strip() for x in atom.split("=", 1) if x.strip()]
|
| + key = atom.pop(0)
|
| + if atom:
|
| + val = atom[0]
|
| + else:
|
| + val = ""
|
| + params[key] = val
|
| + return initial_value, params
|
| + parse = staticmethod(parse)
|
| +
|
| + def from_str(cls, elementstr):
|
| + """Construct an instance from a string of the form 'token;key=val'."""
|
| + ival, params = cls.parse(elementstr)
|
| + return cls(ival, params)
|
| + from_str = classmethod(from_str)
|
| +
|
| +
|
| +q_separator = re.compile(r'; *q *=')
|
| +
|
| +class AcceptElement(HeaderElement):
|
| + """An element (with parameters) from an Accept* header's element list.
|
| +
|
| + AcceptElement objects are comparable; the more-preferred object will be
|
| + "less than" the less-preferred object. They are also therefore sortable;
|
| + if you sort a list of AcceptElement objects, they will be listed in
|
| + priority order; the most preferred value will be first. Yes, it should
|
| + have been the other way around, but it's too late to fix now.
|
| + """
|
| +
|
| + def from_str(cls, elementstr):
|
| + qvalue = None
|
| + # The first "q" parameter (if any) separates the initial
|
| + # media-range parameter(s) (if any) from the accept-params.
|
| + atoms = q_separator.split(elementstr, 1)
|
| + media_range = atoms.pop(0).strip()
|
| + if atoms:
|
| + # The qvalue for an Accept header can have extensions. The other
|
| + # headers cannot, but it's easier to parse them as if they did.
|
| + qvalue = HeaderElement.from_str(atoms[0].strip())
|
| +
|
| + media_type, params = cls.parse(media_range)
|
| + if qvalue is not None:
|
| + params["q"] = qvalue
|
| + return cls(media_type, params)
|
| + from_str = classmethod(from_str)
|
| +
|
| + def qvalue(self):
|
| + val = self.params.get("q", "1")
|
| + if isinstance(val, HeaderElement):
|
| + val = val.value
|
| + return float(val)
|
| + qvalue = property(qvalue, doc="The qvalue, or priority, of this value.")
|
| +
|
| + def __cmp__(self, other):
|
| + diff = cmp(self.qvalue, other.qvalue)
|
| + if diff == 0:
|
| + diff = cmp(str(self), str(other))
|
| + return diff
|
| +
|
| + def __lt__(self, other):
|
| + if self.qvalue == other.qvalue:
|
| + return str(self) < str(other)
|
| + else:
|
| + return self.qvalue < other.qvalue
|
| +
|
| +
|
| +def header_elements(fieldname, fieldvalue):
|
| + """Return a sorted HeaderElement list from a comma-separated header string."""
|
| + if not fieldvalue:
|
| + return []
|
| +
|
| + result = []
|
| + for element in fieldvalue.split(","):
|
| + if fieldname.startswith("Accept") or fieldname == 'TE':
|
| + hv = AcceptElement.from_str(element)
|
| + else:
|
| + hv = HeaderElement.from_str(element)
|
| + result.append(hv)
|
| +
|
| + return list(reversed(sorted(result)))
|
| +
|
| +def decode_TEXT(value):
|
| + r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr")."""
|
| + try:
|
| + # Python 3
|
| + from email.header import decode_header
|
| + except ImportError:
|
| + from email.Header import decode_header
|
| + atoms = decode_header(value)
|
| + decodedvalue = ""
|
| + for atom, charset in atoms:
|
| + if charset is not None:
|
| + atom = atom.decode(charset)
|
| + decodedvalue += atom
|
| + return decodedvalue
|
| +
|
| +def valid_status(status):
|
| + """Return legal HTTP status Code, Reason-phrase and Message.
|
| +
|
| + The status arg must be an int, or a str that begins with an int.
|
| +
|
| + If status is an int, or a str and no reason-phrase is supplied,
|
| + a default reason-phrase will be provided.
|
| + """
|
| +
|
| + if not status:
|
| + status = 200
|
| +
|
| + status = str(status)
|
| + parts = status.split(" ", 1)
|
| + if len(parts) == 1:
|
| + # No reason supplied.
|
| + code, = parts
|
| + reason = None
|
| + else:
|
| + code, reason = parts
|
| + reason = reason.strip()
|
| +
|
| + try:
|
| + code = int(code)
|
| + except ValueError:
|
| + raise ValueError("Illegal response status from server "
|
| + "(%s is non-numeric)." % repr(code))
|
| +
|
| + if code < 100 or code > 599:
|
| + raise ValueError("Illegal response status from server "
|
| + "(%s is out of range)." % repr(code))
|
| +
|
| + if code not in response_codes:
|
| + # code is unknown but not illegal
|
| + default_reason, message = "", ""
|
| + else:
|
| + default_reason, message = response_codes[code]
|
| +
|
| + if reason is None:
|
| + reason = default_reason
|
| +
|
| + return code, reason, message
|
| +
|
| +
|
| +# NOTE: the parse_qs functions that follow are modified version of those
|
| +# in the python3.0 source - we need to pass through an encoding to the unquote
|
| +# method, but the default parse_qs function doesn't allow us to. These do.
|
| +
|
| +def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
|
| + """Parse a query given as a string argument.
|
| +
|
| + Arguments:
|
| +
|
| + qs: URL-encoded query string to be parsed
|
| +
|
| + keep_blank_values: flag indicating whether blank values in
|
| + URL encoded queries should be treated as blank strings. A
|
| + true value indicates that blanks should be retained as blank
|
| + strings. The default false value indicates that blank values
|
| + are to be ignored and treated as if they were not included.
|
| +
|
| + strict_parsing: flag indicating what to do with parsing errors. If
|
| + false (the default), errors are silently ignored. If true,
|
| + errors raise a ValueError exception.
|
| +
|
| + Returns a dict, as G-d intended.
|
| + """
|
| + pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
|
| + d = {}
|
| + for name_value in pairs:
|
| + if not name_value and not strict_parsing:
|
| + continue
|
| + nv = name_value.split('=', 1)
|
| + if len(nv) != 2:
|
| + if strict_parsing:
|
| + raise ValueError("bad query field: %r" % (name_value,))
|
| + # Handle case of a control-name with no equal sign
|
| + if keep_blank_values:
|
| + nv.append('')
|
| + else:
|
| + continue
|
| + if len(nv[1]) or keep_blank_values:
|
| + name = unquote_qs(nv[0], encoding)
|
| + value = unquote_qs(nv[1], encoding)
|
| + if name in d:
|
| + if not isinstance(d[name], list):
|
| + d[name] = [d[name]]
|
| + d[name].append(value)
|
| + else:
|
| + d[name] = value
|
| + return d
|
| +
|
| +
|
| +image_map_pattern = re.compile(r"[0-9]+,[0-9]+")
|
| +
|
| +def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
|
| + """Build a params dictionary from a query_string.
|
| +
|
| + Duplicate key/value pairs in the provided query_string will be
|
| + returned as {'key': [val1, val2, ...]}. Single key/values will
|
| + be returned as strings: {'key': 'value'}.
|
| + """
|
| + if image_map_pattern.match(query_string):
|
| + # Server-side image map. Map the coords to 'x' and 'y'
|
| + # (like CGI::Request does).
|
| + pm = query_string.split(",")
|
| + pm = {'x': int(pm[0]), 'y': int(pm[1])}
|
| + else:
|
| + pm = _parse_qs(query_string, keep_blank_values, encoding=encoding)
|
| + return pm
|
| +
|
| +
|
| +class CaseInsensitiveDict(dict):
|
| + """A case-insensitive dict subclass.
|
| +
|
| + Each key is changed on entry to str(key).title().
|
| + """
|
| +
|
| + def __getitem__(self, key):
|
| + return dict.__getitem__(self, str(key).title())
|
| +
|
| + def __setitem__(self, key, value):
|
| + dict.__setitem__(self, str(key).title(), value)
|
| +
|
| + def __delitem__(self, key):
|
| + dict.__delitem__(self, str(key).title())
|
| +
|
| + def __contains__(self, key):
|
| + return dict.__contains__(self, str(key).title())
|
| +
|
| + def get(self, key, default=None):
|
| + return dict.get(self, str(key).title(), default)
|
| +
|
| + if hasattr({}, 'has_key'):
|
| + def has_key(self, key):
|
| + return dict.has_key(self, str(key).title())
|
| +
|
| + def update(self, E):
|
| + for k in E.keys():
|
| + self[str(k).title()] = E[k]
|
| +
|
| + def fromkeys(cls, seq, value=None):
|
| + newdict = cls()
|
| + for k in seq:
|
| + newdict[str(k).title()] = value
|
| + return newdict
|
| + fromkeys = classmethod(fromkeys)
|
| +
|
| + def setdefault(self, key, x=None):
|
| + key = str(key).title()
|
| + try:
|
| + return self[key]
|
| + except KeyError:
|
| + self[key] = x
|
| + return x
|
| +
|
| + def pop(self, key, default):
|
| + return dict.pop(self, str(key).title(), default)
|
| +
|
| +
|
| +# TEXT = <any OCTET except CTLs, but including LWS>
|
| +#
|
| +# A CRLF is allowed in the definition of TEXT only as part of a header
|
| +# field continuation. It is expected that the folding LWS will be
|
| +# replaced with a single SP before interpretation of the TEXT value."
|
| +if nativestr == bytestr:
|
| + header_translate_table = ''.join([chr(i) for i in xrange(256)])
|
| + header_translate_deletechars = ''.join([chr(i) for i in xrange(32)]) + chr(127)
|
| +else:
|
| + header_translate_table = None
|
| + header_translate_deletechars = bytes(range(32)) + bytes([127])
|
| +
|
| +
|
| +class HeaderMap(CaseInsensitiveDict):
|
| + """A dict subclass for HTTP request and response headers.
|
| +
|
| + Each key is changed on entry to str(key).title(). This allows headers
|
| + to be case-insensitive and avoid duplicates.
|
| +
|
| + Values are header values (decoded according to :rfc:`2047` if necessary).
|
| + """
|
| +
|
| + protocol=(1, 1)
|
| + encodings = ["ISO-8859-1"]
|
| +
|
| + # Someday, when http-bis is done, this will probably get dropped
|
| + # since few servers, clients, or intermediaries do it. But until then,
|
| + # we're going to obey the spec as is.
|
| + # "Words of *TEXT MAY contain characters from character sets other than
|
| + # ISO-8859-1 only when encoded according to the rules of RFC 2047."
|
| + use_rfc_2047 = True
|
| +
|
| + def elements(self, key):
|
| + """Return a sorted list of HeaderElements for the given header."""
|
| + key = str(key).title()
|
| + value = self.get(key)
|
| + return header_elements(key, value)
|
| +
|
| + def values(self, key):
|
| + """Return a sorted list of HeaderElement.value for the given header."""
|
| + return [e.value for e in self.elements(key)]
|
| +
|
| + def output(self):
|
| + """Transform self into a list of (name, value) tuples."""
|
| + header_list = []
|
| + for k, v in self.items():
|
| + if isinstance(k, unicodestr):
|
| + k = self.encode(k)
|
| +
|
| + if not isinstance(v, basestring):
|
| + v = str(v)
|
| +
|
| + if isinstance(v, unicodestr):
|
| + v = self.encode(v)
|
| +
|
| + # See header_translate_* constants above.
|
| + # Replace only if you really know what you're doing.
|
| + k = k.translate(header_translate_table, header_translate_deletechars)
|
| + v = v.translate(header_translate_table, header_translate_deletechars)
|
| +
|
| + header_list.append((k, v))
|
| + return header_list
|
| +
|
| + def encode(self, v):
|
| + """Return the given header name or value, encoded for HTTP output."""
|
| + for enc in self.encodings:
|
| + try:
|
| + return v.encode(enc)
|
| + except UnicodeEncodeError:
|
| + continue
|
| +
|
| + if self.protocol == (1, 1) and self.use_rfc_2047:
|
| + # Encode RFC-2047 TEXT
|
| + # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
|
| + # We do our own here instead of using the email module
|
| + # because we never want to fold lines--folding has
|
| + # been deprecated by the HTTP working group.
|
| + v = b2a_base64(v.encode('utf-8'))
|
| + return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
|
| +
|
| + raise ValueError("Could not encode header part %r using "
|
| + "any of the encodings %r." %
|
| + (v, self.encodings))
|
| +
|
| +
|
| +class Host(object):
|
| + """An internet address.
|
| +
|
| + name
|
| + Should be the client's host name. If not available (because no DNS
|
| + lookup is performed), the IP address should be used instead.
|
| +
|
| + """
|
| +
|
| + ip = "0.0.0.0"
|
| + port = 80
|
| + name = "unknown.tld"
|
| +
|
| + def __init__(self, ip, port, name=None):
|
| + self.ip = ip
|
| + self.port = port
|
| + if name is None:
|
| + name = ip
|
| + self.name = name
|
| +
|
| + def __repr__(self):
|
| + return "httputil.Host(%r, %r, %r)" % (self.ip, self.port, self.name)
|
|
|
| Property changes on: third_party/cherrypy/lib/httputil.py
|
| ___________________________________________________________________
|
| Added: svn:eol-style
|
| + LF
|
|
|
|
|