diff --git a/examples/complex/har_dump.py b/examples/complex/har_dump.py index 0515d0f5e..b6b9ffae6 100644 --- a/examples/complex/har_dump.py +++ b/examples/complex/har_dump.py @@ -206,7 +206,7 @@ def format_request_cookies(fields): def format_response_cookies(fields): - return format_cookies((c[0], c[1].value, c[1].attrs) for c in fields) + return format_cookies((c[0], c[1][0], c[1][1]) for c in fields) def name_value(obj): diff --git a/mitmproxy/net/http/cookies.py b/mitmproxy/net/http/cookies.py index 01d42d465..5b410acc9 100644 --- a/mitmproxy/net/http/cookies.py +++ b/mitmproxy/net/http/cookies.py @@ -1,7 +1,7 @@ -import collections import email.utils import re import time +from typing import Tuple, List, Iterable from mitmproxy.types import multidict @@ -23,10 +23,7 @@ cookies to be set in a single header. Serialization follows RFC6265. http://tools.ietf.org/html/rfc2965 """ -_cookie_params = set(( - 'expires', 'path', 'comment', 'max-age', - 'secure', 'httponly', 'version', -)) +_cookie_params = {'expires', 'path', 'comment', 'max-age', 'secure', 'httponly', 'version'} ESCAPE = re.compile(r"([\"\\])") @@ -43,7 +40,8 @@ class CookieAttrs(multidict.MultiDict): return values[-1] -SetCookie = collections.namedtuple("SetCookie", ["value", "attrs"]) +TSetCookie = Tuple[str, str, CookieAttrs] +TPairs = List[List[str]] # TODO: Should be List[Tuple[str,str]]? def _read_until(s, start, term): @@ -131,15 +129,15 @@ def _read_cookie_pairs(s, off=0): return pairs, off -def _read_set_cookie_pairs(s, off=0): +def _read_set_cookie_pairs(s: str, off=0) -> Tuple[List[TPairs], int]: """ Read pairs of lhs=rhs values from SetCookie headers while handling multiple cookies. off: start offset specials: attributes that are treated specially """ - cookies = [] - pairs = [] + cookies = [] # type: List[TPairs] + pairs = [] # type: TPairs while True: lhs, off = _read_key(s, off, ";=,") @@ -182,7 +180,7 @@ def _read_set_cookie_pairs(s, off=0): return cookies, off -def _has_special(s): +def _has_special(s: str) -> bool: for i in s: if i in '",;\\': return True @@ -238,41 +236,44 @@ def format_cookie_header(lst): return _format_pairs(lst) -def parse_set_cookie_header(line): +def parse_set_cookie_header(line: str) -> List[TSetCookie]: """ - Parse a Set-Cookie header value + Parse a Set-Cookie header value - Returns a list of (name, value, attrs) tuples, where attrs is a + Returns: + A list of (name, value, attrs) tuples, where attrs is a CookieAttrs dict of attributes. No attempt is made to parse attribute values - they are treated purely as strings. """ cookie_pairs, off = _read_set_cookie_pairs(line) - cookies = [ - (pairs[0][0], pairs[0][1], CookieAttrs(tuple(x) for x in pairs[1:])) - for pairs in cookie_pairs if pairs - ] + cookies = [] + for pairs in cookie_pairs: + if pairs: + cookie, *attrs = pairs + cookies.append(( + cookie[0], + cookie[1], + CookieAttrs(attrs) + )) return cookies -def parse_set_cookie_headers(headers): +def parse_set_cookie_headers(headers: Iterable[str]) -> List[TSetCookie]: rv = [] for header in headers: cookies = parse_set_cookie_header(header) - if cookies: - for name, value, attrs in cookies: - rv.append((name, SetCookie(value, attrs))) + rv.extend(cookies) return rv -def format_set_cookie_header(set_cookies): +def format_set_cookie_header(set_cookies: List[TSetCookie]) -> str: """ Formats a Set-Cookie header value. """ rv = [] - for set_cookie in set_cookies: - name, value, attrs = set_cookie + for name, value, attrs in set_cookies: pairs = [(name, value)] pairs.extend( @@ -284,37 +285,36 @@ def format_set_cookie_header(set_cookies): return ", ".join(rv) -def refresh_set_cookie_header(c, delta): +def refresh_set_cookie_header(c: str, delta: int) -> str: """ Args: c: A Set-Cookie string delta: Time delta in seconds Returns: A refreshed Set-Cookie string + Raises: + ValueError, if the cookie is invalid. """ + cookies = parse_set_cookie_header(c) + for cookie in cookies: + name, value, attrs = cookie + if not name or not value: + raise ValueError("Invalid Cookie") - name, value, attrs = parse_set_cookie_header(c)[0] - if not name or not value: - raise ValueError("Invalid Cookie") - - if "expires" in attrs: - e = email.utils.parsedate_tz(attrs["expires"]) - if e: - f = email.utils.mktime_tz(e) + delta - attrs.set_all("expires", [email.utils.formatdate(f)]) - else: - # This can happen when the expires tag is invalid. - # reddit.com sends a an expires tag like this: "Thu, 31 Dec - # 2037 23:59:59 GMT", which is valid RFC 1123, but not - # strictly correct according to the cookie spec. Browsers - # appear to parse this tolerantly - maybe we should too. - # For now, we just ignore this. - del attrs["expires"] - - rv = format_set_cookie_header([(name, value, attrs)]) - if not rv: - raise ValueError("Invalid Cookie") - return rv + if "expires" in attrs: + e = email.utils.parsedate_tz(attrs["expires"]) + if e: + f = email.utils.mktime_tz(e) + delta + attrs.set_all("expires", [email.utils.formatdate(f)]) + else: + # This can happen when the expires tag is invalid. + # reddit.com sends a an expires tag like this: "Thu, 31 Dec + # 2037 23:59:59 GMT", which is valid RFC 1123, but not + # strictly correct according to the cookie spec. Browsers + # appear to parse this tolerantly - maybe we should too. + # For now, we just ignore this. + del attrs["expires"] + return format_set_cookie_header(cookies) def get_expiration_ts(cookie_attrs): diff --git a/mitmproxy/net/http/response.py b/mitmproxy/net/http/response.py index 8edd43b8b..18950fc79 100644 --- a/mitmproxy/net/http/response.py +++ b/mitmproxy/net/http/response.py @@ -131,7 +131,11 @@ class Response(message.Message): def _get_cookies(self): h = self.headers.get_all("set-cookie") - return tuple(cookies.parse_set_cookie_headers(h)) + all_cookies = cookies.parse_set_cookie_headers(h) + return tuple( + (name, (value, attrs)) + for name, value, attrs in all_cookies + ) def _set_cookies(self, value): cookie_headers = [] diff --git a/test/mitmproxy/net/http/test_cookies.py b/test/mitmproxy/net/http/test_cookies.py index 5c30dbdbc..680a50336 100644 --- a/test/mitmproxy/net/http/test_cookies.py +++ b/test/mitmproxy/net/http/test_cookies.py @@ -283,6 +283,10 @@ def test_refresh_cookie(): c = "foo/bar=bla" assert cookies.refresh_set_cookie_header(c, 0) + # https://github.com/mitmproxy/mitmproxy/issues/2250 + c = "" + assert cookies.refresh_set_cookie_header(c, 60) == "" + @mock.patch('time.time') def test_get_expiration_ts(*args):