Skip to content

Commit 3870d52

Browse files
Merge branch 'master' of https://github.com/aio-libs/aiohttp into py314
2 parents 5bd27fa + 48082a7 commit 3870d52

File tree

7 files changed

+165
-22
lines changed

7 files changed

+165
-22
lines changed

.github/workflows/ci-cd.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ jobs:
190190
python -m pip install -r requirements/test.in -c requirements/test.txt
191191
- name: Restore llhttp generated files
192192
if: ${{ matrix.no-extensions == '' }}
193-
uses: actions/download-artifact@v4
193+
uses: actions/download-artifact@v5
194194
with:
195195
name: llhttp
196196
path: vendor/llhttp/build/
@@ -277,7 +277,7 @@ jobs:
277277
run: |
278278
python -m pip install -r requirements/test.in -c requirements/test.txt
279279
- name: Restore llhttp generated files
280-
uses: actions/download-artifact@v4
280+
uses: actions/download-artifact@v5
281281
with:
282282
name: llhttp
283283
path: vendor/llhttp/build/
@@ -341,7 +341,7 @@ jobs:
341341
python -m
342342
pip install -r requirements/cython.in -c requirements/cython.txt
343343
- name: Restore llhttp generated files
344-
uses: actions/download-artifact@v4
344+
uses: actions/download-artifact@v5
345345
with:
346346
name: llhttp
347347
path: vendor/llhttp/build/
@@ -428,15 +428,15 @@ jobs:
428428
python -m
429429
pip install -r requirements/cython.in -c requirements/cython.txt
430430
- name: Restore llhttp generated files
431-
uses: actions/download-artifact@v4
431+
uses: actions/download-artifact@v5
432432
with:
433433
name: llhttp
434434
path: vendor/llhttp/build/
435435
- name: Cythonize
436436
run: |
437437
make cythonize
438438
- name: Build wheels
439-
uses: pypa/[email protected].1
439+
uses: pypa/[email protected].3
440440
env:
441441
CIBW_SKIP: pp* ${{ matrix.musl == 'musllinux' && '*manylinux*' || '*musllinux*' }}
442442
CIBW_ARCHS_MACOS: x86_64 arm64 universal2
@@ -473,7 +473,7 @@ jobs:
473473
run: |
474474
echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token
475475
- name: Download distributions
476-
uses: actions/download-artifact@v4
476+
uses: actions/download-artifact@v5
477477
with:
478478
path: dist
479479
pattern: dist-*

CHANGES.rst

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,63 @@
1010

1111
.. towncrier release notes start
1212
13+
3.12.15 (2025-07-28)
14+
====================
15+
16+
Bug fixes
17+
---------
18+
19+
- Fixed :class:`~aiohttp.DigestAuthMiddleware` to preserve the algorithm case from the server's challenge in the authorization response. This improves compatibility with servers that perform case-sensitive algorithm matching (e.g., servers expecting ``algorithm=MD5-sess`` instead of ``algorithm=MD5-SESS``)
20+
-- by :user:`bdraco`.
21+
22+
23+
*Related issues and pull requests on GitHub:*
24+
:issue:`11352`.
25+
26+
27+
28+
29+
Improved documentation
30+
----------------------
31+
32+
- Remove outdated contents of ``aiohttp-devtools`` and ``aiohttp-swagger``
33+
from Web_advanced docs.
34+
-- by :user:`Cycloctane`
35+
36+
37+
*Related issues and pull requests on GitHub:*
38+
:issue:`11347`.
39+
40+
41+
42+
43+
Packaging updates and notes for downstreams
44+
-------------------------------------------
45+
46+
- Started including the ``llhttp`` :file:`LICENSE` file in wheels by adding ``vendor/llhttp/LICENSE`` to ``license-files`` in :file:`setup.cfg` -- by :user:`threexc`.
47+
48+
49+
*Related issues and pull requests on GitHub:*
50+
:issue:`11226`.
51+
52+
53+
54+
55+
Contributor-facing changes
56+
--------------------------
57+
58+
- Updated a regex in `test_aiohttp_request_coroutine` for Python 3.14.
59+
60+
61+
*Related issues and pull requests on GitHub:*
62+
:issue:`11271`.
63+
64+
65+
66+
67+
----
68+
69+
1370
3.12.14 (2025-07-10)
1471
====================
1572

CHANGES/11226.packaging.rst

Lines changed: 0 additions & 1 deletion
This file was deleted.

CHANGES/11271.contrib.rst

Lines changed: 0 additions & 1 deletion
This file was deleted.

aiohttp/client_middleware_digest_auth.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,9 @@ async def _encode(
245245
)
246246

247247
qop_raw = challenge.get("qop", "")
248-
algorithm = challenge.get("algorithm", "MD5").upper()
248+
# Preserve original algorithm case for response while using uppercase for processing
249+
algorithm_original = challenge.get("algorithm", "MD5")
250+
algorithm = algorithm_original.upper()
249251
opaque = challenge.get("opaque", "")
250252

251253
# Convert string values to bytes once
@@ -342,7 +344,7 @@ def KD(s: bytes, d: bytes) -> bytes:
342344
"nonce": escape_quotes(nonce),
343345
"uri": path,
344346
"response": response_digest.decode(),
345-
"algorithm": algorithm,
347+
"algorithm": algorithm_original,
346348
}
347349

348350
# Optional fields

docs/web_advanced.rst

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1268,20 +1268,13 @@ the middleware might use :meth:`BaseRequest.clone`.
12681268
for modifying *scheme*, *host* and *remote* attributes according
12691269
to ``Forwarded`` and ``X-Forwarded-*`` HTTP headers.
12701270

1271-
Swagger support
1272-
---------------
1273-
1274-
`aiohttp-swagger <https://github.com/cr0hn/aiohttp-swagger>`_ is a
1275-
library that allow to add Swagger documentation and embed the
1276-
Swagger-UI into your :mod:`aiohttp.web` project.
1277-
12781271
CORS support
12791272
------------
12801273

12811274
:mod:`aiohttp.web` itself does not support `Cross-Origin Resource
12821275
Sharing <https://en.wikipedia.org/wiki/Cross-origin_resource_sharing>`_, but
12831276
there is an aiohttp plugin for it:
1284-
`aiohttp_cors <https://github.com/aio-libs/aiohttp_cors>`_.
1277+
`aiohttp-cors <https://github.com/aio-libs/aiohttp-cors>`_.
12851278

12861279

12871280
Debug Toolbar
@@ -1324,10 +1317,8 @@ Install with ``pip``:
13241317
13251318
$ pip install aiohttp-devtools
13261319
1327-
* ``runserver`` provides a development server with auto-reload,
1328-
live-reload, static file serving.
1329-
* ``start`` is a `cookiecutter command which does the donkey work
1330-
of creating new :mod:`aiohttp.web` Applications.
1320+
``adev runserver`` provides a development server with auto-reload,
1321+
live-reload, static file serving.
13311322

13321323
Documentation and a complete tutorial of creating and running an app
13331324
locally are available at `aiohttp-devtools`_.

tests/test_client_middleware_digest_auth.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Test digest authentication middleware for aiohttp client."""
22

33
import io
4+
import re
45
from hashlib import md5, sha1
56
from typing import Generator, Literal, Union
67
from unittest import mock
@@ -211,6 +212,48 @@ async def test_encode_unsupported_algorithm(
211212
await digest_auth_mw._encode("GET", URL("http://example.com/resource"), b"")
212213

213214

215+
@pytest.mark.parametrize("algorithm", ["MD5", "MD5-SESS", "SHA-256"])
216+
async def test_encode_algorithm_case_preservation_uppercase(
217+
digest_auth_mw: DigestAuthMiddleware,
218+
qop_challenge: DigestAuthChallenge,
219+
algorithm: str,
220+
) -> None:
221+
"""Test that uppercase algorithm case is preserved in the response header."""
222+
# Create a challenge with the specific algorithm case
223+
challenge = qop_challenge.copy()
224+
challenge["algorithm"] = algorithm
225+
digest_auth_mw._challenge = challenge
226+
227+
header = await digest_auth_mw._encode(
228+
"GET", URL("http://example.com/resource"), b""
229+
)
230+
231+
# The algorithm in the response should match the exact case from the challenge
232+
assert f"algorithm={algorithm}" in header
233+
234+
235+
@pytest.mark.parametrize("algorithm", ["md5", "MD5-sess", "sha-256"])
236+
async def test_encode_algorithm_case_preservation_lowercase(
237+
digest_auth_mw: DigestAuthMiddleware,
238+
qop_challenge: DigestAuthChallenge,
239+
algorithm: str,
240+
) -> None:
241+
"""Test that lowercase/mixed-case algorithm is preserved in the response header."""
242+
# Create a challenge with the specific algorithm case
243+
challenge = qop_challenge.copy()
244+
challenge["algorithm"] = algorithm
245+
digest_auth_mw._challenge = challenge
246+
247+
header = await digest_auth_mw._encode(
248+
"GET", URL("http://example.com/resource"), b""
249+
)
250+
251+
# The algorithm in the response should match the exact case from the challenge
252+
assert f"algorithm={algorithm}" in header
253+
# Also verify it's not the uppercase version
254+
assert f"algorithm={algorithm.upper()}" not in header
255+
256+
214257
async def test_invalid_qop_rejected(
215258
digest_auth_mw: DigestAuthMiddleware, basic_challenge: DigestAuthChallenge
216259
) -> None:
@@ -1231,3 +1274,55 @@ def test_in_protection_space_multiple_spaces(
12311274
digest_auth_mw._in_protection_space(URL("http://example.com/secure")) is False
12321275
)
12331276
assert digest_auth_mw._in_protection_space(URL("http://example.com/other")) is False
1277+
1278+
1279+
async def test_case_sensitive_algorithm_server(
1280+
aiohttp_server: AiohttpServer,
1281+
) -> None:
1282+
"""Test authentication with a server that requires exact algorithm case matching.
1283+
1284+
This simulates servers like Prusa printers that expect the algorithm
1285+
to be returned with the exact same case as sent in the challenge.
1286+
"""
1287+
digest_auth_mw = DigestAuthMiddleware("testuser", "testpass")
1288+
request_count = 0
1289+
auth_algorithms: list[str] = []
1290+
1291+
async def handler(request: Request) -> Response:
1292+
nonlocal request_count
1293+
request_count += 1
1294+
1295+
if not (auth_header := request.headers.get(hdrs.AUTHORIZATION)):
1296+
# Send challenge with lowercase-sess algorithm (like Prusa)
1297+
challenge = 'Digest realm="Administrator", nonce="test123", qop="auth", algorithm="MD5-sess", opaque="xyz123"'
1298+
return Response(
1299+
status=401,
1300+
headers={"WWW-Authenticate": challenge},
1301+
text="Unauthorized",
1302+
)
1303+
1304+
# Extract algorithm from auth response
1305+
algo_match = re.search(r"algorithm=([^,\s]+)", auth_header)
1306+
assert algo_match is not None
1307+
auth_algorithms.append(algo_match.group(1))
1308+
1309+
# Case-sensitive server: only accept exact case match
1310+
assert "algorithm=MD5-sess" in auth_header
1311+
return Response(text="Success")
1312+
1313+
app = Application()
1314+
app.router.add_get("/api/test", handler)
1315+
server = await aiohttp_server(app)
1316+
1317+
async with (
1318+
ClientSession(middlewares=(digest_auth_mw,)) as session,
1319+
session.get(server.make_url("/api/test")) as resp,
1320+
):
1321+
assert resp.status == 200
1322+
text = await resp.text()
1323+
assert text == "Success"
1324+
1325+
# Verify the middleware preserved the exact algorithm case
1326+
assert request_count == 2 # Initial 401 + successful retry
1327+
assert len(auth_algorithms) == 1
1328+
assert auth_algorithms[0] == "MD5-sess" # Not "MD5-SESS"

0 commit comments

Comments
 (0)