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