| Index: Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/request.py
|
| diff --git a/Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/request.py b/Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/request.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..1cc2a3d14f9a777d2f4bb7721b5bdd9461299c52
|
| --- /dev/null
|
| +++ b/Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/request.py
|
| @@ -0,0 +1,576 @@
|
| +import base64
|
| +import cgi
|
| +import Cookie
|
| +import StringIO
|
| +import tempfile
|
| +import urlparse
|
| +
|
| +import stash
|
| +from utils import HTTPException
|
| +
|
| +missing = object()
|
| +
|
| +
|
| +class Server(object):
|
| + """Data about the server environment
|
| +
|
| + .. attribute:: config
|
| +
|
| + Environment configuration information with information about the
|
| + various servers running, their hostnames and ports.
|
| +
|
| + .. attribute:: stash
|
| +
|
| + Stash object holding state stored on the server between requests.
|
| +
|
| + """
|
| + config = None
|
| +
|
| + def __init__(self, request):
|
| + self.stash = stash.Stash(request.url_parts.path)
|
| +
|
| +
|
| +class InputFile(object):
|
| + max_buffer_size = 1024*1024
|
| +
|
| + def __init__(self, rfile, length):
|
| + """File-like object used to provide a seekable view of request body data"""
|
| + self._file = rfile
|
| + self.length = length
|
| +
|
| + self._file_position = 0
|
| +
|
| + if length > self.max_buffer_size:
|
| + self._buf = tempfile.TemporaryFile(mode="rw+b")
|
| + else:
|
| + self._buf = StringIO.StringIO()
|
| +
|
| + @property
|
| + def _buf_position(self):
|
| + rv = self._buf.tell()
|
| + assert rv <= self._file_position
|
| + return rv
|
| +
|
| + def read(self, bytes=-1):
|
| + assert self._buf_position <= self._file_position
|
| +
|
| + if bytes < 0:
|
| + bytes = self.length - self._buf_position
|
| + bytes_remaining = min(bytes, self.length - self._buf_position)
|
| +
|
| + if bytes_remaining == 0:
|
| + return ""
|
| +
|
| + if self._buf_position != self._file_position:
|
| + buf_bytes = min(bytes_remaining, self._file_position - self._buf_position)
|
| + old_data = self._buf.read(buf_bytes)
|
| + bytes_remaining -= buf_bytes
|
| + else:
|
| + old_data = ""
|
| +
|
| + assert self._buf_position == self._file_position, (
|
| + "Before reading buffer position (%i) didn't match file position (%i)" %
|
| + (self._buf_position, self._file_position))
|
| + new_data = self._file.read(bytes_remaining)
|
| + self._buf.write(new_data)
|
| + self._file_position += bytes_remaining
|
| + assert self._buf_position == self._file_position, (
|
| + "After reading buffer position (%i) didn't match file position (%i)" %
|
| + (self._buf_position, self._file_position))
|
| +
|
| + return old_data + new_data
|
| +
|
| + def tell(self):
|
| + return self._buf_position
|
| +
|
| + def seek(self, offset):
|
| + if offset > self.length or offset < 0:
|
| + raise ValueError
|
| + if offset <= self._file_position:
|
| + self._buf.seek(offset)
|
| + else:
|
| + self.read(offset - self._file_position)
|
| +
|
| + def readline(self, max_bytes=None):
|
| + if max_bytes is None:
|
| + max_bytes = self.length - self._buf_position
|
| +
|
| + if self._buf_position < self._file_position:
|
| + data = self._buf.readline(max_bytes)
|
| + if data.endswith("\n") or len(data) == max_bytes:
|
| + return data
|
| + else:
|
| + data = ""
|
| +
|
| + assert self._buf_position == self._file_position
|
| +
|
| + initial_position = self._file_position
|
| + found = False
|
| + buf = []
|
| + max_bytes -= len(data)
|
| + while not found:
|
| + readahead = self.read(min(2, max_bytes))
|
| + max_bytes -= len(readahead)
|
| + for i, c in enumerate(readahead):
|
| + if c == "\n":
|
| + buf.append(readahead[:i+1])
|
| + found = True
|
| + break
|
| + if not found:
|
| + buf.append(readahead)
|
| + if not readahead or not max_bytes:
|
| + break
|
| + new_data = "".join(buf)
|
| + data += new_data
|
| + self.seek(initial_position + len(new_data))
|
| + return data
|
| +
|
| + def readlines(self):
|
| + rv = []
|
| + while True:
|
| + data = self.readline()
|
| + if data:
|
| + rv.append(data)
|
| + else:
|
| + break
|
| + return rv
|
| +
|
| + def next(self):
|
| + data = self.readline()
|
| + if data:
|
| + return data
|
| + else:
|
| + raise StopIteration
|
| +
|
| + def __iter__(self):
|
| + return self
|
| +
|
| +
|
| +class Request(object):
|
| + """Object representing a HTTP request.
|
| +
|
| + .. attribute:: doc_root
|
| +
|
| + The local directory to use as a base when resolving paths
|
| +
|
| + .. attribute:: route_match
|
| +
|
| + Regexp match object from matching the request path to the route
|
| + selected for the request.
|
| +
|
| + .. attribute:: protocol_version
|
| +
|
| + HTTP version specified in the request.
|
| +
|
| + .. attribute:: method
|
| +
|
| + HTTP method in the request.
|
| +
|
| + .. attribute:: request_path
|
| +
|
| + Request path as it appears in the HTTP request.
|
| +
|
| + .. attribute:: url
|
| +
|
| + Absolute URL for the request.
|
| +
|
| + .. attribute:: headers
|
| +
|
| + List of request headers.
|
| +
|
| + .. attribute:: raw_input
|
| +
|
| + File-like object representing the body of the request.
|
| +
|
| + .. attribute:: url_parts
|
| +
|
| + Parts of the requested URL as obtained by urlparse.urlsplit(path)
|
| +
|
| + .. attribute:: request_line
|
| +
|
| + Raw request line
|
| +
|
| + .. attribute:: headers
|
| +
|
| + RequestHeaders object providing a dictionary-like representation of
|
| + the request headers.
|
| +
|
| + .. attribute:: body
|
| +
|
| + Request body as a string
|
| +
|
| + .. attribute:: GET
|
| +
|
| + MultiDict representing the parameters supplied with the request.
|
| + Note that these may be present on non-GET requests; the name is
|
| + chosen to be familiar to users of other systems such as PHP.
|
| +
|
| + .. attribute:: POST
|
| +
|
| + MultiDict representing the request body parameters. Most parameters
|
| + are present as string values, but file uploads have file-like
|
| + values.
|
| +
|
| + .. attribute:: cookies
|
| +
|
| + Cookies object representing cookies sent with the request with a
|
| + dictionary-like interface.
|
| +
|
| + .. attribute:: auth
|
| +
|
| + Object with username and password properties representing any
|
| + credentials supplied using HTTP authentication.
|
| +
|
| + .. attribute:: server
|
| +
|
| + Server object containing information about the server environment.
|
| + """
|
| +
|
| + def __init__(self, request_handler):
|
| + self.doc_root = request_handler.server.router.doc_root
|
| + self.route_match = None # Set by the router
|
| +
|
| + self.protocol_version = request_handler.protocol_version
|
| + self.method = request_handler.command
|
| +
|
| + scheme = request_handler.server.scheme
|
| + host = request_handler.headers.get("Host")
|
| + port = request_handler.server.server_address[1]
|
| +
|
| + if host is None:
|
| + host = request_handler.server.server_address[0]
|
| + else:
|
| + if ":" in host:
|
| + host, port = host.split(":", 1)
|
| +
|
| + self.request_path = request_handler.path
|
| +
|
| + if self.request_path.startswith(scheme + "://"):
|
| + self.url = request_handler.path
|
| + else:
|
| + self.url = "%s://%s:%s%s" % (scheme,
|
| + host,
|
| + port,
|
| + self.request_path)
|
| + self.url_parts = urlparse.urlsplit(self.url)
|
| +
|
| + self._raw_headers = request_handler.headers
|
| +
|
| + self.request_line = request_handler.raw_requestline
|
| +
|
| + self._headers = None
|
| +
|
| + self.raw_input = InputFile(request_handler.rfile,
|
| + int(self.headers.get("Content-Length", 0)))
|
| + self._body = None
|
| +
|
| + self._GET = None
|
| + self._POST = None
|
| + self._cookies = None
|
| + self._auth = None
|
| +
|
| + self.server = Server(self)
|
| +
|
| + def __repr__(self):
|
| + return "<Request %s %s>" % (self.method, self.url)
|
| +
|
| + @property
|
| + def GET(self):
|
| + if self._GET is None:
|
| + params = urlparse.parse_qsl(self.url_parts.query, keep_blank_values=True)
|
| + self._GET = MultiDict()
|
| + for key, value in params:
|
| + self._GET.add(key, value)
|
| + return self._GET
|
| +
|
| + @property
|
| + def POST(self):
|
| + if self._POST is None:
|
| + #Work out the post parameters
|
| + pos = self.raw_input.tell()
|
| + self.raw_input.seek(0)
|
| + fs = cgi.FieldStorage(fp=self.raw_input,
|
| + environ={"REQUEST_METHOD": self.method},
|
| + headers=self.headers,
|
| + keep_blank_values=True)
|
| + self._POST = MultiDict.from_field_storage(fs)
|
| + self.raw_input.seek(pos)
|
| + return self._POST
|
| +
|
| + @property
|
| + def cookies(self):
|
| + if self._cookies is None:
|
| + parser = Cookie.BaseCookie()
|
| + cookie_headers = self.headers.get("cookie", "")
|
| + parser.load(cookie_headers)
|
| + cookies = Cookies()
|
| + for key, value in parser.iteritems():
|
| + cookies[key] = CookieValue(value)
|
| + self._cookies = cookies
|
| + return self._cookies
|
| +
|
| + @property
|
| + def headers(self):
|
| + if self._headers is None:
|
| + self._headers = RequestHeaders(self._raw_headers)
|
| + return self._headers
|
| +
|
| + @property
|
| + def body(self):
|
| + if self._body is None:
|
| + pos = self.raw_input.tell()
|
| + self.raw_input.seek(0)
|
| + self._body = self.raw_input.read()
|
| + self.raw_input.seek(pos)
|
| + return self._body
|
| +
|
| + @property
|
| + def auth(self):
|
| + if self._auth is None:
|
| + self._auth = Authentication(self.headers)
|
| + return self._auth
|
| +
|
| +
|
| +class RequestHeaders(dict):
|
| + """Dictionary-like API for accessing request headers."""
|
| + def __init__(self, items):
|
| + for key, value in zip(items.keys(), items.values()):
|
| + key = key.lower()
|
| + if key in self:
|
| + self[key].append(value)
|
| + else:
|
| + dict.__setitem__(self, key, [value])
|
| +
|
| + def __getitem__(self, key):
|
| + """Get all headers of a certain (case-insensitive) name. If there is
|
| + more than one, the values are returned comma separated"""
|
| + values = dict.__getitem__(self, key.lower())
|
| + if len(values) == 1:
|
| + return values[0]
|
| + else:
|
| + return ", ".join(values)
|
| +
|
| + def __setitem__(self, name, value):
|
| + raise Exception
|
| +
|
| + def get(self, key, default=None):
|
| + """Get a string representing all headers with a particular value,
|
| + with multiple headers separated by a comma. If no header is found
|
| + return a default value
|
| +
|
| + :param key: The header name to look up (case-insensitive)
|
| + :param default: The value to return in the case of no match
|
| + """
|
| + try:
|
| + return self[key]
|
| + except KeyError:
|
| + return default
|
| +
|
| + def get_list(self, key, default=missing):
|
| + """Get all the header values for a particular field name as
|
| + a list"""
|
| + try:
|
| + return dict.__getitem__(self, key.lower())
|
| + except KeyError:
|
| + if default is not missing:
|
| + return default
|
| + else:
|
| + raise
|
| +
|
| + def __contains__(self, key):
|
| + return dict.__contains__(self, key.lower())
|
| +
|
| + def iteritems(self):
|
| + for item in self:
|
| + yield item, self[item]
|
| +
|
| + def itervalues(self):
|
| + for item in self:
|
| + yield self[item]
|
| +
|
| +class CookieValue(object):
|
| + """Representation of cookies.
|
| +
|
| + Note that cookies are considered read-only and the string value
|
| + of the cookie will not change if you update the field values.
|
| + However this is not enforced.
|
| +
|
| + .. attribute:: key
|
| +
|
| + The name of the cookie.
|
| +
|
| + .. attribute:: value
|
| +
|
| + The value of the cookie
|
| +
|
| + .. attribute:: expires
|
| +
|
| + The expiry date of the cookie
|
| +
|
| + .. attribute:: path
|
| +
|
| + The path of the cookie
|
| +
|
| + .. attribute:: comment
|
| +
|
| + The comment of the cookie.
|
| +
|
| + .. attribute:: domain
|
| +
|
| + The domain with which the cookie is associated
|
| +
|
| + .. attribute:: max_age
|
| +
|
| + The max-age value of the cookie.
|
| +
|
| + .. attribute:: secure
|
| +
|
| + Whether the cookie is marked as secure
|
| +
|
| + .. attribute:: httponly
|
| +
|
| + Whether the cookie is marked as httponly
|
| +
|
| + """
|
| + def __init__(self, morsel):
|
| + self.key = morsel.key
|
| + self.value = morsel.value
|
| +
|
| + for attr in ["expires", "path",
|
| + "comment", "domain", "max-age",
|
| + "secure", "version", "httponly"]:
|
| + setattr(self, attr.replace("-", "_"), morsel[attr])
|
| +
|
| + self._str = morsel.OutputString()
|
| +
|
| + def __str__(self):
|
| + return self._str
|
| +
|
| + def __repr__(self):
|
| + return self._str
|
| +
|
| + def __eq__(self, other):
|
| + """Equality comparison for cookies. Compares to other cookies
|
| + based on value alone and on non-cookies based on the equality
|
| + of self.value with the other object so that a cookie with value
|
| + "ham" compares equal to the string "ham"
|
| + """
|
| + if hasattr(other, "value"):
|
| + return self.value == other.value
|
| + return self.value == other
|
| +
|
| +
|
| +class MultiDict(dict):
|
| + """Dictionary type that holds multiple values for each
|
| + key"""
|
| + #TODO: this should perhaps also order the keys
|
| + def __init__(self):
|
| + pass
|
| +
|
| + def __setitem__(self, name, value):
|
| + dict.__setitem__(self, name, [value])
|
| +
|
| + def add(self, name, value):
|
| + if name in self:
|
| + dict.__getitem__(self, name).append(value)
|
| + else:
|
| + dict.__setitem__(self, name, [value])
|
| +
|
| + def __getitem__(self, key):
|
| + """Get the first value with a given key"""
|
| + #TODO: should this instead be the last value?
|
| + return self.first(key)
|
| +
|
| + def first(self, key, default=missing):
|
| + """Get the first value with a given key
|
| +
|
| + :param key: The key to lookup
|
| + :param default: The default to return if key is
|
| + not found (throws if nothing is
|
| + specified)
|
| + """
|
| + if key in self and dict.__getitem__(self, key):
|
| + return dict.__getitem__(self, key)[0]
|
| + elif default is not missing:
|
| + return default
|
| + raise KeyError
|
| +
|
| + def last(self, key, default=missing):
|
| + """Get the last value with a given key
|
| +
|
| + :param key: The key to lookup
|
| + :param default: The default to return if key is
|
| + not found (throws if nothing is
|
| + specified)
|
| + """
|
| + if key in self and dict.__getitem__(self, key):
|
| + return dict.__getitem__(self, key)[-1]
|
| + elif default is not missing:
|
| + return default
|
| + raise KeyError
|
| +
|
| + def get_list(self, key):
|
| + """Get all values with a given key as a list
|
| +
|
| + :param key: The key to lookup
|
| + """
|
| + return dict.__getitem__(self, key)
|
| +
|
| + @classmethod
|
| + def from_field_storage(cls, fs):
|
| + self = cls()
|
| + if fs.list is None:
|
| + return self
|
| + for key in fs:
|
| + values = fs[key]
|
| + if not isinstance(values, list):
|
| + values = [values]
|
| +
|
| + for value in values:
|
| + if value.filename:
|
| + value = value
|
| + else:
|
| + value = value.value
|
| + self.add(key, value)
|
| + return self
|
| +
|
| +
|
| +class Cookies(MultiDict):
|
| + """MultiDict specialised for Cookie values"""
|
| + def __init__(self):
|
| + pass
|
| +
|
| + def __getitem__(self, key):
|
| + return self.last(key)
|
| +
|
| +
|
| +class Authentication(object):
|
| + """Object for dealing with HTTP Authentication
|
| +
|
| + .. attribute:: username
|
| +
|
| + The username supplied in the HTTP Authorization
|
| + header, or None
|
| +
|
| + .. attribute:: password
|
| +
|
| + The password supplied in the HTTP Authorization
|
| + header, or None
|
| + """
|
| + def __init__(self, headers):
|
| + self.username = None
|
| + self.password = None
|
| +
|
| + auth_schemes = {"Basic": self.decode_basic}
|
| +
|
| + if "authorization" in headers:
|
| + header = headers.get("authorization")
|
| + auth_type, data = header.split(" ", 1)
|
| + if auth_type in auth_schemes:
|
| + self.username, self.password = auth_schemes[auth_type](data)
|
| + else:
|
| + raise HTTPException(400, "Unsupported authentication scheme %s" % auth_type)
|
| +
|
| + def decode_basic(self, data):
|
| + decoded_data = base64.decodestring(data)
|
| + return decoded_data.split(":", 1)
|
|
|