@@ -1,5 +1,5 @@
Loading
1 1
import asyncio
2 -
import http.cookies
2 +
from http import cookies as http_cookies
3 3
import json
4 4
import typing
5 5
from collections.abc import Mapping
@@ -23,6 +23,33 @@
Loading
23 23
}
24 24
25 25
26 +
def cookie_parser(cookie_string: str) -> typing.Dict[str, str]:
27 +
    """
28 +
    This function parses a ``Cookie`` HTTP header into a dict of key/value pairs.
29 +
30 +
    It attempts to mimic browser cookie parsing behavior: browsers and web servers
31 +
    frequently disregard the spec (RFC 6265) when setting and reading cookies,
32 +
    so we attempt to suit the common scenarios here.
33 +
34 +
    This function has been adapted from Django 3.1.0.
35 +
    Note: we are explicitly _NOT_ using `SimpleCookie.load` because it is based
36 +
    on an outdated spec and will fail on lots of input we want to support
37 +
    """
38 +
    cookie_dict: typing.Dict[str, str] = {}
39 +
    for chunk in cookie_string.split(";"):
40 +
        if "=" in chunk:
41 +
            key, val = chunk.split("=", 1)
42 +
        else:
43 +
            # Assume an empty name per
44 +
            # https://bugzilla.mozilla.org/show_bug.cgi?id=169091
45 +
            key, val = "", chunk
46 +
        key, val = key.strip(), val.strip()
47 +
        if key or val:
48 +
            # unquote using Python's algorithm.
49 +
            cookie_dict[key] = http_cookies._unquote(val)  # type: ignore
50 +
    return cookie_dict
51 +
52 +
26 53
class ClientDisconnect(Exception):
27 54
    pass
28 55
@@ -87,16 +114,11 @@
Loading
87 114
    @property
88 115
    def cookies(self) -> typing.Dict[str, str]:
89 116
        if not hasattr(self, "_cookies"):
90 -
            cookies = {}
117 +
            cookies: typing.Dict[str, str] = {}
91 118
            cookie_header = self.headers.get("cookie")
119 +
92 120
            if cookie_header:
93 -
                cookie = http.cookies.SimpleCookie()  # type: http.cookies.BaseCookie
94 -
                try:
95 -
                    cookie.load(cookie_header)
96 -
                except http.cookies.CookieError:
97 -
                    pass
98 -
                for key, morsel in cookie.items():
99 -
                    cookies[key] = morsel.value
121 +
                cookies = cookie_parser(cookie_header)
100 122
            self._cookies = cookies
101 123
        return self._cookies
102 124

@@ -285,15 +285,112 @@
Loading
285 285
    assert response.text == "Hello, cookies!"
286 286
287 287
288 -
def test_invalid_cookie():
288 +
def test_cookie_lenient_parsing():
289 +
    """
290 +
    The following test is based on a cookie set by Okta, a well-known authorization service.
291 +
    It turns out that it's common practice to set cookies that would be invalid according to
292 +
    the spec.
293 +
    """
294 +
    tough_cookie = (
295 +
        "provider-oauth-nonce=validAsciiblabla; "
296 +
        'okta-oauth-redirect-params={"responseType":"code","state":"somestate",'
297 +
        '"nonce":"somenonce","scopes":["openid","profile","email","phone"],'
298 +
        '"urls":{"issuer":"https://subdomain.okta.com/oauth2/authServer",'
299 +
        '"authorizeUrl":"https://subdomain.okta.com/oauth2/authServer/v1/authorize",'
300 +
        '"userinfoUrl":"https://subdomain.okta.com/oauth2/authServer/v1/userinfo"}}; '
301 +
        "importantCookie=importantValue; sessionCookie=importantSessionValue"
302 +
    )
303 +
    expected_keys = {
304 +
        "importantCookie",
305 +
        "okta-oauth-redirect-params",
306 +
        "provider-oauth-nonce",
307 +
        "sessionCookie",
308 +
    }
309 +
310 +
    async def app(scope, receive, send):
311 +
        request = Request(scope, receive)
312 +
        response = JSONResponse({"cookies": request.cookies})
313 +
        await response(scope, receive, send)
314 +
315 +
    client = TestClient(app)
316 +
    response = client.get("/", headers={"cookie": tough_cookie})
317 +
    result = response.json()
318 +
    assert len(result["cookies"]) == 4
319 +
    assert set(result["cookies"].keys()) == expected_keys
320 +
321 +
322 +
# These test cases copied from Tornado's implementation
323 +
@pytest.mark.parametrize(
324 +
    "set_cookie,expected",
325 +
    [
326 +
        ("chips=ahoy; vienna=finger", {"chips": "ahoy", "vienna": "finger"}),
327 +
        # all semicolons are delimiters, even within quotes
328 +
        (
329 +
            'keebler="E=mc2; L=\\"Loves\\"; fudge=\\012;"',
330 +
            {"keebler": '"E=mc2', "L": '\\"Loves\\"', "fudge": "\\012", "": '"'},
331 +
        ),
332 +
        # Illegal cookies that have an '=' char in an unquoted value.
333 +
        ("keebler=E=mc2", {"keebler": "E=mc2"}),
334 +
        # Cookies with ':' character in their name.
335 +
        ("key:term=value:term", {"key:term": "value:term"}),
336 +
        # Cookies with '[' and ']'.
337 +
        ("a=b; c=[; d=r; f=h", {"a": "b", "c": "[", "d": "r", "f": "h"}),
338 +
        # Cookies that RFC6265 allows.
339 +
        ("a=b; Domain=example.com", {"a": "b", "Domain": "example.com"}),
340 +
        # parse_cookie() keeps only the last cookie with the same name.
341 +
        ("a=b; h=i; a=c", {"a": "c", "h": "i"}),
342 +
    ],
343 +
)
344 +
def test_cookies_edge_cases(set_cookie, expected):
345 +
    async def app(scope, receive, send):
346 +
        request = Request(scope, receive)
347 +
        response = JSONResponse({"cookies": request.cookies})
348 +
        await response(scope, receive, send)
349 +
350 +
    client = TestClient(app)
351 +
    response = client.get("/", headers={"cookie": set_cookie})
352 +
    result = response.json()
353 +
    assert result["cookies"] == expected
354 +
355 +
356 +
@pytest.mark.parametrize(
357 +
    "set_cookie,expected",
358 +
    [
359 +
        # Chunks without an equals sign appear as unnamed values per
360 +
        # https://bugzilla.mozilla.org/show_bug.cgi?id=169091
361 +
        (
362 +
            "abc=def; unnamed; django_language=en",
363 +
            {"": "unnamed", "abc": "def", "django_language": "en"},
364 +
        ),
365 +
        # Even a double quote may be an unamed value.
366 +
        ('a=b; "; c=d', {"a": "b", "": '"', "c": "d"}),
367 +
        # Spaces in names and values, and an equals sign in values.
368 +
        ("a b c=d e = f; gh=i", {"a b c": "d e = f", "gh": "i"}),
369 +
        # More characters the spec forbids.
370 +
        ('a   b,c<>@:/[]?{}=d  "  =e,f g', {"a   b,c<>@:/[]?{}": 'd  "  =e,f g'}),
371 +
        # Unicode characters. The spec only allows ASCII.
372 +
        # ("saint=André Bessette", {"saint": "André Bessette"}),
373 +
        # Browsers don't send extra whitespace or semicolons in Cookie headers,
374 +
        # but cookie_parser() should parse whitespace the same way
375 +
        # document.cookie parses whitespace.
376 +
        # ("  =  b  ;  ;  =  ;   c  =  ;  ", {"": "b", "c": ""}),
377 +
    ],
378 +
)
379 +
def test_cookies_invalid(set_cookie, expected):
380 +
    """
381 +
    Cookie strings that are against the RFC6265 spec but which browsers will send if set
382 +
    via document.cookie.
383 +
    """
384 +
289 385
    async def app(scope, receive, send):
290 386
        request = Request(scope, receive)
291 387
        response = JSONResponse({"cookies": request.cookies})
292 388
        await response(scope, receive, send)
293 389
294 390
    client = TestClient(app)
295 -
    response = client.get("/", cookies={"invalid/cookie": "test", "valid": "test2"})
296 -
    assert response.json() == {"cookies": {}}
391 +
    response = client.get("/", headers={"cookie": set_cookie})
392 +
    result = response.json()
393 +
    assert result["cookies"] == expected
297 394
298 395
299 396
def test_chunked_encoding():
Files Coverage
starlette 100.00%
tests 99.49%
Project Totals (59 files) 99.75%
Notifications are pending CI completion. Periodically Codecov will check the CI state, when complete notifications will be submitted. Push notifications now.
2176.1
TRAVIS_PYTHON_VERSION=3.6
TRAVIS_OS_NAME=linux
2176.3
TRAVIS_PYTHON_VERSION=3.8
TRAVIS_OS_NAME=linux
2176.2
TRAVIS_PYTHON_VERSION=3.7
TRAVIS_OS_NAME=linux
1
coverage:
2
  precision: 2
3
  round: down
4
  range: "80...100"
5

6
  status:
7
    project: yes
8
    patch: no
9
    changes: no
10

11
comment: off
Sunburst
The inner-most circle is the entire project, moving away from the center are folders then, finally, a single file. The size and color of each slice is representing the number of statements and the coverage, respectively.
Icicle
The top section represents the entire project. Proceeding with folders and finally individual files. The size and color of each slice is representing the number of statements and the coverage, respectively.
Grid
Each block represents a single file in the project. The size and color of each block is represented by the number of statements and the coverage, respectively.
Loading