diff --git a/aiohttp/client.py b/aiohttp/client.py index ec5bd63ebbb..f8502242423 100644 --- a/aiohttp/client.py +++ b/aiohttp/client.py @@ -7,7 +7,6 @@ import sys import traceback import warnings -import http.cookies import urllib.parse from multidict import MultiDictProxy, MultiDict, CIMultiDict, upstr @@ -15,6 +14,7 @@ import aiohttp from .client_reqrep import ClientRequest, ClientResponse from .errors import WSServerHandshakeError +from .helpers import CookieJar from .websocket import WS_KEY, WebSocketParser, WebSocketWriter from .websocket_client import ClientWebSocketResponse from . import hdrs @@ -52,13 +52,13 @@ def __init__(self, *, connector=None, loop=None, cookies=None, if loop.get_debug(): self._source_traceback = traceback.extract_stack(sys._getframe(1)) - self._cookies = http.cookies.SimpleCookie() + self._cookie_jar = CookieJar(loop=loop) # For Backward compatability with `share_cookies` connectors if connector._share_cookies: - self._update_cookies(connector.cookies) + self._cookie_jar.update_cookies(connector.cookies) if cookies is not None: - self._update_cookies(cookies) + self._cookie_jar.update_cookies(cookies) self._connector = connector self._default_auth = auth self._version = version @@ -173,10 +173,13 @@ def _request(self, method, url, *, skip_headers.add(upstr(i)) while True: + + cookies = self._cookie_jar.filter_cookies(url) + req = self._request_class( method, url, params=params, headers=headers, skip_auto_headers=skip_headers, data=data, - cookies=self.cookies, encoding=encoding, + cookies=cookies, encoding=encoding, auth=auth, version=version, compress=compress, chunked=chunked, expect100=expect100, loop=self._loop, response_class=self._response_class) @@ -196,7 +199,8 @@ def _request(self, method, url, *, except OSError as exc: raise aiohttp.ClientOSError(*exc.args) from exc - self._update_cookies(resp.cookies) + self._cookie_jar.update_cookies(resp.cookies, resp.url) + # For Backward compatability with `share_cookie` connectors if self._connector._share_cookies: self._connector.update_cookies(resp.cookies) @@ -345,19 +349,6 @@ def _ws_connect(self, url, *, autoping, self._loop) - def _update_cookies(self, cookies): - """Update shared cookies.""" - if isinstance(cookies, dict): - cookies = cookies.items() - - for name, value in cookies: - if isinstance(value, http.cookies.Morsel): - # use dict method because SimpleCookie class modifies value - # before Python 3.4 - dict.__setitem__(self.cookies, name, value) - else: - self.cookies[name] = value - def _prepare_headers(self, headers): """ Add default headers and transform it to CIMultiDict """ @@ -451,7 +442,7 @@ def connector(self): @property def cookies(self): """The session cookies.""" - return self._cookies + return self._cookie_jar.cookies @property def version(self): diff --git a/aiohttp/helpers.py b/aiohttp/helpers.py index 974e88a513e..e44d3b489ee 100644 --- a/aiohttp/helpers.py +++ b/aiohttp/helpers.py @@ -5,9 +5,11 @@ import datetime import functools import io +import ipaddress import os import re -from urllib.parse import quote, urlencode +from urllib.parse import quote, urlencode, urlsplit +from http.cookies import SimpleCookie, Morsel from collections import namedtuple from pathlib import Path @@ -451,6 +453,14 @@ def requote_uri(uri): return quote(uri, safe=safe_without_percent) +def is_ip_address(hostname): + try: + ipaddress.ip_address(hostname) + except ValueError: + return False + return True + + class Timeout: """Timeout context manager. @@ -496,3 +506,257 @@ def __exit__(self, exc_type, exc_val, exc_tb): def _cancel_task(self): self._cancelled = self._task.cancel() + + +class CookieJar: + """Implements cookie storage adhering to RFC 6265.""" + + DATE_TOKENS_RE = re.compile( + "[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]*" + "(?P[\x00-\x08\x0A-\x1F\d:a-zA-Z\x7F-\xFF]+)") + + DATE_HMS_TIME_RE = re.compile("(\d{1,2}):(\d{1,2}):(\d{1,2})") + + DATE_DAY_OF_MONTH_RE = re.compile("(\d{1,2})") + + DATE_MONTH_RE = re.compile( + "(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)", re.I) + + DATE_YEAR_RE = re.compile("(\d{2,4})") + + def __init__(self, cookies=None, loop=None): + self._cookies = SimpleCookie() + self._loop = loop or asyncio.get_event_loop() + self._host_only_cookies = set() + + if cookies is not None: + self.update_cookies(cookies) + + @property + def cookies(self): + """The session cookies.""" + return self._cookies + + def _expire_cookie(self, name): + if name in self._cookies: + del self._cookies[name] + + def update_cookies(self, cookies, response_url=None): + """Update cookies.""" + url_parsed = urlsplit(response_url or "") + hostname = url_parsed.hostname + + if is_ip_address(hostname): + # Don't accept cookies from IPs + return + + if isinstance(cookies, dict): + cookies = cookies.items() + + for name, value in cookies: + if isinstance(value, Morsel): + + if not self._add_morsel(name, value, hostname): + continue + + else: + self._cookies[name] = value + + cookie = self._cookies[name] + + if not cookie["domain"] and hostname is not None: + # Set the cookie's domain to the response hostname + # and set its host-only-flag + self._host_only_cookies.add(name) + cookie["domain"] = hostname + + if not cookie["path"] or not cookie["path"].startswith("/"): + # Set the cookie's path to the response path + path = url_parsed.path + if not path.startswith("/"): + path = "/" + else: + # Cut everything from the last slash to the end + path = "/" + path[1:path.rfind("/")] + cookie["path"] = path + + max_age = cookie["max-age"] + if max_age: + try: + delta_seconds = int(max_age) + self._loop.call_later( + delta_seconds, self._expire_cookie, name) + except ValueError: + cookie["max-age"] = "" + + expires = cookie["expires"] + if not cookie["max-age"] and expires: + expire_time = self._parse_date(expires) + if expire_time: + self._loop.call_at( + expire_time.timestamp(), + self._expire_cookie, name) + else: + cookie["expires"] = "" + + # Remove the host-only flags of nonexistent cookies + self._host_only_cookies -= ( + self._host_only_cookies.difference(self._cookies.keys())) + + def _add_morsel(self, name, value, hostname): + """Add a Morsel to the cookie jar.""" + cookie_domain = value["domain"] + if cookie_domain.startswith("."): + # Remove leading dot + cookie_domain = cookie_domain[1:] + value["domain"] = cookie_domain + + if not cookie_domain or not hostname: + dict.__setitem__(self._cookies, name, value) + return True + + if not self._is_domain_match(cookie_domain, hostname): + # Setting cookies for different domains is not allowed + return False + + # use dict method because SimpleCookie class modifies value + # before Python 3.4 + dict.__setitem__(self._cookies, name, value) + return True + + def filter_cookies(self, request_url): + """Returns this jar's cookies filtered by their attributes.""" + url_parsed = urlsplit(request_url) + filtered = SimpleCookie() + + for name, cookie in self._cookies.items(): + cookie_domain = cookie["domain"] + + # Send shared cookies + if not cookie_domain: + dict.__setitem__(filtered, name, cookie) + continue + + hostname = url_parsed.hostname or "" + + if is_ip_address(hostname): + continue + + if name in self._host_only_cookies: + if cookie_domain != hostname: + continue + elif not self._is_domain_match(cookie_domain, hostname): + continue + + if not self._is_path_match(url_parsed.path, cookie["path"]): + continue + + is_secure = url_parsed.scheme in ("https", "wss") + + if cookie["secure"] and not is_secure: + continue + + dict.__setitem__(filtered, name, cookie) + + return filtered + + @staticmethod + def _is_domain_match(domain, hostname): + """Implements domain matching adhering to RFC 6265.""" + if hostname == domain: + return True + + if not hostname.endswith(domain): + return False + + non_matching = hostname[:-len(domain)] + + if not non_matching.endswith("."): + return False + + return not is_ip_address(hostname) + + @staticmethod + def _is_path_match(req_path, cookie_path): + """Implements path matching adhering to RFC 6265.""" + if req_path == cookie_path: + return True + + if not req_path.startswith(cookie_path): + return False + + if cookie_path.endswith("/"): + return True + + non_matching = req_path[len(cookie_path):] + + return non_matching.startswith("/") + + @classmethod + def _parse_date(cls, date_str): + """Implements date string parsing adhering to RFC 6265.""" + if not date_str: + return + + found_time = False + found_day_of_month = False + found_month = False + found_year = False + + hour = minute = second = 0 + day_of_month = 0 + month = "" + year = 0 + + for token_match in cls.DATE_TOKENS_RE.finditer(date_str): + + token = token_match.group("token") + + if not found_time: + time_match = cls.DATE_HMS_TIME_RE.match(token) + if time_match: + found_time = True + hour, minute, second = [ + int(s) for s in time_match.groups()] + continue + + if not found_day_of_month: + day_of_month_match = cls.DATE_DAY_OF_MONTH_RE.match(token) + if day_of_month_match: + found_day_of_month = True + day_of_month = int(day_of_month_match.group()) + continue + + if not found_month: + month_match = cls.DATE_MONTH_RE.match(token) + if month_match: + found_month = True + month = month_match.group() + continue + + if not found_year: + year_match = cls.DATE_YEAR_RE.match(token) + if year_match: + found_year = True + year = int(year_match.group()) + + if 70 <= year <= 99: + year += 1900 + elif 0 <= year <= 69: + year += 2000 + + if False in (found_day_of_month, found_month, found_year, found_time): + return + + if not 1 <= day_of_month <= 31: + return + + if year < 1601 or hour > 23 or minute > 59 or second > 59: + return + + dt = datetime.datetime.strptime( + "%s %d %d:%d:%d %d" % ( + month, day_of_month, hour, minute, second, year + ), "%b %d %H:%M:%S %Y") + + return dt.replace(tzinfo=datetime.timezone.utc) diff --git a/tests/test_client_functional_oldstyle.py b/tests/test_client_functional_oldstyle.py index f115e328636..01b76cb4350 100644 --- a/tests/test_client_functional_oldstyle.py +++ b/tests/test_client_functional_oldstyle.py @@ -1145,9 +1145,12 @@ def test_session_cookies(self, m_log): session.request('get', httpd.url('cookies'))) self.assertEqual(resp.cookies['c1'].value, 'cookie1') self.assertEqual(resp.cookies['c2'].value, 'cookie2') - self.assertEqual(session.cookies, resp.cookies) resp.close() + # Add the received cookies as shared for sending them to the test + # server, which is only accessible via IP + session.cookies.update(resp.cookies) + # Assert, that we send those cookies in next requests r = self.loop.run_until_complete( session.request('get', httpd.url('method', 'get'))) diff --git a/tests/test_client_session.py b/tests/test_client_session.py index 63333853a38..668ca0a72d1 100644 --- a/tests/test_client_session.py +++ b/tests/test_client_session.py @@ -3,12 +3,14 @@ import gc import re import types +import http.cookies from unittest import mock from multidict import CIMultiDict, MultiDict import aiohttp import pytest +from aiohttp import web from aiohttp.client import ClientSession from aiohttp.connector import BaseConnector, TCPConnector @@ -369,3 +371,56 @@ def test_request_ctx_manager_props(loop): assert isinstance(ctx_mgr.gi_frame, types.FrameType) assert not ctx_mgr.gi_running assert isinstance(ctx_mgr.gi_code, types.CodeType) + + +@pytest.mark.run_loop +def test_cookie_jar_usage(create_app_and_client): + req_url = None + + init_mock = mock.Mock(return_value=None) + update_mock = mock.Mock(return_value=None) + filter_mock = mock.Mock(return_value=None) + + patches = mock.patch.multiple( + "aiohttp.helpers.CookieJar", + __init__=init_mock, + update_cookies=update_mock, + filter_cookies=filter_mock, + ) + + @asyncio.coroutine + def handler(request): + nonlocal req_url + req_url = "http://%s/" % request.host + + resp = web.Response() + resp.set_cookie("response", "resp_value") + return resp + + with patches: + app, client = yield from create_app_and_client( + client_params={"cookies": {"request": "req_value"}} + ) + app.router.add_route('GET', '/', handler) + + # Updating the cookie jar with initial user defined cookies + assert init_mock.called + assert update_mock.called + assert update_mock.call_args[0] == ( + {"request": "req_value"}, + ) + + update_mock.reset_mock() + yield from client.get("/") + + # Filtering the cookie jar before sending the request, + # getting the request URL as only parameter + assert filter_mock.called + assert filter_mock.call_args[0] == (req_url,) + + # Updating the cookie jar with the response cookies + assert update_mock.called + resp_cookies = update_mock.call_args[0][0] + assert isinstance(resp_cookies, http.cookies.SimpleCookie) + assert "response" in resp_cookies + assert resp_cookies["response"].value == "resp_value" diff --git a/tests/test_helpers.py b/tests/test_helpers.py index dbf954bdb43..c7712ec7784 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,7 +1,10 @@ +import asyncio import pytest -from unittest import mock -from aiohttp import helpers +import unittest import datetime +import http.cookies +from unittest import mock +from aiohttp import helpers, test_utils def test_parse_mimetype_1(): @@ -205,3 +208,425 @@ def test_requote_uri_properly_requotes(): # Ensure requoting doesn't break expectations. quoted = 'http://example.com/fiz?buz=%25ppicture' assert quoted == helpers.requote_uri(quoted) + + +def test_is_ip_address(): + assert helpers.is_ip_address("127.0.0.1") + assert helpers.is_ip_address("::1") + assert helpers.is_ip_address("FE80:0000:0000:0000:0202:B3FF:FE1E:8329") + + # Hostnames + assert not helpers.is_ip_address("localhost") + assert not helpers.is_ip_address("www.example.com") + + # Out of range + assert not helpers.is_ip_address("999.999.999.999") + # Contain a port + assert not helpers.is_ip_address("127.0.0.1:80") + assert not helpers.is_ip_address("[2001:db8:0:1]:80") + # Too many "::" + assert not helpers.is_ip_address("1200::AB00:1234::2552:7777:1313") + + +class TestCookieJar(unittest.TestCase): + + def setUp(self): + # Cookies to send from client to server as "Cookie" header + self.cookies_to_send = http.cookies.SimpleCookie( + "shared-cookie=first; " + "domain-cookie=second; Domain=example.com; " + "subdomain1-cookie=third; Domain=test1.example.com; " + "subdomain2-cookie=fourth; Domain=test2.example.com; " + "dotted-domain-cookie=fifth; Domain=.example.com; " + "different-domain-cookie=sixth; Domain=different.org; " + "secure-cookie=seventh; Domain=secure.com; Secure; " + "no-path-cookie=eighth; Domain=pathtest.com; " + "path1-cookie=nineth; Domain=pathtest.com; Path=/; " + "path2-cookie=tenth; Domain=pathtest.com; Path=/one; " + "path3-cookie=eleventh; Domain=pathtest.com; Path=/one/two; " + "path4-cookie=twelfth; Domain=pathtest.com; Path=/one/two/; " + "expires-cookie=thirteenth; Domain=expirestest.com; Path=/;" + " Expires=Tue, 1 Jan 1980 12:00:00 GMT; " + "max-age-cookie=fourteenth; Domain=maxagetest.com; Path=/;" + " Max-Age=60; " + "invalid-max-age-cookie=fifteenth; Domain=invalid-values.com; " + " Max-Age=string; " + "invalid-expires-cookie=sixteenth; Domain=invalid-values.com; " + " Expires=string;" + ) + + # Cookies received from the server as "Set-Cookie" header + self.cookies_to_receive = http.cookies.SimpleCookie( + "unconstrained-cookie=first; Path=/; " + "domain-cookie=second; Domain=example.com; Path=/; " + "subdomain1-cookie=third; Domain=test1.example.com; Path=/; " + "subdomain2-cookie=fourth; Domain=test2.example.com; Path=/; " + "dotted-domain-cookie=fifth; Domain=.example.com; Path=/; " + "different-domain-cookie=sixth; Domain=different.org; Path=/; " + "no-path-cookie=seventh; Domain=pathtest.com; " + "path-cookie=eighth; Domain=pathtest.com; Path=/somepath; " + "wrong-path-cookie=nineth; Domain=pathtest.com; Path=somepath;" + ) + + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + + self.jar = helpers.CookieJar(loop=self.loop) + + def tearDown(self): + self.loop.close() + + def request_reply_with_same_url(self, url): + self.jar.update_cookies(self.cookies_to_send) + cookies_sent = self.jar.filter_cookies(url) + + self.jar.cookies.clear() + + self.jar.update_cookies(self.cookies_to_receive, url) + cookies_received = self.jar.cookies.copy() + + self.jar.cookies.clear() + + return cookies_sent, cookies_received + + def timed_request( + self, url, update_time, send_time): + time_func = "time.monotonic" + + with mock.patch(time_func, return_value=update_time): + self.jar.update_cookies(self.cookies_to_send) + + with mock.patch(time_func, return_value=send_time): + test_utils.run_briefly(self.loop) + + cookies_sent = self.jar.filter_cookies(url) + + self.jar.cookies.clear() + + return cookies_sent + + def test_constructor(self): + jar = helpers.CookieJar(self.cookies_to_send, self.loop) + self.assertEqual(jar.cookies, self.cookies_to_send) + self.assertEqual(jar._loop, self.loop) + + def test_domain_filter_ip(self): + cookies_sent, cookies_received = ( + self.request_reply_with_same_url("http://1.2.3.4/")) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie" + }) + + self.assertEqual(set(cookies_received.keys()), set()) + + def test_domain_filter_same_host(self): + cookies_sent, cookies_received = ( + self.request_reply_with_same_url("http://example.com/")) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "domain-cookie", + "dotted-domain-cookie" + }) + + self.assertEqual(set(cookies_received.keys()), { + "unconstrained-cookie", + "domain-cookie", + "dotted-domain-cookie" + }) + + def test_domain_filter_same_host_and_subdomain(self): + cookies_sent, cookies_received = ( + self.request_reply_with_same_url("http://test1.example.com/")) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "domain-cookie", + "subdomain1-cookie", + "dotted-domain-cookie" + }) + + self.assertEqual(set(cookies_received.keys()), { + "unconstrained-cookie", + "domain-cookie", + "subdomain1-cookie", + "dotted-domain-cookie" + }) + + def test_domain_filter_same_host_diff_subdomain(self): + cookies_sent, cookies_received = ( + self.request_reply_with_same_url("http://different.example.com/")) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "domain-cookie", + "dotted-domain-cookie" + }) + + self.assertEqual(set(cookies_received.keys()), { + "unconstrained-cookie", + "domain-cookie", + "dotted-domain-cookie" + }) + + def test_domain_filter_diff_host(self): + cookies_sent, cookies_received = ( + self.request_reply_with_same_url("http://different.org/")) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "different-domain-cookie" + }) + + self.assertEqual(set(cookies_received.keys()), { + "unconstrained-cookie", + "different-domain-cookie" + }) + + def test_domain_filter_host_only(self): + self.jar.update_cookies(self.cookies_to_receive, "http://example.com/") + + cookies_sent = self.jar.filter_cookies("http://example.com/") + self.assertIn("unconstrained-cookie", set(cookies_sent.keys())) + + cookies_sent = self.jar.filter_cookies("http://different.org/") + self.assertNotIn("unconstrained-cookie", set(cookies_sent.keys())) + + def test_secure_filter(self): + cookies_sent, _ = ( + self.request_reply_with_same_url("http://secure.com/")) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie" + }) + + cookies_sent, _ = ( + self.request_reply_with_same_url("https://secure.com/")) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "secure-cookie" + }) + + def test_path_filter_root(self): + cookies_sent, _ = ( + self.request_reply_with_same_url("http://pathtest.com/")) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "no-path-cookie", + "path1-cookie" + }) + + def test_path_filter_folder(self): + + cookies_sent, _ = ( + self.request_reply_with_same_url("http://pathtest.com/one/")) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "no-path-cookie", + "path1-cookie", + "path2-cookie" + }) + + def test_path_filter_file(self): + + cookies_sent, _ = self.request_reply_with_same_url( + "http://pathtest.com/one/two") + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "no-path-cookie", + "path1-cookie", + "path2-cookie", + "path3-cookie" + }) + + def test_path_filter_subfolder(self): + + cookies_sent, _ = self.request_reply_with_same_url( + "http://pathtest.com/one/two/") + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "no-path-cookie", + "path1-cookie", + "path2-cookie", + "path3-cookie", + "path4-cookie" + }) + + def test_path_filter_subsubfolder(self): + + cookies_sent, _ = self.request_reply_with_same_url( + "http://pathtest.com/one/two/three/") + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "no-path-cookie", + "path1-cookie", + "path2-cookie", + "path3-cookie", + "path4-cookie" + }) + + def test_path_filter_different_folder(self): + + cookies_sent, _ = ( + self.request_reply_with_same_url("http://pathtest.com/hundred/")) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "no-path-cookie", + "path1-cookie" + }) + + def test_path_value(self): + _, cookies_received = ( + self.request_reply_with_same_url("http://pathtest.com/")) + + self.assertEqual(set(cookies_received.keys()), { + "unconstrained-cookie", + "no-path-cookie", + "path-cookie", + "wrong-path-cookie" + }) + + self.assertEqual(cookies_received["no-path-cookie"]["path"], "/") + self.assertEqual(cookies_received["path-cookie"]["path"], "/somepath") + self.assertEqual(cookies_received["wrong-path-cookie"]["path"], "/") + + def test_expires(self): + ts_before = datetime.datetime( + 1975, 1, 1, tzinfo=datetime.timezone.utc).timestamp() + + ts_after = datetime.datetime( + 1985, 1, 1, tzinfo=datetime.timezone.utc).timestamp() + + cookies_sent = self.timed_request( + "http://expirestest.com/", ts_before, ts_before) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "expires-cookie" + }) + + cookies_sent = self.timed_request( + "http://expirestest.com/", ts_before, ts_after) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie" + }) + + def test_max_age(self): + cookies_sent = self.timed_request( + "http://maxagetest.com/", 1000, 1000) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "max-age-cookie" + }) + + cookies_sent = self.timed_request( + "http://maxagetest.com/", 1000, 2000) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie" + }) + + def test_invalid_values(self): + cookies_sent, cookies_received = ( + self.request_reply_with_same_url("http://invalid-values.com/")) + + self.assertEqual(set(cookies_sent.keys()), { + "shared-cookie", + "invalid-max-age-cookie", + "invalid-expires-cookie" + }) + + cookie = cookies_sent["invalid-max-age-cookie"] + self.assertEqual(cookie["max-age"], "") + + cookie = cookies_sent["invalid-expires-cookie"] + self.assertEqual(cookie["expires"], "") + + def test_domain_matching(self): + test_func = helpers.CookieJar._is_domain_match + + self.assertTrue(test_func("test.com", "test.com")) + self.assertTrue(test_func("test.com", "sub.test.com")) + + self.assertFalse(test_func("test.com", "")) + self.assertFalse(test_func("test.com", "test.org")) + self.assertFalse(test_func("diff-test.com", "test.com")) + self.assertFalse(test_func("test.com", "diff-test.com")) + self.assertFalse(test_func("test.com", "127.0.0.1")) + + def test_path_matching(self): + test_func = helpers.CookieJar._is_path_match + + self.assertTrue(test_func("/", "")) + self.assertTrue(test_func("/file", "")) + self.assertTrue(test_func("/folder/file", "")) + self.assertTrue(test_func("/", "/")) + self.assertTrue(test_func("/file", "/")) + self.assertTrue(test_func("/file", "/file")) + self.assertTrue(test_func("/folder/", "/folder/")) + self.assertTrue(test_func("/folder/", "/")) + self.assertTrue(test_func("/folder/file", "/")) + + self.assertFalse(test_func("/", "/file")) + self.assertFalse(test_func("/", "/folder/")) + self.assertFalse(test_func("/file", "/folder/file")) + self.assertFalse(test_func("/folder/", "/folder/file")) + self.assertFalse(test_func("/different-file", "/file")) + self.assertFalse(test_func("/different-folder/", "/folder/")) + + def test_date_parsing(self): + parse_func = helpers.CookieJar._parse_date + utc = datetime.timezone.utc + + self.assertEqual(parse_func(""), None) + + # 70 -> 1970 + self.assertEqual( + parse_func("Tue, 1 Jan 70 00:00:00 GMT"), + datetime.datetime(1970, 1, 1, tzinfo=utc)) + + # 10 -> 2010 + self.assertEqual( + parse_func("Tue, 1 Jan 10 00:00:00 GMT"), + datetime.datetime(2010, 1, 1, tzinfo=utc)) + + # No day of week string + self.assertEqual( + parse_func("1 Jan 1970 00:00:00 GMT"), + datetime.datetime(1970, 1, 1, tzinfo=utc)) + + # No timezone string + self.assertEqual( + parse_func("Tue, 1 Jan 1970 00:00:00"), + datetime.datetime(1970, 1, 1, tzinfo=utc)) + + # No year + self.assertEqual(parse_func("Tue, 1 Jan 00:00:00 GMT"), None) + + # No month + self.assertEqual(parse_func("Tue, 1 1970 00:00:00 GMT"), None) + + # No day of month + self.assertEqual(parse_func("Tue, Jan 1970 00:00:00 GMT"), None) + + # No time + self.assertEqual(parse_func("Tue, 1 Jan 1970 GMT"), None) + + # Invalid day of month + self.assertEqual(parse_func("Tue, 0 Jan 1970 00:00:00 GMT"), None) + + # Invalid year + self.assertEqual(parse_func("Tue, 1 Jan 1500 00:00:00 GMT"), None) + + # Invalid time + self.assertEqual(parse_func("Tue, 1 Jan 1970 77:88:99 GMT"), None)