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 If-None-Match not using weak comparison #9063

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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/9063.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed ``If-None-Match`` not using weak comparison -- by :user:`Dreamsorcerer`.
17 changes: 12 additions & 5 deletions aiohttp/web_fileresponse.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,12 @@ async def _sendfile(
return writer

@staticmethod
def _strong_etag_match(etag_value: str, etags: Tuple[ETag, ...]) -> bool:
def _etag_match(etag_value: str, etags: Tuple[ETag, ...], *, weak: bool) -> bool:
if len(etags) == 1 and etags[0].value == ETAG_ANY:
return True
return any(etag.value == etag_value for etag in etags if not etag.is_weak)
return any(
etag.value == etag_value for etag in etags if weak or not etag.is_weak
)

async def _not_modified(
self, request: "BaseRequest", etag_value: str, last_modified: float
Expand Down Expand Up @@ -201,9 +203,11 @@ async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter
etag_value = f"{st.st_mtime_ns:x}-{st.st_size:x}"
last_modified = st.st_mtime

# https://tools.ietf.org/html/rfc7232#section-6
# https://www.rfc-editor.org/rfc/rfc9110#section-13.1.1-2
ifmatch = request.if_match
if ifmatch is not None and not self._strong_etag_match(etag_value, ifmatch):
if ifmatch is not None and not self._etag_match(
etag_value, ifmatch, weak=False
):
return await self._precondition_failed(request)

unmodsince = request.if_unmodified_since
Expand All @@ -214,8 +218,11 @@ async def prepare(self, request: "BaseRequest") -> Optional[AbstractStreamWriter
):
return await self._precondition_failed(request)

# https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2-2
ifnonematch = request.if_none_match
if ifnonematch is not None and self._strong_etag_match(etag_value, ifnonematch):
if ifnonematch is not None and self._etag_match(
etag_value, ifnonematch, weak=True
):
return await self._not_modified(request, etag_value, last_modified)

modsince = request.if_modified_since
Expand Down
36 changes: 34 additions & 2 deletions tests/test_web_sendfile_functional.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,10 +500,9 @@ async def test_static_file_if_none_match(

resp = await client.get("/")
assert 200 == resp.status
original_etag = resp.headers.get("ETag")
original_etag = resp.headers["ETag"]

assert resp.headers.get("Last-Modified") is not None
assert original_etag is not None
resp.close()
resp.release()

Expand Down Expand Up @@ -542,6 +541,39 @@ async def test_static_file_if_none_match_star(
await client.close()


@pytest.mark.parametrize("if_modified_since", ("", "Fri, 31 Dec 9999 23:59:59 GMT"))
async def test_static_file_if_none_match_weak(
aiohttp_client: Any,
app_with_static_route: web.Application,
if_modified_since: str,
) -> None:
client = await aiohttp_client(app_with_static_route)

resp = await client.get("/")
assert 200 == resp.status
original_etag = resp.headers["ETag"]

assert resp.headers.get("Last-Modified") is not None
resp.close()
resp.release()

weak_etag = f"W/{original_etag}"

resp = await client.get(
"/",
headers={"If-None-Match": weak_etag, "If-Modified-Since": if_modified_since},
)
body = await resp.read()
assert 304 == resp.status
assert resp.headers.get("Content-Length") is None
assert resp.headers.get("ETag") == original_etag
assert b"" == body
resp.close()
resp.release()

await client.close()


@pytest.mark.skipif(not ssl, reason="ssl not supported")
async def test_static_file_ssl(
aiohttp_server: Any,
Expand Down
Loading