Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added quote_cookie option to CookieJar #4881

Merged
merged 16 commits into from
Oct 15, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES/2571.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a ``quote_cookie`` option to ``CookieJar``, a way to skip quotation wrapping of cookies containing special characters.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ Jian Zeng
Jinkyu Yi
Joel Watts
Jon Nabozny
Jonas Krüger Svensson
Jonas Obrist
Jonny Tan
Joongi Kim
Expand Down
12 changes: 9 additions & 3 deletions aiohttp/cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ class CookieJar(AbstractCookieJar):
MAX_TIME = datetime.datetime.max.replace(
tzinfo=datetime.timezone.utc)

def __init__(self, *, unsafe: bool=False) -> None:
def __init__(self, *, unsafe: bool=False, quote_cookie: bool=True) -> None:
self._loop = get_running_loop()
self._cookies = defaultdict(SimpleCookie) #type: DefaultDict[str, SimpleCookie[str]] # noqa
self._host_only_cookies = set() # type: Set[Tuple[str, str]]
self._unsafe = unsafe
self._quote_cookie = quote_cookie
self._next_expiration = next_whole_second()
self._expirations = {} # type: Dict[Tuple[str, str], datetime.datetime] # noqa: E501

Expand Down Expand Up @@ -194,15 +195,20 @@ def update_cookies(self,

self._do_expiration()

def filter_cookies(self, request_url: URL=URL()) -> 'BaseCookie[str]':
def filter_cookies(self,
request_url: URL=URL()
) -> Union['BaseCookie[str]', 'SimpleCookie[str]']:
"""Returns this jar's cookies filtered by their attributes."""
self._do_expiration()
if not isinstance(request_url, URL):
warnings.warn("The method accepts yarl.URL instances only, got {}"
.format(type(request_url)),
DeprecationWarning)
request_url = URL(request_url)
filtered = SimpleCookie() # type: SimpleCookie[str]
filtered: Union['SimpleCookie[str]', 'BaseCookie[str]'] = (
SimpleCookie() if self._quote_cookie
else BaseCookie()
)
hostname = request_url.raw_host or ""
is_not_secure = request_url.scheme not in ("https", "wss")

Expand Down
28 changes: 25 additions & 3 deletions docs/client_advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,17 +171,39 @@ Cookie Safety
By default :class:`~aiohttp.ClientSession` uses strict version of
:class:`aiohttp.CookieJar`. :rfc:`2109` explicitly forbids cookie
accepting from URLs with IP address instead of DNS name
(e.g. `http://127.0.0.1:80/cookie`).
(e.g. ``http://127.0.0.1:80/cookie``).

It's good but sometimes for testing we need to enable support for such
cookies. It should be done by passing `unsafe=True` to
cookies. It should be done by passing ``unsafe=True`` to
:class:`aiohttp.CookieJar` constructor::


jar = aiohttp.CookieJar(unsafe=True)
session = aiohttp.ClientSession(cookie_jar=jar)


.. _aiohttp-client-cookie-quoting-routine:

Cookie Quoting Routine
^^^^^^^^^^^^^^^^^^^^^^

The client uses the :class:`~aiohttp.SimpleCookie` quoting routines
conform to the :rfc:`2109`, which in turn references the character definitions
from :rfc:`2068`. They provide a two-way quoting algorithm where any non-text
character is translated into a 4 character sequence: a forward-slash
followed by the three-digit octal equivalent of the character.
Any ``\`` or ``"`` is quoted with a preceding ``\`` slash.
Because of the way browsers really handle cookies (as opposed to what the RFC
says) we also encode ``,`` and ``;``.

Some backend systems does not support quoted cookies. You can skip this
quotation routine by passing ``quote_cookie=False`` to the
:class:`~aiohttp.CookieJar` constructor::

jar = aiohttp.CookieJar(quote_cookie=False)
session = aiohttp.ClientSession(cookie_jar=jar)


.. _aiohttp-client-dummy-cookie-jar:

Dummy Cookie Jar
Expand Down Expand Up @@ -527,7 +549,7 @@ Contrary to the ``requests`` library, it won't read environment
variables by default. But you can do so by passing
``trust_env=True`` into :class:`aiohttp.ClientSession`
constructor for extracting proxy configuration from
*HTTP_PROXY*, *HTTPS_PROXY*, *WS_PROXY* or *WSS_PROXY* *environment
*HTTP_PROXY*, *HTTPS_PROXY*, *WS_PROXY* or *WSS_PROXY* *environment
variables* (all are case insensitive)::

async with aiohttp.ClientSession(trust_env=True) as session:
Expand Down
8 changes: 8 additions & 0 deletions tests/test_client_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,14 @@ def test_cookies(make_request) -> None:
assert 'cookie1=val1' == req.headers['COOKIE']


def test_cookies_is_quoted_with_special_characters(make_request) -> None:
req = make_request('get', 'http://test.com/path',
cookies={'cookie1': 'val/one'})

assert 'COOKIE' in req.headers
assert 'cookie1="val/one"' == req.headers['COOKIE']


def test_cookies_merge_with_headers(make_request) -> None:
req = make_request('get', 'http://test.com/path',
headers={'cookie': 'cookie1=val1'},
Expand Down
51 changes: 35 additions & 16 deletions tests/test_cookiejar.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,26 +272,45 @@ async def test_domain_filter_ip_cookie_receive(cookies_to_receive) -> None:
assert len(jar) == 0


async def test_preserving_ip_domain_cookies(loop) -> None:
jar = CookieJar(unsafe=True)
jar.update_cookies(SimpleCookie(
"shared-cookie=first; "
"ip-cookie=second; Domain=127.0.0.1;"
))
cookies_sent = jar.filter_cookies(URL("http://127.0.0.1/")).output(
header='Cookie:')
assert cookies_sent == ('Cookie: ip-cookie=second\r\n'
'Cookie: shared-cookie=first')


async def test_preserving_quoted_cookies(loop) -> None:
jar = CookieJar(unsafe=True)
@pytest.mark.parametrize(
('cookies', 'expected', 'quote_bool'),
[
("shared-cookie=first; ip-cookie=second; Domain=127.0.0.1;",
'Cookie: ip-cookie=second\r\nCookie: shared-cookie=first',
True),
("ip-cookie=\"second\"; Domain=127.0.0.1;",
'Cookie: ip-cookie=\"second\"',
True),
("custom-cookie=value/one;",
'Cookie: custom-cookie="value/one"',
True),
("custom-cookie=value1;",
'Cookie: custom-cookie=value1',
True),
("custom-cookie=value/one;",
'Cookie: custom-cookie=value/one',
False),
],
ids=(
'IP domain preserved',
'no shared cookie',
'quoted cookie with special char',
'quoted cookie w/o special char',
'unquoted cookie with special char',
),
)
async def test_quotes_correctly_based_on_input(loop,
cookies,
expected,
quote_bool
) -> None:
jar = CookieJar(unsafe=True, quote_cookie=quote_bool)
jar.update_cookies(SimpleCookie(
"ip-cookie=\"second\"; Domain=127.0.0.1;"
cookies
))
cookies_sent = jar.filter_cookies(URL("http://127.0.0.1/")).output(
header='Cookie:')
assert cookies_sent == 'Cookie: ip-cookie=\"second\"'
assert cookies_sent == expected


async def test_ignore_domain_ending_with_dot(loop) -> None:
Expand Down