2015-09-25 22:39:04 +00:00
|
|
|
from __future__ import absolute_import, print_function, division
|
|
|
|
|
2016-07-08 00:29:22 +00:00
|
|
|
import six
|
2016-08-23 07:17:06 +00:00
|
|
|
import time
|
|
|
|
from email.utils import parsedate_tz, formatdate, mktime_tz
|
|
|
|
from netlib import human
|
|
|
|
from netlib import multidict
|
2016-05-31 23:12:10 +00:00
|
|
|
from netlib.http import cookies
|
|
|
|
from netlib.http import headers as nheaders
|
|
|
|
from netlib.http import message
|
2016-08-23 07:17:06 +00:00
|
|
|
from netlib.http import status_codes
|
|
|
|
from typing import AnyStr # noqa
|
|
|
|
from typing import Dict # noqa
|
|
|
|
from typing import Iterable # noqa
|
|
|
|
from typing import Tuple # noqa
|
|
|
|
from typing import Union # noqa
|
2015-09-26 15:39:50 +00:00
|
|
|
|
|
|
|
|
2016-05-31 23:12:10 +00:00
|
|
|
class ResponseData(message.MessageData):
|
2016-08-18 15:24:27 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
http_version,
|
|
|
|
status_code,
|
|
|
|
reason=None,
|
|
|
|
headers=(),
|
|
|
|
content=None,
|
|
|
|
timestamp_start=None,
|
|
|
|
timestamp_end=None
|
|
|
|
):
|
2016-07-08 00:29:22 +00:00
|
|
|
if isinstance(http_version, six.text_type):
|
|
|
|
http_version = http_version.encode("ascii", "strict")
|
|
|
|
if isinstance(reason, six.text_type):
|
|
|
|
reason = reason.encode("ascii", "strict")
|
2016-05-31 23:12:10 +00:00
|
|
|
if not isinstance(headers, nheaders.Headers):
|
|
|
|
headers = nheaders.Headers(headers)
|
2016-07-08 00:29:22 +00:00
|
|
|
if isinstance(content, six.text_type):
|
|
|
|
raise ValueError("Content must be bytes, not {}".format(type(content).__name__))
|
2015-09-26 15:39:50 +00:00
|
|
|
|
|
|
|
self.http_version = http_version
|
|
|
|
self.status_code = status_code
|
|
|
|
self.reason = reason
|
|
|
|
self.headers = headers
|
|
|
|
self.content = content
|
|
|
|
self.timestamp_start = timestamp_start
|
|
|
|
self.timestamp_end = timestamp_end
|
|
|
|
|
|
|
|
|
2016-05-31 23:12:10 +00:00
|
|
|
class Response(message.Message):
|
2015-09-26 15:39:50 +00:00
|
|
|
"""
|
|
|
|
An HTTP response.
|
|
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
2016-07-02 08:51:47 +00:00
|
|
|
super(Response, self).__init__()
|
2016-04-02 11:50:53 +00:00
|
|
|
self.data = ResponseData(*args, **kwargs)
|
2015-09-26 15:39:50 +00:00
|
|
|
|
|
|
|
def __repr__(self):
|
2016-07-02 08:51:47 +00:00
|
|
|
if self.raw_content:
|
2015-09-26 15:39:50 +00:00
|
|
|
details = "{}, {}".format(
|
|
|
|
self.headers.get("content-type", "unknown content type"),
|
2016-07-02 08:51:47 +00:00
|
|
|
human.pretty_size(len(self.raw_content))
|
2015-09-26 15:39:50 +00:00
|
|
|
)
|
|
|
|
else:
|
2015-09-26 22:49:41 +00:00
|
|
|
details = "no content"
|
2015-09-26 15:39:50 +00:00
|
|
|
return "Response({status_code} {reason}, {details})".format(
|
|
|
|
status_code=self.status_code,
|
|
|
|
reason=self.reason,
|
|
|
|
details=details
|
|
|
|
)
|
|
|
|
|
2016-08-23 07:17:06 +00:00
|
|
|
@classmethod
|
|
|
|
def make(
|
|
|
|
cls,
|
|
|
|
status_code=200, # type: int
|
|
|
|
content=b"", # type: AnyStr
|
|
|
|
headers=() # type: Union[Dict[AnyStr, AnyStr], Iterable[Tuple[bytes, bytes]]]
|
|
|
|
):
|
|
|
|
"""
|
|
|
|
Simplified API for creating response objects.
|
|
|
|
"""
|
|
|
|
resp = cls(
|
|
|
|
b"HTTP/1.1",
|
|
|
|
status_code,
|
|
|
|
status_codes.RESPONSES.get(status_code, "").encode(),
|
|
|
|
(),
|
|
|
|
None
|
|
|
|
)
|
|
|
|
|
|
|
|
# Headers can be list or dict, we differentiate here.
|
|
|
|
if isinstance(headers, dict):
|
|
|
|
resp.headers = nheaders.Headers(**headers)
|
|
|
|
elif isinstance(headers, Iterable):
|
|
|
|
resp.headers = nheaders.Headers(headers)
|
|
|
|
else:
|
|
|
|
raise TypeError("Expected headers to be an iterable or dict, but is {}.".format(
|
|
|
|
type(headers).__name__
|
|
|
|
))
|
|
|
|
|
2016-09-22 03:34:01 +00:00
|
|
|
# Assign this manually to update the content-length header.
|
|
|
|
if isinstance(content, bytes):
|
|
|
|
resp.content = content
|
|
|
|
elif isinstance(content, str):
|
|
|
|
resp.text = content
|
|
|
|
else:
|
|
|
|
raise TypeError("Expected content to be str or bytes, but is {}.".format(
|
|
|
|
type(content).__name__
|
|
|
|
))
|
|
|
|
|
2016-08-23 07:17:06 +00:00
|
|
|
return resp
|
|
|
|
|
2015-09-26 15:39:50 +00:00
|
|
|
@property
|
|
|
|
def status_code(self):
|
|
|
|
"""
|
|
|
|
HTTP Status Code, e.g. ``200``.
|
|
|
|
"""
|
|
|
|
return self.data.status_code
|
|
|
|
|
|
|
|
@status_code.setter
|
|
|
|
def status_code(self, status_code):
|
|
|
|
self.data.status_code = status_code
|
|
|
|
|
|
|
|
@property
|
|
|
|
def reason(self):
|
|
|
|
"""
|
|
|
|
HTTP Reason Phrase, e.g. "Not Found".
|
|
|
|
This is always :py:obj:`None` for HTTP2 requests, because HTTP2 responses do not contain a reason phrase.
|
|
|
|
"""
|
2016-05-31 23:12:10 +00:00
|
|
|
return message._native(self.data.reason)
|
2015-09-26 15:39:50 +00:00
|
|
|
|
|
|
|
@reason.setter
|
|
|
|
def reason(self, reason):
|
2016-05-31 23:12:10 +00:00
|
|
|
self.data.reason = message._always_bytes(reason)
|
2015-09-26 15:39:50 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def cookies(self):
|
2016-05-31 23:12:10 +00:00
|
|
|
# type: () -> multidict.MultiDictView
|
2015-09-26 15:39:50 +00:00
|
|
|
"""
|
2016-06-09 01:28:43 +00:00
|
|
|
The response cookies. A possibly empty
|
|
|
|
:py:class:`~netlib.multidict.MultiDictView`, where the keys are cookie
|
|
|
|
name strings, and values are (value, attr) tuples. Value is a string,
|
|
|
|
and attr is an MultiDictView containing cookie attributes. Within
|
|
|
|
attrs, unary attributes (e.g. HTTPOnly) are indicated by a Null value.
|
2015-09-26 15:39:50 +00:00
|
|
|
|
2016-05-19 05:50:19 +00:00
|
|
|
Caveats:
|
|
|
|
Updating the attr
|
2015-09-26 15:39:50 +00:00
|
|
|
"""
|
2016-05-31 23:12:10 +00:00
|
|
|
return multidict.MultiDictView(
|
2016-05-20 23:37:36 +00:00
|
|
|
self._get_cookies,
|
|
|
|
self._set_cookies
|
|
|
|
)
|
2016-05-19 05:50:19 +00:00
|
|
|
|
2016-05-20 23:37:36 +00:00
|
|
|
def _get_cookies(self):
|
2016-05-19 05:50:19 +00:00
|
|
|
h = self.headers.get_all("set-cookie")
|
|
|
|
return tuple(cookies.parse_set_cookie_headers(h))
|
|
|
|
|
2016-05-20 23:37:36 +00:00
|
|
|
def _set_cookies(self, value):
|
2016-05-19 05:50:19 +00:00
|
|
|
cookie_headers = []
|
2016-05-20 23:37:36 +00:00
|
|
|
for k, v in value:
|
2016-09-27 15:34:52 +00:00
|
|
|
header = cookies.format_set_cookie_header([(k, v[0], v[1])])
|
2016-05-19 05:50:19 +00:00
|
|
|
cookie_headers.append(header)
|
|
|
|
self.headers.set_all("set-cookie", cookie_headers)
|
2015-09-26 15:39:50 +00:00
|
|
|
|
2016-05-20 23:37:36 +00:00
|
|
|
@cookies.setter
|
|
|
|
def cookies(self, value):
|
|
|
|
self._set_cookies(value)
|
|
|
|
|
2016-04-02 20:49:05 +00:00
|
|
|
def refresh(self, now=None):
|
|
|
|
"""
|
|
|
|
This fairly complex and heuristic function refreshes a server
|
|
|
|
response for replay.
|
|
|
|
|
|
|
|
- It adjusts date, expires and last-modified headers.
|
|
|
|
- It adjusts cookie expiration.
|
|
|
|
"""
|
|
|
|
if not now:
|
|
|
|
now = time.time()
|
|
|
|
delta = now - self.timestamp_start
|
|
|
|
refresh_headers = [
|
|
|
|
"date",
|
|
|
|
"expires",
|
|
|
|
"last-modified",
|
|
|
|
]
|
|
|
|
for i in refresh_headers:
|
|
|
|
if i in self.headers:
|
|
|
|
d = parsedate_tz(self.headers[i])
|
|
|
|
if d:
|
|
|
|
new = mktime_tz(d) + delta
|
|
|
|
self.headers[i] = formatdate(new)
|
|
|
|
c = []
|
|
|
|
for set_cookie_header in self.headers.get_all("set-cookie"):
|
|
|
|
try:
|
|
|
|
refreshed = cookies.refresh_set_cookie_header(set_cookie_header, delta)
|
|
|
|
except ValueError:
|
|
|
|
refreshed = set_cookie_header
|
|
|
|
c.append(refreshed)
|
|
|
|
if c:
|
|
|
|
self.headers.set_all("set-cookie", c)
|