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

Fix double unquoting in url dispatcher #9267

Merged
merged 52 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
d8af609
Fix double unquoting in url dispatcher
bdraco Sep 23, 2024
f0e471e
Revert "Fix double unquoting in url dispatcher"
bdraco Sep 23, 2024
01587b1
fix
bdraco Sep 23, 2024
1426f56
fixes
bdraco Sep 23, 2024
16174eb
fixes
bdraco Sep 23, 2024
a588cab
fix
bdraco Sep 23, 2024
d66ecdf
no way around a breaking change are we need to tell people %2F is no …
bdraco Sep 23, 2024
843581f
Revert "no way around a breaking change are we need to tell people %2…
bdraco Sep 23, 2024
4e1d41e
adjust
bdraco Sep 23, 2024
d89668f
coverage
bdraco Sep 23, 2024
66d1393
fix
bdraco Sep 23, 2024
d125e31
remove unquoting
bdraco Sep 23, 2024
62fea0d
fix
bdraco Sep 23, 2024
fa0f880
fix
bdraco Sep 23, 2024
08b4da7
fix
bdraco Sep 23, 2024
2149f7b
Merge branch 'master' into fix_double_unquoting
bdraco Sep 23, 2024
48d2f73
adjust for a potential yarl revert
bdraco Sep 23, 2024
722fad2
Merge remote-tracking branch 'upstream/fix_double_unquoting' into fix…
bdraco Sep 23, 2024
e7551ff
fix url_for for plain
bdraco Sep 23, 2024
34cfa5a
downgrade yarl to get a CI run
bdraco Sep 23, 2024
7a4c527
Revert "Switch to using `yarl.URL.absolute` over `yarl.URL.is_absolut…
bdraco Sep 23, 2024
0237dbe
Revert "Remove unused backwards compatibility code for old yarl versi…
bdraco Sep 23, 2024
1b9db46
one more place
bdraco Sep 23, 2024
c842bd4
Revert "one more place"
bdraco Sep 23, 2024
7b41d4e
Revert "Revert "Remove unused backwards compatibility code for old ya…
bdraco Sep 23, 2024
195e3f2
Revert "Revert "Switch to using `yarl.URL.absolute` over `yarl.URL.is…
bdraco Sep 23, 2024
f477850
Revert "downgrade yarl to get a CI run"
bdraco Sep 23, 2024
4cb1e82
unquoted
bdraco Sep 23, 2024
3cc1d29
Update tests/test_urldispatch.py
bdraco Sep 23, 2024
2142d7f
coverage
bdraco Sep 23, 2024
bc974f0
Merge remote-tracking branch 'upstream/fix_double_unquoting' into fix…
bdraco Sep 23, 2024
efd49d1
type
bdraco Sep 23, 2024
60c6e95
having the raw_path will not help since we cannot use it for the regex
bdraco Sep 23, 2024
ee3502f
use path_safe
bdraco Sep 23, 2024
ddc538e
Update aiohttp/web_urldispatcher.py
bdraco Sep 23, 2024
d987829
its always %2F
bdraco Sep 23, 2024
0e5b5a9
changelog
bdraco Sep 23, 2024
d7988fb
Update tests/test_urldispatch.py
bdraco Sep 23, 2024
3fefa9b
Update tests/test_web_urldispatcher.py
bdraco Sep 23, 2024
31c182a
Update tests/test_web_urldispatcher.py
bdraco Sep 23, 2024
28cd7f9
Update tests/test_urldispatch.py
Dreamsorcerer Sep 23, 2024
544a476
Increase minimum yarl version to 1.12.0
bdraco Sep 23, 2024
5b9225f
changelog
bdraco Sep 23, 2024
ff1f2f5
Update CHANGES/9268.breaking.rst
bdraco Sep 23, 2024
170bae0
fix test
bdraco Sep 23, 2024
ac79e5a
test will fail
bdraco Sep 23, 2024
fc78f54
Merge branch 'min_yarl_1120' into fix_double_unquoting
bdraco Sep 23, 2024
74175fc
Merge remote-tracking branch 'upstream/fix_double_unquoting' into fix…
bdraco Sep 23, 2024
ea33da4
changelog
bdraco Sep 23, 2024
0f7d802
changelog
bdraco Sep 23, 2024
7f9eebd
remove xfail
bdraco Sep 23, 2024
2694229
fix issue id
bdraco Sep 23, 2024
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/9267.bugfix.rst
24 changes: 12 additions & 12 deletions aiohttp/web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,7 @@ def register_route(self, route: "ResourceRoute") -> None:
async def resolve(self, request: Request) -> _Resolve:
allowed_methods: Set[str] = set()

match_dict = self._match(request.rel_url.path)
match_dict = self._match(request.rel_url.path_safe)
if match_dict is None:
return None, allowed_methods

Expand Down Expand Up @@ -402,8 +402,7 @@ def _match(self, path: str) -> Optional[Dict[str, str]]:
# string comparison is about 10 times faster than regexp matching
if self._path == path:
return {}
else:
return None
return None

def raw_match(self, path: str) -> bool:
return self._path == path
Expand Down Expand Up @@ -473,10 +472,9 @@ def _match(self, path: str) -> Optional[Dict[str, str]]:
match = self._pattern.fullmatch(path)
if match is None:
return None
else:
return {
key: _unquote_path(value) for key, value in match.groupdict().items()
}
return {
key: _unquote_path_safe(value) for key, value in match.groupdict().items()
}

def raw_match(self, path: str) -> bool:
return self._orig_path == path
Expand Down Expand Up @@ -618,7 +616,7 @@ def set_options_route(self, handler: Handler) -> None:
)

async def resolve(self, request: Request) -> _Resolve:
path = request.rel_url.path
path = request.rel_url.path_safe
method = request.method
allowed_methods = set(self._routes)
if not path.startswith(self._prefix2) and path != self._prefix:
Expand All @@ -627,7 +625,7 @@ async def resolve(self, request: Request) -> _Resolve:
if method not in allowed_methods:
return None, allowed_methods

match_dict = {"filename": _unquote_path(path[len(self._prefix) + 1 :])}
match_dict = {"filename": _unquote_path_safe(path[len(self._prefix) + 1 :])}
return (UrlMappingMatchInfo(match_dict, self._routes[method]), allowed_methods)

def __len__(self) -> int:
Expand Down Expand Up @@ -1007,7 +1005,7 @@ async def resolve(self, request: Request) -> UrlMappingMatchInfo:
# candidates for a given url part because there are multiple resources
# registered for the same canonical path, we resolve them in a linear
# fashion to ensure registration order is respected.
url_part = request.rel_url.path
url_part = request.rel_url.path_safe
while url_part:
for candidate in resource_index.get(url_part, ()):
match_dict, allowed = await candidate.resolve(request)
Expand Down Expand Up @@ -1256,8 +1254,10 @@ def _quote_path(value: str) -> str:
return URL.build(path=value, encoded=False).raw_path


def _unquote_path(value: str) -> str:
return URL.build(path=value, encoded=True).path.replace("%2F", "/")
Dreamsorcerer marked this conversation as resolved.
Show resolved Hide resolved
def _unquote_path_safe(value: str) -> str:
if "%" not in value:
Dreamsorcerer marked this conversation as resolved.
Show resolved Hide resolved
return value
return value.replace("%2F", "/").replace("%25", "%")


def _requote_path(value: str) -> str:
Expand Down
17 changes: 14 additions & 3 deletions tests/test_urldispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from collections.abc import Container, Iterable, Mapping, MutableMapping, Sized
from functools import partial
from typing import Awaitable, Callable, Dict, List, NoReturn, Optional, Type
from urllib.parse import unquote
from urllib.parse import quote
Dreamsorcerer marked this conversation as resolved.
Show resolved Hide resolved

import pytest
from yarl import URL
Expand Down Expand Up @@ -486,7 +486,7 @@ def test_add_static_quoting(router: web.UrlDispatcher) -> None:
)
assert router["static"] is resource
url = resource.url_for(filename="/1 2/файл%2F.txt")
assert url.path == "/пре %2Fфикс/1 2/файл%2F.txt"
assert url.path == "/пре /фикс/1 2/файл%2F.txt"
Dreamsorcerer marked this conversation as resolved.
Show resolved Hide resolved
assert str(url) == (
"/%D0%BF%D1%80%D0%B5%20%2F%D1%84%D0%B8%D0%BA%D1%81"
"/1%202/%D1%84%D0%B0%D0%B9%D0%BB%252F.txt"
Expand Down Expand Up @@ -664,7 +664,7 @@ def test_route_dynamic_quoting(router: web.UrlDispatcher) -> None:
route = router.add_route("GET", r"/пре %2Fфикс/{arg}", handler)

url = route.url_for(arg="1 2/текст%2F")
assert url.path == "/пре %2Fфикс/1 2/текст%2F"
assert url.path == "/пре /фикс/1 2/текст%2F"
assert str(url) == (
"/%D0%BF%D1%80%D0%B5%20%2F%D1%84%D0%B8%D0%BA%D1%81"
"/1%202/%D1%82%D0%B5%D0%BA%D1%81%D1%82%252F"
Expand Down Expand Up @@ -777,6 +777,17 @@ async def test_dynamic_match_unquoted_path(router: web.UrlDispatcher) -> None:
assert match_info == {"path": "path", "subpath": unquote(resource_id)}
bdraco marked this conversation as resolved.
Show resolved Hide resolved


async def test_dynamic_match_double_quoted_path(router: web.UrlDispatcher) -> None:
"""Verify that double-quoted path is unquoted only once."""
handler = make_handler()
router.add_route("GET", "/{path}/{subpath}", handler)
resource_id = quote("my/path|with!some%strange$characters", safe="")
double_quoted_resource_id = quote(resource_id, safe="")
req = make_mocked_request("GET", f"/path/{double_quoted_resource_id}")
match_info = await router.resolve(req)
assert match_info == {"path": "path", "subpath": resource_id}


def test_add_route_not_started_with_slash(router: web.UrlDispatcher) -> None:
with pytest.raises(ValueError):
handler = make_handler()
Expand Down
18 changes: 17 additions & 1 deletion tests/test_web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import socket
import sys
from stat import S_IFIFO, S_IMODE
from typing import Any, Generator, Optional
from typing import Any, Generator, NoReturn, Optional

import pytest
import yarl
Expand Down Expand Up @@ -864,6 +864,22 @@ async def handler(request: web.Request) -> web.Response:
assert resp.status == expected_http_resp_status


async def test_decoded_raw_match_regex(aiohttp_client: AiohttpClient) -> None:
"""Verify that raw_match only matches decoded url."""
app = web.Application()

async def handler(request: web.Request) -> NoReturn:
assert False

app.router.add_get("/467%2C802%2C24834%2C24952%2C25362%2C40574/hello", handler)
client = await aiohttp_client(app)

async with client.get(
yarl.URL("/467%2C802%2C24834%2C24952%2C25362%2C40574/hello", encoded=True)
) as resp:
assert resp.status == 404 # should only match decoded url


async def test_order_is_preserved(aiohttp_client: AiohttpClient) -> None:
"""Test route order is preserved.

Expand Down
Loading