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 |