| Index: Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/response.py
|
| diff --git a/Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/response.py b/Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/response.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..52bc79c56c5391e3a6c37cf1acbe305afa9f52e4
|
| --- /dev/null
|
| +++ b/Tools/Scripts/webkitpy/thirdparty/wpt/wpt/tools/wptserve/wptserve/response.py
|
| @@ -0,0 +1,443 @@
|
| +from collections import OrderedDict
|
| +from datetime import datetime, timedelta
|
| +import Cookie
|
| +import json
|
| +import types
|
| +import uuid
|
| +import socket
|
| +
|
| +from constants import response_codes
|
| +from logger import get_logger
|
| +
|
| +missing = object()
|
| +
|
| +class Response(object):
|
| + """Object representing the response to a HTTP request
|
| +
|
| + :param handler: RequestHandler being used for this response
|
| + :param request: Request that this is the response for
|
| +
|
| + .. attribute:: request
|
| +
|
| + Request associated with this Response.
|
| +
|
| + .. attribute:: encoding
|
| +
|
| + The encoding to use when converting unicode to strings for output.
|
| +
|
| + .. attribute:: add_required_headers
|
| +
|
| + Boolean indicating whether mandatory headers should be added to the
|
| + response.
|
| +
|
| + .. attribute:: send_body_for_head_request
|
| +
|
| + Boolean, default False, indicating whether the body content should be
|
| + sent when the request method is HEAD.
|
| +
|
| + .. attribute:: explicit_flush
|
| +
|
| + Boolean indicating whether output should be flushed automatically or only
|
| + when requested.
|
| +
|
| + .. attribute:: writer
|
| +
|
| + The ResponseWriter for this response
|
| +
|
| + .. attribute:: status
|
| +
|
| + Status tuple (code, message). Can be set to an integer, in which case the
|
| + message part is filled in automatically, or a tuple.
|
| +
|
| + .. attribute:: headers
|
| +
|
| + List of HTTP headers to send with the response. Each item in the list is a
|
| + tuple of (name, value).
|
| +
|
| + .. attribute:: content
|
| +
|
| + The body of the response. This can either be a string or a iterable of response
|
| + parts. If it is an iterable, any item may be a string or a function of zero
|
| + parameters which, when called, returns a string."""
|
| +
|
| + def __init__(self, handler, request):
|
| + self.request = request
|
| + self.encoding = "utf8"
|
| +
|
| + self.add_required_headers = True
|
| + self.send_body_for_head_request = False
|
| + self.explicit_flush = False
|
| + self.close_connection = False
|
| +
|
| + self.writer = ResponseWriter(handler, self)
|
| +
|
| + self._status = (200, None)
|
| + self.headers = ResponseHeaders()
|
| + self.content = []
|
| +
|
| + self.logger = get_logger()
|
| +
|
| + @property
|
| + def status(self):
|
| + return self._status
|
| +
|
| + @status.setter
|
| + def status(self, value):
|
| + if hasattr(value, "__len__"):
|
| + if len(value) != 2:
|
| + raise ValueError
|
| + else:
|
| + self._status = (int(value[0]), str(value[1]))
|
| + else:
|
| + self._status = (int(value), None)
|
| +
|
| + def set_cookie(self, name, value, path="/", domain=None, max_age=None,
|
| + expires=None, secure=False, httponly=False, comment=None):
|
| + """Set a cookie to be sent with a Set-Cookie header in the
|
| + response
|
| +
|
| + :param name: String name of the cookie
|
| + :param value: String value of the cookie
|
| + :param max_age: datetime.timedelta int representing the time (in seconds)
|
| + until the cookie expires
|
| + :param path: String path to which the cookie applies
|
| + :param domain: String domain to which the cookie applies
|
| + :param secure: Boolean indicating whether the cookie is marked as secure
|
| + :param httponly: Boolean indicating whether the cookie is marked as
|
| + HTTP Only
|
| + :param comment: String comment
|
| + :param expires: datetime.datetime or datetime.timedelta indicating a
|
| + time or interval from now when the cookie expires
|
| +
|
| + """
|
| + days = dict((i+1, name) for i, name in enumerate(["jan", "feb", "mar",
|
| + "apr", "may", "jun",
|
| + "jul", "aug", "sep",
|
| + "oct", "nov", "dec"]))
|
| + if value is None:
|
| + value = ''
|
| + max_age = 0
|
| + expires = timedelta(days=-1)
|
| +
|
| + if isinstance(expires, timedelta):
|
| + expires = datetime.utcnow() + expires
|
| +
|
| + if expires is not None:
|
| + expires_str = expires.strftime("%d %%s %Y %H:%M:%S GMT")
|
| + expires_str = expires_str % days[expires.month]
|
| + expires = expires_str
|
| +
|
| + if max_age is not None:
|
| + if hasattr(max_age, "total_seconds"):
|
| + max_age = int(max_age.total_seconds())
|
| + max_age = "%.0d" % max_age
|
| +
|
| + m = Cookie.Morsel()
|
| +
|
| + def maybe_set(key, value):
|
| + if value is not None and value is not False:
|
| + m[key] = value
|
| +
|
| + m.set(name, value, value)
|
| + maybe_set("path", path)
|
| + maybe_set("domain", domain)
|
| + maybe_set("comment", comment)
|
| + maybe_set("expires", expires)
|
| + maybe_set("max-age", max_age)
|
| + maybe_set("secure", secure)
|
| + maybe_set("httponly", httponly)
|
| +
|
| + self.headers.append("Set-Cookie", m.OutputString())
|
| +
|
| + def unset_cookie(self, name):
|
| + """Remove a cookie from those that are being sent with the response"""
|
| + cookies = self.headers.get("Set-Cookie")
|
| + parser = Cookie.BaseCookie()
|
| + for cookie in cookies:
|
| + parser.load(cookie)
|
| +
|
| + if name in parser.keys():
|
| + del self.headers["Set-Cookie"]
|
| + for m in parser.values():
|
| + if m.key != name:
|
| + self.headers.append(("Set-Cookie", m.OutputString()))
|
| +
|
| + def delete_cookie(self, name, path="/", domain=None):
|
| + """Delete a cookie on the client by setting it to the empty string
|
| + and to expire in the past"""
|
| + self.set_cookie(name, None, path=path, domain=domain, max_age=0,
|
| + expires=timedelta(days=-1))
|
| +
|
| + def iter_content(self):
|
| + """Iterator returning chunks of response body content.
|
| +
|
| + If any part of the content is a function, this will be called
|
| + and the resulting value (if any) returned."""
|
| + if type(self.content) in types.StringTypes:
|
| + yield self.content
|
| + else:
|
| + for item in self.content:
|
| + if hasattr(item, "__call__"):
|
| + value = item()
|
| + else:
|
| + value = item
|
| + if value:
|
| + yield value
|
| +
|
| + def write_status_headers(self):
|
| + """Write out the status line and headers for the response"""
|
| + self.writer.write_status(*self.status)
|
| + for item in self.headers:
|
| + self.writer.write_header(*item)
|
| + self.writer.end_headers()
|
| +
|
| + def write_content(self):
|
| + """Write out the response content"""
|
| + if self.request.method != "HEAD" or self.send_body_for_head_request:
|
| + for item in self.iter_content():
|
| + self.writer.write_content(item)
|
| +
|
| + def write(self):
|
| + """Write the whole response"""
|
| + self.write_status_headers()
|
| + self.write_content()
|
| +
|
| + def set_error(self, code, message=""):
|
| + """Set the response status headers and body to indicate an
|
| + error"""
|
| + err = {"code": code,
|
| + "message": message}
|
| + data = json.dumps({"error": err})
|
| + self.status = code
|
| + self.headers = [("Content-Type", "text/json"),
|
| + ("Content-Length", len(data))]
|
| + self.content = data
|
| + if code == 500:
|
| + self.logger.error(message)
|
| +
|
| +
|
| +class MultipartContent(object):
|
| + def __init__(self, boundary=None, default_content_type=None):
|
| + self.items = []
|
| + if boundary is None:
|
| + boundary = str(uuid.uuid4())
|
| + self.boundary = boundary
|
| + self.default_content_type = default_content_type
|
| +
|
| + def __call__(self):
|
| + boundary = "--" + self.boundary
|
| + rv = ["", boundary]
|
| + for item in self.items:
|
| + rv.append(str(item))
|
| + rv.append(boundary)
|
| + rv[-1] += "--"
|
| + return "\r\n".join(rv)
|
| +
|
| + def append_part(self, data, content_type=None, headers=None):
|
| + if content_type is None:
|
| + content_type = self.default_content_type
|
| + self.items.append(MultipartPart(data, content_type, headers))
|
| +
|
| + def __iter__(self):
|
| + #This is hackish; when writing the response we need an iterable
|
| + #or a string. For a multipart/byterange response we want an
|
| + #iterable that contains a single callable; the MultipartContent
|
| + #object itself
|
| + yield self
|
| +
|
| +
|
| +class MultipartPart(object):
|
| + def __init__(self, data, content_type=None, headers=None):
|
| + self.headers = ResponseHeaders()
|
| +
|
| + if content_type is not None:
|
| + self.headers.set("Content-Type", content_type)
|
| +
|
| + if headers is not None:
|
| + for name, value in headers:
|
| + if name.lower() == "content-type":
|
| + func = self.headers.set
|
| + else:
|
| + func = self.headers.append
|
| + func(name, value)
|
| +
|
| + self.data = data
|
| +
|
| + def __str__(self):
|
| + rv = []
|
| + for item in self.headers:
|
| + rv.append("%s: %s" % item)
|
| + rv.append("")
|
| + rv.append(self.data)
|
| + return "\r\n".join(rv)
|
| +
|
| +
|
| +class ResponseHeaders(object):
|
| + """Dictionary-like object holding the headers for the response"""
|
| + def __init__(self):
|
| + self.data = OrderedDict()
|
| +
|
| + def set(self, key, value):
|
| + """Set a header to a specific value, overwriting any previous header
|
| + with the same name
|
| +
|
| + :param key: Name of the header to set
|
| + :param value: Value to set the header to
|
| + """
|
| + self.data[key.lower()] = (key, [value])
|
| +
|
| + def append(self, key, value):
|
| + """Add a new header with a given name, not overwriting any existing
|
| + headers with the same name
|
| +
|
| + :param key: Name of the header to add
|
| + :param value: Value to set for the header
|
| + """
|
| + if key.lower() in self.data:
|
| + self.data[key.lower()][1].append(value)
|
| + else:
|
| + self.set(key, value)
|
| +
|
| + def get(self, key, default=missing):
|
| + """Get the set values for a particular header."""
|
| + try:
|
| + return self[key]
|
| + except KeyError:
|
| + if default is missing:
|
| + return []
|
| + return default
|
| +
|
| + def __getitem__(self, key):
|
| + """Get a list of values for a particular header
|
| +
|
| + """
|
| + return self.data[key.lower()][1]
|
| +
|
| + def __delitem__(self, key):
|
| + del self.data[key.lower()]
|
| +
|
| + def __contains__(self, key):
|
| + return key.lower() in self.data
|
| +
|
| + def __setitem__(self, key, value):
|
| + self.set(key, value)
|
| +
|
| + def __iter__(self):
|
| + for key, values in self.data.itervalues():
|
| + for value in values:
|
| + yield key, value
|
| +
|
| + def items(self):
|
| + return list(self)
|
| +
|
| + def update(self, items_iter):
|
| + for name, value in items_iter:
|
| + self.set(name, value)
|
| +
|
| + def __repr__(self):
|
| + return repr(self.data)
|
| +
|
| +
|
| +class ResponseWriter(object):
|
| + """Object providing an API to write out a HTTP response.
|
| +
|
| + :param handler: The RequestHandler being used.
|
| + :param response: The Response associated with this writer.
|
| +
|
| + After each part of the response is written, the output is
|
| + flushed unless response.explicit_flush is False, in which case
|
| + the user must call .flush() explicitly."""
|
| + def __init__(self, handler, response):
|
| + self._wfile = handler.wfile
|
| + self._response = response
|
| + self._handler = handler
|
| + self._headers_seen = set()
|
| + self._headers_complete = False
|
| + self.content_written = False
|
| + self.request = response.request
|
| +
|
| + def write_status(self, code, message=None):
|
| + """Write out the status line of a response.
|
| +
|
| + :param code: The integer status code of the response.
|
| + :param message: The message of the response. Defaults to the message commonly used
|
| + with the status code."""
|
| + if message is None:
|
| + if code in response_codes:
|
| + message = response_codes[code][0]
|
| + else:
|
| + message = ''
|
| + self.write("%s %d %s\r\n" %
|
| + (self._response.request.protocol_version, code, message))
|
| +
|
| + def write_header(self, name, value):
|
| + """Write out a single header for the response.
|
| +
|
| + :param name: Name of the header field
|
| + :param value: Value of the header field
|
| + """
|
| + self._headers_seen.add(name.lower())
|
| + self.write("%s: %s\r\n" % (name, value))
|
| + if not self._response.explicit_flush:
|
| + self.flush()
|
| +
|
| + def write_default_headers(self):
|
| + for name, f in [("Server", self._handler.version_string),
|
| + ("Date", self._handler.date_time_string)]:
|
| + if name.lower() not in self._headers_seen:
|
| + self.write_header(name, f())
|
| +
|
| + if (type(self._response.content) in (str, unicode) and
|
| + "content-length" not in self._headers_seen):
|
| + #Would be nice to avoid double-encoding here
|
| + self.write_header("Content-Length", len(self.encode(self._response.content)))
|
| +
|
| + def end_headers(self):
|
| + """Finish writing headers and write the separator.
|
| +
|
| + Unless add_required_headers on the response is False,
|
| + this will also add HTTP-mandated headers that have not yet been supplied
|
| + to the response headers"""
|
| +
|
| + if self._response.add_required_headers:
|
| + self.write_default_headers()
|
| +
|
| + self.write("\r\n")
|
| + if "content-length" not in self._headers_seen:
|
| + self._response.close_connection = True
|
| + if not self._response.explicit_flush:
|
| + self.flush()
|
| + self._headers_complete = True
|
| +
|
| + def write_content(self, data):
|
| + """Write the body of the response."""
|
| + self.write(self.encode(data))
|
| + if not self._response.explicit_flush:
|
| + self.flush()
|
| +
|
| + def write(self, data):
|
| + """Write directly to the response, converting unicode to bytes
|
| + according to response.encoding. Does not flush."""
|
| + self.content_written = True
|
| + try:
|
| + self._wfile.write(self.encode(data))
|
| + except socket.error:
|
| + # This can happen if the socket got closed by the remote end
|
| + pass
|
| +
|
| + def encode(self, data):
|
| + """Convert unicode to bytes according to response.encoding."""
|
| + if isinstance(data, str):
|
| + return data
|
| + elif isinstance(data, unicode):
|
| + return data.encode(self._response.encoding)
|
| + else:
|
| + raise ValueError
|
| +
|
| + def flush(self):
|
| + """Flush the output."""
|
| + try:
|
| + self._wfile.flush()
|
| + except socket.error:
|
| + # This can happen if the socket got closed by the remote end
|
| + pass
|
|
|