Skip to content
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
3 changes: 3 additions & 0 deletions CHANGES/13286.contrib.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Made the test suite skip the symlink tests when the host does not permit creating
symlinks, rather than failing with ``OSError: [WinError 1314]`` on Windows without
``SeCreateSymbolicLinkPrivilege`` -- by :user:`Zuhef`.
3 changes: 3 additions & 0 deletions CHANGES/13294.doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Corrected the documented default for :class:`~aiohttp.web.WebSocketResponse`'s
``writer_limit`` parameter, which is ``262144`` (256 KiB) rather than the
``65536`` (64 KiB) that was previously documented -- by :user:`LALITH0110`.
2 changes: 2 additions & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ Konstantin Valetov
Krzysztof Blazewicz
Kyrylo Perevozchikov
Kyungmin Lee
Lalith Kothuru
Lars P. S酶ndergaard
Lee LieWhite
Liu Hua
Expand Down Expand Up @@ -446,6 +447,7 @@ Yuvi Panda
Zainab Lawal
Zeal Wierslee
Zlatan Si膷anica
Zuhef Ahmed
艁ukasz Setla
袦邪褉泻 袣芯褉械薪斜械褉谐
小械屑褢薪 袦邪褉褜褟褋懈薪
7 changes: 4 additions & 3 deletions aiohttp/client_reqrep.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
_CONNECTION_CLOSED_EXCEPTION = ClientConnectionError("Connection closed")
_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]")
_DIGITS_RE = re.compile(r"\d+", re.ASCII)
_LINK_PARAM_RE = re.compile(r"^([^\s=]+)\s*=\s*(?:(['\"])(.*?)\2|(\S*))$", re.M)


@frozen_dataclass_decorator
Expand Down Expand Up @@ -497,12 +498,12 @@ def links(self) -> "MultiDictProxy[MultiDictProxy[str | URL]]":
link: MultiDict[str | URL] = MultiDict()

for param in params:
match = re.match(r"^\s*(\S*)\s*=\s*(['\"]?)(.*?)(\2)\s*$", param, re.M)
match = _LINK_PARAM_RE.match(param.strip())
if match is None: # Malformed param
continue
key, _, value, _ = match.groups()
key, _, value_quoted, value_unquoted = match.groups()

link.add(key, value)
link.add(key, value_unquoted if value_quoted is None else value_quoted)

key = link.get("rel", url)

Expand Down
3 changes: 2 additions & 1 deletion aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,16 @@
[ \t]*
(?:
"( (?:[^"\\]|\\.)* )" # group 1: top-level quoted-string
[ \t]* (?:,|\Z)
| ( # group 2: unquoted element
(?:
(?<=[^\s]=) {_QUOTED_STRING} # parameter quoted value
| (?<=\s) \( {_ESCAPED_COMMENT} \) # comment
| [^,] # any non-comma character
)+?
)
(?:,|\Z)
)
[ \t]* (?:,|\Z)
""",
re.VERBOSE,
)
Expand Down
5 changes: 2 additions & 3 deletions docs/web_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -970,7 +970,7 @@ and :ref:`aiohttp-web-signals` handlers::
.. class:: WebSocketResponse(*, timeout=10.0, receive_timeout=None, \
autoclose=True, autoping=True, heartbeat=None, \
protocols=(), compress=True, max_msg_size=4194304, \
writer_limit=65536, decode_text=True)
writer_limit=262144, decode_text=True)
:canonical: aiohttp.web_ws.WebSocketResponse

Class for handling server-side websockets, inherited from
Expand All @@ -984,7 +984,6 @@ and :ref:`aiohttp-web-signals` handlers::
To enable back-pressure from slow websocket clients treat methods
:meth:`ping`, :meth:`pong`, :meth:`send_str`,
:meth:`send_bytes`, :meth:`send_json`, :meth:`send_frame` as coroutines.
By default write buffer size is set to 64k.

:param bool autoping: Automatically send
:const:`~aiohttp.WSMsgType.PONG` on
Expand Down Expand Up @@ -1029,7 +1028,7 @@ and :ref:`aiohttp-web-signals` handlers::
``request.transport.close()`` to avoid
leaking resources.

:param int writer_limit: maximum size of write buffer, 64 KB by default.
:param int writer_limit: maximum size of write buffer, 256 KiB by default.
Once the buffer is full, the websocket will pause
to drain the buffer.

Expand Down
12 changes: 12 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,18 @@ async def loop_debug_mode() -> AsyncIterator[None]:
loop.set_debug(False)


@pytest.fixture(scope="session")
def symlinks_supported() -> None:
"""Skip tests if symlinks not support (e.g. Windows permission problem)."""
with TemporaryDirectory() as tmp_dir:
target = Path(tmp_dir) / "symlink-target"
target.touch()
try:
(Path(tmp_dir) / "symlink").symlink_to(target)
except OSError as exc: # pragma: no cover
pytest.skip(f"requires privilege to create symlinks: {exc}")


@pytest.fixture
def unix_sockname(
tmp_path: Path, tmp_path_factory: pytest.TempPathFactory
Expand Down
39 changes: 39 additions & 0 deletions tests/test_benchmarks_client_response.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""codspeed benchmarks for ClientResponse."""

from typing import TYPE_CHECKING

import pytest

from aiohttp.client_reqrep import _LINK_PARAM_RE

if TYPE_CHECKING:
from pytest_codspeed import BenchmarkFixture
else:
pytest_codspeed = pytest.importorskip("pytest_codspeed")
BenchmarkFixture = pytest_codspeed.BenchmarkFixture


@pytest.mark.parametrize(
"value",
(
pytest.param("\n\t" * 13367 + "\x00", id="embedded_newlines"),
pytest.param('\x00="' + "\t" * 14436 + "\x00", id="null_key_quote"),
pytest.param(
("a=" * 6000) + "'" + ("b" * 6000) + " " + "b",
id="key_swallows_equals",
),
pytest.param(
"key=" + (" " * 16000) + "'" + ("x" * 16000) + " " + "x",
id="whitespace_run_after_equals",
),
),
)
def test_link_param_pattern_redos_payload(
value: str, benchmark: "BenchmarkFixture"
) -> None:
# None of these payloads describe a valid link param; they must not match.
assert _LINK_PARAM_RE.match(value) is None

@benchmark
def _run() -> None:
_LINK_PARAM_RE.match(value)
36 changes: 36 additions & 0 deletions tests/test_benchmarks_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""codspeed benchmarks for aiohttp.helpers."""

from typing import TYPE_CHECKING

import pytest

from aiohttp.helpers import _LIST_ELEMENT_RE

if TYPE_CHECKING:
from pytest_codspeed import BenchmarkFixture
else:
pytest_codspeed = pytest.importorskip("pytest_codspeed")
BenchmarkFixture = pytest_codspeed.BenchmarkFixture


@pytest.mark.parametrize(
"value",
(
pytest.param(
"a" + (" " * 16000) + "x",
id="whitespace_run_after_content",
),
pytest.param(
r'\="(' * 16309,
id="interleaved_quote_paren_triggers",
),
),
)
def test_list_element_pattern_redos_payload(
value: str, benchmark: "BenchmarkFixture"
) -> None:
assert len(list(_LIST_ELEMENT_RE.finditer(value))) == 1

@benchmark
def _run() -> None:
list(_LIST_ELEMENT_RE.finditer(value))
8 changes: 6 additions & 2 deletions tests/test_urldispatch.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,9 @@ def test_add_static_append_version_non_exists_file_without_slash(


def test_add_static_append_version_follow_symlink(
router: web.UrlDispatcher, tmp_path: pathlib.Path
router: web.UrlDispatcher,
tmp_path: pathlib.Path,
symlinks_supported: None,
) -> None:
# Tests the access to a symlink, in static folder with apeend_version
symlink_path = tmp_path / "append_version_symlink"
Expand All @@ -486,7 +488,9 @@ def test_add_static_append_version_follow_symlink(


def test_add_static_append_version_not_follow_symlink(
router: web.UrlDispatcher, tmp_path: pathlib.Path
router: web.UrlDispatcher,
tmp_path: pathlib.Path,
symlinks_supported: None,
) -> None:
# Tests the access to a symlink, in static folder with apeend_version

Expand Down
16 changes: 12 additions & 4 deletions tests/test_web_urldispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,9 @@ async def test_access_root_of_static_handler_xss(


async def test_follow_symlink(
tmp_path: pathlib.Path, aiohttp_client: AiohttpClient
tmp_path: pathlib.Path,
aiohttp_client: AiohttpClient,
symlinks_supported: None,
) -> None:
# Tests the access to a symlink, in static folder
data = "hello world"
Expand Down Expand Up @@ -257,7 +259,9 @@ async def test_follow_symlink_directory_traversal(


async def test_follow_symlink_directory_traversal_after_normalization(
tmp_path: pathlib.Path, aiohttp_client: AiohttpClient
tmp_path: pathlib.Path,
aiohttp_client: AiohttpClient,
symlinks_supported: None,
) -> None:
# Tests that break_symlink_sandbox does not allow directory transversal
# after normalization
Expand Down Expand Up @@ -518,7 +522,9 @@ def mock_open(self: pathlib.Path, *args: Any, **kwargs: Any) -> Any:


async def test_access_symlink_loop(
tmp_path: pathlib.Path, aiohttp_client: AiohttpClient
tmp_path: pathlib.Path,
aiohttp_client: AiohttpClient,
symlinks_supported: None,
) -> None:
# Tests the access to a looped symlink, which could not be resolved.
my_dir_path = tmp_path / "my_symlink"
Expand All @@ -536,7 +542,9 @@ async def test_access_symlink_loop(


async def test_access_compressed_file_as_symlink(
tmp_path: pathlib.Path, aiohttp_client: AiohttpClient
tmp_path: pathlib.Path,
aiohttp_client: AiohttpClient,
symlinks_supported: None,
) -> None:
"""Test that compressed file variants as symlinks are ignored."""
private_file = tmp_path / "private.txt"
Expand Down
Loading