diff --git a/httpie/output/ui/man_pages.py b/httpie/output/ui/man_pages.py index 0ba4974578..117563ff35 100644 --- a/httpie/output/ui/man_pages.py +++ b/httpie/output/ui/man_pages.py @@ -27,7 +27,7 @@ def is_available(program: str) -> bool: stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) - except Exception: + except OSError: # There might be some errors outside the process, e.g # a permission error to execute something that is not an # executable. diff --git a/httpie/utils.py b/httpie/utils.py index 4735b2be5d..3cb42883e5 100644 --- a/httpie/utils.py +++ b/httpie/utils.py @@ -190,14 +190,17 @@ def _max_age_to_expires(cookies, now): Translate `max-age` into `expires` for Requests to take it into account. HACK/FIXME: - """ for cookie in cookies: if 'expires' in cookie: continue max_age = cookie.get('max-age') - if max_age and max_age.isdigit(): - cookie['expires'] = now + float(max_age) + if max_age: + match = re.match(r'^([-+]?)(\d+)$', max_age) + if match: + sign, digits = match.groups() + sign_value = -1 if sign == '-' else 1 + cookie['expires'] = now + sign_value * int(digits) def parse_content_type_header(header): diff --git a/tests/test_sessions.py b/tests/test_sessions.py index aa5243487d..9fd4eba733 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -442,6 +442,54 @@ def test_get_expired_cookies_using_max_age(self): def test_get_expired_cookies_manages_multiple_cookie_headers(self, cookies, now, expected_expired): assert get_expired_cookies(cookies, now=now) == expected_expired + @pytest.mark.parametrize( + 'cookies, now, expected_expired', + [ + # Negative max-age means the cookie expired N seconds ago + # (RFC 6265 ยง 5.2.2), so it must be treated as expired now. + ( + 'session=abc; Max-Age=-300; path=/; domain=.example.com; HttpOnly', + None, + [{'name': 'session', 'path': '/'}], + ), + # An explicitly positive max-age in the future is NOT expired. + ( + 'session=abc; Max-Age=300; path=/; domain=.example.com; HttpOnly', + None, + [], + ), + # An explicitly positive max-age in the past (more than N seconds + # ago) IS expired. + ( + # Clock is in 1970 (well before the max-age is reached), so a + # 10-second max-age is in the future and the cookie is not yet + # expired. + 'session=abc; Max-Age=10; path=/; domain=.example.com; HttpOnly', + 100.0, + [], + ), + ( + # max-age=0 with a non-zero clock: cookie expired exactly at + # `now`, so the expiry is <= now and it is treated as + # expired. + 'session=abc; Max-Age=0; path=/; domain=.example.com; HttpOnly', + 100.0, + [{'name': 'session', 'path': '/'}], + ), + # A bare '-' with no digits is not a number and must be ignored + # rather than raising. + ( + 'session=abc; Max-Age=-; path=/; domain=.example.com; HttpOnly', + None, + [], + ), + ], + ) + def test_get_expired_cookies_handles_signed_max_age( + self, cookies, now, expected_expired, + ): + assert get_expired_cookies(cookies, now=now) == expected_expired + class TestCookieStorage(CookieTestBase):