2015-09-21 23:48:35 +00:00
|
|
|
|
2015-07-14 21:02:14 +00:00
|
|
|
|
2015-09-15 17:12:15 +00:00
|
|
|
from ..odict import ODict
|
|
|
|
from .. import utils, encoding
|
2015-09-21 23:48:35 +00:00
|
|
|
from ..utils import always_bytes, native
|
2015-09-15 17:12:15 +00:00
|
|
|
from . import cookies
|
2015-09-21 23:48:35 +00:00
|
|
|
from .headers import Headers
|
2015-08-01 08:39:14 +00:00
|
|
|
|
2015-09-15 17:12:15 +00:00
|
|
|
from six.moves import urllib
|
|
|
|
|
2015-09-16 16:43:24 +00:00
|
|
|
# TODO: Move somewhere else?
|
|
|
|
ALPN_PROTO_HTTP1 = b'http/1.1'
|
|
|
|
ALPN_PROTO_H2 = b'h2'
|
2015-09-21 23:48:35 +00:00
|
|
|
HDR_FORM_URLENCODED = "application/x-www-form-urlencoded"
|
|
|
|
HDR_FORM_MULTIPART = "multipart/form-data"
|
2015-07-14 21:02:14 +00:00
|
|
|
|
2015-07-29 09:27:43 +00:00
|
|
|
CONTENT_MISSING = 0
|
|
|
|
|
|
|
|
|
2015-09-17 13:16:12 +00:00
|
|
|
class Message(object):
|
|
|
|
def __init__(self, http_version, headers, body, timestamp_start, timestamp_end):
|
|
|
|
self.http_version = http_version
|
|
|
|
if not headers:
|
|
|
|
headers = Headers()
|
|
|
|
assert isinstance(headers, Headers)
|
|
|
|
self.headers = headers
|
|
|
|
|
|
|
|
self._body = body
|
|
|
|
self.timestamp_start = timestamp_start
|
|
|
|
self.timestamp_end = timestamp_end
|
|
|
|
|
|
|
|
@property
|
|
|
|
def body(self):
|
|
|
|
return self._body
|
|
|
|
|
|
|
|
@body.setter
|
|
|
|
def body(self, body):
|
|
|
|
self._body = body
|
|
|
|
if isinstance(body, bytes):
|
2015-09-21 23:48:35 +00:00
|
|
|
self.headers["content-length"] = str(len(body)).encode()
|
2015-09-17 13:16:12 +00:00
|
|
|
|
|
|
|
content = body
|
|
|
|
|
|
|
|
def __eq__(self, other):
|
|
|
|
if isinstance(other, Message):
|
|
|
|
return self.__dict__ == other.__dict__
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
class Request(Message):
|
2015-07-17 07:37:57 +00:00
|
|
|
def __init__(
|
2015-09-05 16:15:47 +00:00
|
|
|
self,
|
|
|
|
form_in,
|
|
|
|
method,
|
|
|
|
scheme,
|
|
|
|
host,
|
|
|
|
port,
|
|
|
|
path,
|
2015-09-17 13:16:12 +00:00
|
|
|
http_version,
|
2015-09-05 16:15:47 +00:00
|
|
|
headers=None,
|
|
|
|
body=None,
|
|
|
|
timestamp_start=None,
|
|
|
|
timestamp_end=None,
|
|
|
|
form_out=None
|
2015-07-17 07:37:57 +00:00
|
|
|
):
|
2015-09-17 13:16:12 +00:00
|
|
|
super(Request, self).__init__(http_version, headers, body, timestamp_start, timestamp_end)
|
2015-07-27 07:36:50 +00:00
|
|
|
|
2015-07-17 07:37:57 +00:00
|
|
|
self.form_in = form_in
|
|
|
|
self.method = method
|
|
|
|
self.scheme = scheme
|
|
|
|
self.host = host
|
|
|
|
self.port = port
|
|
|
|
self.path = path
|
2015-08-15 18:30:22 +00:00
|
|
|
self.form_out = form_out or form_in
|
2015-07-17 07:37:57 +00:00
|
|
|
|
|
|
|
def __repr__(self):
|
2015-09-15 17:12:15 +00:00
|
|
|
if self.host and self.port:
|
2015-09-20 22:44:17 +00:00
|
|
|
hostport = "{}:{}".format(native(self.host,"idna"), self.port)
|
2015-08-01 08:39:14 +00:00
|
|
|
else:
|
2015-09-15 17:12:15 +00:00
|
|
|
hostport = ""
|
|
|
|
path = self.path or ""
|
|
|
|
return "HTTPRequest({} {}{})".format(
|
|
|
|
self.method, hostport, path
|
|
|
|
)
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def anticache(self):
|
|
|
|
"""
|
|
|
|
Modifies this request to remove headers that might produce a cached
|
|
|
|
response. That is, we remove ETags and If-Modified-Since headers.
|
|
|
|
"""
|
|
|
|
delheaders = [
|
2015-09-21 23:48:35 +00:00
|
|
|
"if-modified-since",
|
|
|
|
"if-none-match",
|
2015-08-01 08:39:14 +00:00
|
|
|
]
|
|
|
|
for i in delheaders:
|
2015-09-05 16:15:47 +00:00
|
|
|
self.headers.pop(i, None)
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def anticomp(self):
|
|
|
|
"""
|
|
|
|
Modifies this request to remove headers that will compress the
|
|
|
|
resource's data.
|
|
|
|
"""
|
2015-09-21 23:48:35 +00:00
|
|
|
self.headers["accept-encoding"] = "identity"
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def constrain_encoding(self):
|
|
|
|
"""
|
|
|
|
Limits the permissible Accept-Encoding values, based on what we can
|
|
|
|
decode appropriately.
|
|
|
|
"""
|
2015-09-21 23:48:35 +00:00
|
|
|
accept_encoding = self.headers.get("accept-encoding")
|
2015-09-05 16:15:47 +00:00
|
|
|
if accept_encoding:
|
2015-09-21 16:38:50 +00:00
|
|
|
self.headers["accept-encoding"] = (
|
2015-08-01 08:39:14 +00:00
|
|
|
', '.join(
|
2015-09-05 16:15:47 +00:00
|
|
|
e
|
|
|
|
for e in encoding.ENCODINGS
|
|
|
|
if e in accept_encoding
|
|
|
|
)
|
|
|
|
)
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def update_host_header(self):
|
|
|
|
"""
|
|
|
|
Update the host header to reflect the current target.
|
|
|
|
"""
|
2015-09-21 16:38:50 +00:00
|
|
|
self.headers["host"] = self.host
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def get_form(self):
|
|
|
|
"""
|
|
|
|
Retrieves the URL-encoded or multipart form data, returning an ODict object.
|
|
|
|
Returns an empty ODict if there is no data or the content-type
|
|
|
|
indicates non-form data.
|
|
|
|
"""
|
|
|
|
if self.body:
|
2015-09-21 23:48:35 +00:00
|
|
|
if HDR_FORM_URLENCODED in self.headers.get("content-type", "").lower():
|
2015-08-01 08:39:14 +00:00
|
|
|
return self.get_form_urlencoded()
|
2015-09-21 23:48:35 +00:00
|
|
|
elif HDR_FORM_MULTIPART in self.headers.get("content-type", "").lower():
|
2015-08-01 08:39:14 +00:00
|
|
|
return self.get_form_multipart()
|
2015-09-15 17:12:15 +00:00
|
|
|
return ODict([])
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def get_form_urlencoded(self):
|
|
|
|
"""
|
|
|
|
Retrieves the URL-encoded form data, returning an ODict object.
|
|
|
|
Returns an empty ODict if there is no data or the content-type
|
|
|
|
indicates non-form data.
|
|
|
|
"""
|
2015-09-21 23:48:35 +00:00
|
|
|
if self.body and HDR_FORM_URLENCODED in self.headers.get("content-type", "").lower():
|
2015-09-15 17:12:15 +00:00
|
|
|
return ODict(utils.urldecode(self.body))
|
|
|
|
return ODict([])
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def get_form_multipart(self):
|
2015-09-21 23:48:35 +00:00
|
|
|
if self.body and HDR_FORM_MULTIPART in self.headers.get("content-type", "").lower():
|
2015-09-15 17:12:15 +00:00
|
|
|
return ODict(
|
2015-08-01 08:39:14 +00:00
|
|
|
utils.multipartdecode(
|
|
|
|
self.headers,
|
|
|
|
self.body))
|
2015-09-15 17:12:15 +00:00
|
|
|
return ODict([])
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def set_form_urlencoded(self, odict):
|
|
|
|
"""
|
|
|
|
Sets the body to the URL-encoded form data, and adds the
|
|
|
|
appropriate content-type header. Note that this will destory the
|
|
|
|
existing body if there is one.
|
|
|
|
"""
|
|
|
|
# FIXME: If there's an existing content-type header indicating a
|
|
|
|
# url-encoded form, leave it alone.
|
2015-09-21 23:48:35 +00:00
|
|
|
self.headers["content-type"] = HDR_FORM_URLENCODED
|
2015-08-01 08:39:14 +00:00
|
|
|
self.body = utils.urlencode(odict.lst)
|
|
|
|
|
|
|
|
def get_path_components(self):
|
|
|
|
"""
|
|
|
|
Returns the path components of the URL as a list of strings.
|
|
|
|
|
|
|
|
Components are unquoted.
|
|
|
|
"""
|
2015-09-15 17:12:15 +00:00
|
|
|
_, _, path, _, _, _ = urllib.parse.urlparse(self.url)
|
2015-09-20 22:44:17 +00:00
|
|
|
return [urllib.parse.unquote(native(i,"ascii")) for i in path.split(b"/") if i]
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def set_path_components(self, lst):
|
|
|
|
"""
|
|
|
|
Takes a list of strings, and sets the path component of the URL.
|
|
|
|
|
|
|
|
Components are quoted.
|
|
|
|
"""
|
2015-09-15 17:12:15 +00:00
|
|
|
lst = [urllib.parse.quote(i, safe="") for i in lst]
|
2015-09-20 22:44:17 +00:00
|
|
|
path = always_bytes("/" + "/".join(lst))
|
2015-09-15 17:12:15 +00:00
|
|
|
scheme, netloc, _, params, query, fragment = urllib.parse.urlparse(self.url)
|
|
|
|
self.url = urllib.parse.urlunparse(
|
2015-08-01 08:39:14 +00:00
|
|
|
[scheme, netloc, path, params, query, fragment]
|
|
|
|
)
|
|
|
|
|
|
|
|
def get_query(self):
|
|
|
|
"""
|
|
|
|
Gets the request query string. Returns an ODict object.
|
|
|
|
"""
|
2015-09-15 17:12:15 +00:00
|
|
|
_, _, _, _, query, _ = urllib.parse.urlparse(self.url)
|
2015-08-01 08:39:14 +00:00
|
|
|
if query:
|
2015-09-15 17:12:15 +00:00
|
|
|
return ODict(utils.urldecode(query))
|
|
|
|
return ODict([])
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def set_query(self, odict):
|
|
|
|
"""
|
|
|
|
Takes an ODict object, and sets the request query string.
|
|
|
|
"""
|
2015-09-15 17:12:15 +00:00
|
|
|
scheme, netloc, path, params, _, fragment = urllib.parse.urlparse(self.url)
|
2015-08-01 08:39:14 +00:00
|
|
|
query = utils.urlencode(odict.lst)
|
2015-09-15 17:12:15 +00:00
|
|
|
self.url = urllib.parse.urlunparse(
|
2015-08-01 08:39:14 +00:00
|
|
|
[scheme, netloc, path, params, query, fragment]
|
|
|
|
)
|
|
|
|
|
|
|
|
def pretty_host(self, hostheader):
|
|
|
|
"""
|
|
|
|
Heuristic to get the host of the request.
|
|
|
|
|
|
|
|
Note that pretty_host() does not always return the TCP destination
|
|
|
|
of the request, e.g. if an upstream proxy is in place
|
|
|
|
|
|
|
|
If hostheader is set to True, the Host: header will be used as
|
|
|
|
additional (and preferred) data source. This is handy in
|
|
|
|
transparent mode, where only the IO of the destination is known,
|
|
|
|
but not the resolved name. This is disabled by default, as an
|
|
|
|
attacker may spoof the host header to confuse an analyst.
|
|
|
|
"""
|
2015-09-21 16:38:50 +00:00
|
|
|
if hostheader and "host" in self.headers:
|
2015-08-01 08:39:14 +00:00
|
|
|
try:
|
2015-09-21 23:48:35 +00:00
|
|
|
return self.headers["host"]
|
2015-08-01 08:39:14 +00:00
|
|
|
except ValueError:
|
2015-09-15 17:12:15 +00:00
|
|
|
pass
|
|
|
|
if self.host:
|
|
|
|
return self.host.decode("idna")
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def pretty_url(self, hostheader):
|
|
|
|
if self.form_out == "authority": # upstream proxy mode
|
2015-09-20 22:44:17 +00:00
|
|
|
return b"%s:%d" % (always_bytes(self.pretty_host(hostheader)), self.port)
|
2015-08-01 08:39:14 +00:00
|
|
|
return utils.unparse_url(self.scheme,
|
|
|
|
self.pretty_host(hostheader),
|
|
|
|
self.port,
|
2015-09-20 22:44:17 +00:00
|
|
|
self.path)
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def get_cookies(self):
|
|
|
|
"""
|
|
|
|
Returns a possibly empty netlib.odict.ODict object.
|
|
|
|
"""
|
2015-09-15 17:12:15 +00:00
|
|
|
ret = ODict()
|
2015-09-17 14:31:50 +00:00
|
|
|
for i in self.headers.get_all("Cookie"):
|
2015-09-21 23:48:35 +00:00
|
|
|
ret.extend(cookies.parse_cookie_header(i))
|
2015-08-01 08:39:14 +00:00
|
|
|
return ret
|
|
|
|
|
|
|
|
def set_cookies(self, odict):
|
|
|
|
"""
|
|
|
|
Takes an netlib.odict.ODict object. Over-writes any existing Cookie
|
|
|
|
headers.
|
|
|
|
"""
|
|
|
|
v = cookies.format_cookie_header(odict)
|
2015-09-21 16:38:50 +00:00
|
|
|
self.headers["cookie"] = v
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def url(self):
|
|
|
|
"""
|
|
|
|
Returns a URL string, constructed from the Request's URL components.
|
|
|
|
"""
|
|
|
|
return utils.unparse_url(
|
|
|
|
self.scheme,
|
|
|
|
self.host,
|
|
|
|
self.port,
|
|
|
|
self.path
|
2015-09-20 22:44:17 +00:00
|
|
|
)
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
@url.setter
|
|
|
|
def url(self, url):
|
|
|
|
"""
|
|
|
|
Parses a URL specification, and updates the Request's information
|
|
|
|
accordingly.
|
|
|
|
|
2015-09-15 17:12:15 +00:00
|
|
|
Raises:
|
|
|
|
ValueError if the URL was invalid
|
2015-08-01 08:39:14 +00:00
|
|
|
"""
|
2015-09-15 17:12:15 +00:00
|
|
|
# TODO: Should handle incoming unicode here.
|
2015-08-01 08:39:14 +00:00
|
|
|
parts = utils.parse_url(url)
|
|
|
|
if not parts:
|
|
|
|
raise ValueError("Invalid URL: %s" % url)
|
|
|
|
self.scheme, self.host, self.port, self.path = parts
|
2015-07-17 07:37:57 +00:00
|
|
|
|
2015-07-27 07:36:50 +00:00
|
|
|
|
2015-09-17 13:16:12 +00:00
|
|
|
class Response(Message):
|
2015-07-14 21:02:14 +00:00
|
|
|
def __init__(
|
2015-09-05 16:15:47 +00:00
|
|
|
self,
|
2015-09-17 13:16:12 +00:00
|
|
|
http_version,
|
2015-09-05 16:15:47 +00:00
|
|
|
status_code,
|
|
|
|
msg=None,
|
|
|
|
headers=None,
|
|
|
|
body=None,
|
|
|
|
timestamp_start=None,
|
|
|
|
timestamp_end=None,
|
2015-07-14 21:02:14 +00:00
|
|
|
):
|
2015-09-17 13:16:12 +00:00
|
|
|
super(Response, self).__init__(http_version, headers, body, timestamp_start, timestamp_end)
|
2015-07-14 21:02:14 +00:00
|
|
|
self.status_code = status_code
|
|
|
|
self.msg = msg
|
|
|
|
|
|
|
|
def __repr__(self):
|
2015-08-01 08:39:14 +00:00
|
|
|
# return "Response(%s - %s)" % (self.status_code, self.msg)
|
|
|
|
|
|
|
|
if self.body:
|
|
|
|
size = utils.pretty_size(len(self.body))
|
|
|
|
else:
|
|
|
|
size = "content missing"
|
2015-08-16 21:33:11 +00:00
|
|
|
# TODO: Remove "(unknown content type, content missing)" edge-case
|
|
|
|
return "<Response: {status_code} {msg} ({contenttype}, {size})>".format(
|
2015-08-01 08:39:14 +00:00
|
|
|
status_code=self.status_code,
|
|
|
|
msg=self.msg,
|
2015-09-21 16:38:50 +00:00
|
|
|
contenttype=self.headers.get("content-type", "unknown content type"),
|
2015-08-10 18:44:36 +00:00
|
|
|
size=size)
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def get_cookies(self):
|
|
|
|
"""
|
|
|
|
Get the contents of all Set-Cookie headers.
|
|
|
|
|
|
|
|
Returns a possibly empty ODict, where keys are cookie name strings,
|
|
|
|
and values are [value, attr] lists. Value is a string, and attr is
|
|
|
|
an ODictCaseless containing cookie attributes. Within attrs, unary
|
|
|
|
attributes (e.g. HTTPOnly) are indicated by a Null value.
|
|
|
|
"""
|
|
|
|
ret = []
|
2015-09-21 16:38:50 +00:00
|
|
|
for header in self.headers.get_all("set-cookie"):
|
2015-09-21 23:48:35 +00:00
|
|
|
v = cookies.parse_set_cookie_header(header)
|
2015-08-01 08:39:14 +00:00
|
|
|
if v:
|
|
|
|
name, value, attrs = v
|
|
|
|
ret.append([name, [value, attrs]])
|
2015-09-15 17:12:15 +00:00
|
|
|
return ODict(ret)
|
2015-08-01 08:39:14 +00:00
|
|
|
|
|
|
|
def set_cookies(self, odict):
|
|
|
|
"""
|
|
|
|
Set the Set-Cookie headers on this response, over-writing existing
|
|
|
|
headers.
|
|
|
|
|
|
|
|
Accepts an ODict of the same format as that returned by get_cookies.
|
|
|
|
"""
|
|
|
|
values = []
|
|
|
|
for i in odict.lst:
|
|
|
|
values.append(
|
|
|
|
cookies.format_set_cookie_header(
|
|
|
|
i[0],
|
|
|
|
i[1][0],
|
|
|
|
i[1][1]
|
|
|
|
)
|
|
|
|
)
|
2015-09-21 16:38:50 +00:00
|
|
|
self.headers.set_all("set-cookie", values)
|