fluidattacks_core_http 12.0.0__tar.gz → 12.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/.gitignore +4 -0
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/PKG-INFO +1 -1
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/build/default.nix +0 -1
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/build/filter.nix +1 -0
- fluidattacks_core_http-12.1.0/fluidattacks_core/http/client.py +158 -0
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/import-linter.cfg +1 -10
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/pyproject.toml +3 -1
- fluidattacks_core_http-12.1.0/tests/__init__.py +1 -0
- fluidattacks_core_http-12.1.0/tests/test_client.py +239 -0
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/uv.lock +52 -1
- fluidattacks_core_http-12.0.0/fluidattacks_core/http/client.py +0 -90
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/.envrc +0 -0
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/flake.lock +0 -0
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/flake.nix +0 -0
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/fluidattacks_core/http/__init__.py +0 -0
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/fluidattacks_core/http/py.typed +0 -0
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/fluidattacks_core/http/validations.py +0 -0
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/mypy.ini +0 -0
- {fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/ruff.toml +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import ipaddress
|
|
2
|
+
import ssl
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import (
|
|
5
|
+
Any,
|
|
6
|
+
Literal,
|
|
7
|
+
)
|
|
8
|
+
from urllib.parse import urljoin
|
|
9
|
+
|
|
10
|
+
import aiohttp
|
|
11
|
+
import certifi
|
|
12
|
+
|
|
13
|
+
from .validations import (
|
|
14
|
+
validate_local_request,
|
|
15
|
+
validate_url,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
_MAX_REDIRECTS = 10
|
|
19
|
+
_METHODS_SWITCH_TO_GET = frozenset({301, 302, 303})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_secure_connector(
|
|
23
|
+
*,
|
|
24
|
+
allow_local_network: bool,
|
|
25
|
+
allow_localhost: bool,
|
|
26
|
+
) -> type[aiohttp.TCPConnector]:
|
|
27
|
+
class SecureTCPConnector(aiohttp.TCPConnector):
|
|
28
|
+
async def _resolve_host(
|
|
29
|
+
self,
|
|
30
|
+
host: str,
|
|
31
|
+
port: int,
|
|
32
|
+
traces: Sequence[aiohttp.tracing.Trace] | None = None,
|
|
33
|
+
) -> list[aiohttp.abc.ResolveResult]:
|
|
34
|
+
hosts = await super()._resolve_host(host, port, traces)
|
|
35
|
+
resolved_ips = [ipaddress.ip_address(host["host"]) for host in hosts]
|
|
36
|
+
validate_local_request(
|
|
37
|
+
resolved_ips,
|
|
38
|
+
allow_local_network=allow_local_network,
|
|
39
|
+
allow_localhost=allow_localhost,
|
|
40
|
+
)
|
|
41
|
+
return hosts
|
|
42
|
+
|
|
43
|
+
return SecureTCPConnector
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def request( # noqa: PLR0913
|
|
47
|
+
url: str,
|
|
48
|
+
*,
|
|
49
|
+
method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
50
|
+
allow_local_network: bool = False,
|
|
51
|
+
allow_localhost: bool = False,
|
|
52
|
+
allow_redirects: bool | None = None,
|
|
53
|
+
ascii_only: bool = False,
|
|
54
|
+
dns_rebind_protection: bool = True,
|
|
55
|
+
enforce_sanitization: bool = False,
|
|
56
|
+
headers: dict[str, str] | None = None,
|
|
57
|
+
json: Any | None = None, # noqa: ANN401
|
|
58
|
+
ports: list[int] | None = None,
|
|
59
|
+
schemes: list[str] | None = None,
|
|
60
|
+
timeout: int = 10, # noqa: ASYNC109
|
|
61
|
+
) -> aiohttp.ClientResponse:
|
|
62
|
+
validate_url(
|
|
63
|
+
url,
|
|
64
|
+
ascii_only=ascii_only,
|
|
65
|
+
enforce_sanitization=enforce_sanitization,
|
|
66
|
+
ports=ports or [],
|
|
67
|
+
schemes=schemes or [],
|
|
68
|
+
)
|
|
69
|
+
connector = (
|
|
70
|
+
get_secure_connector(
|
|
71
|
+
allow_local_network=allow_local_network,
|
|
72
|
+
allow_localhost=allow_localhost,
|
|
73
|
+
)
|
|
74
|
+
if dns_rebind_protection
|
|
75
|
+
else aiohttp.TCPConnector
|
|
76
|
+
)
|
|
77
|
+
connection = connector(
|
|
78
|
+
ssl=ssl.create_default_context(cafile=certifi.where()),
|
|
79
|
+
)
|
|
80
|
+
if allow_redirects is True:
|
|
81
|
+
return await _request_with_validated_redirects(
|
|
82
|
+
connection=connection,
|
|
83
|
+
headers=headers,
|
|
84
|
+
initial_url=url,
|
|
85
|
+
initial_method=method,
|
|
86
|
+
json=json,
|
|
87
|
+
timeout=timeout,
|
|
88
|
+
validation_options={
|
|
89
|
+
"ascii_only": ascii_only,
|
|
90
|
+
"enforce_sanitization": enforce_sanitization,
|
|
91
|
+
"ports": ports or [],
|
|
92
|
+
"schemes": schemes or [],
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
aiohttp_follow = allow_redirects if allow_redirects is not None else not dns_rebind_protection
|
|
96
|
+
|
|
97
|
+
async with (
|
|
98
|
+
aiohttp.ClientSession(
|
|
99
|
+
connector=connection,
|
|
100
|
+
headers=headers,
|
|
101
|
+
) as session,
|
|
102
|
+
session.request(
|
|
103
|
+
method,
|
|
104
|
+
url,
|
|
105
|
+
allow_redirects=aiohttp_follow,
|
|
106
|
+
json=json,
|
|
107
|
+
timeout=aiohttp.ClientTimeout(total=timeout),
|
|
108
|
+
) as response,
|
|
109
|
+
):
|
|
110
|
+
await response.read()
|
|
111
|
+
return response
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def _request_with_validated_redirects( # noqa: PLR0913
|
|
115
|
+
*,
|
|
116
|
+
connection: aiohttp.BaseConnector,
|
|
117
|
+
headers: dict[str, str] | None,
|
|
118
|
+
initial_url: str,
|
|
119
|
+
initial_method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
120
|
+
json: Any, # noqa: ANN401
|
|
121
|
+
timeout: int, # noqa: ASYNC109
|
|
122
|
+
validation_options: dict[str, Any],
|
|
123
|
+
) -> aiohttp.ClientResponse:
|
|
124
|
+
"""Follow redirects manually so validate_url runs on every hop.
|
|
125
|
+
|
|
126
|
+
aiohttp's ``allow_redirects=True`` short-circuits our
|
|
127
|
+
scheme/port/ascii/sanitization checks after the first hop, letting a
|
|
128
|
+
compromised server redirect to a target the caller had restricted.
|
|
129
|
+
"""
|
|
130
|
+
current_url = initial_url
|
|
131
|
+
current_method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] = initial_method
|
|
132
|
+
current_json: Any = json
|
|
133
|
+
async with aiohttp.ClientSession(connector=connection, headers=headers) as session:
|
|
134
|
+
for _ in range(_MAX_REDIRECTS + 1):
|
|
135
|
+
async with session.request(
|
|
136
|
+
current_method,
|
|
137
|
+
current_url,
|
|
138
|
+
allow_redirects=False,
|
|
139
|
+
json=current_json,
|
|
140
|
+
timeout=aiohttp.ClientTimeout(total=timeout),
|
|
141
|
+
) as response:
|
|
142
|
+
await response.read()
|
|
143
|
+
status = response.status
|
|
144
|
+
location = response.headers.get("Location")
|
|
145
|
+
if not (300 <= status < 400) or location is None:
|
|
146
|
+
return response
|
|
147
|
+
current_url = urljoin(current_url, location)
|
|
148
|
+
validate_url(current_url, **validation_options)
|
|
149
|
+
if status in _METHODS_SWITCH_TO_GET:
|
|
150
|
+
current_method = "GET"
|
|
151
|
+
current_json = None
|
|
152
|
+
raise aiohttp.TooManyRedirects(
|
|
153
|
+
response.request_info,
|
|
154
|
+
(),
|
|
155
|
+
message=(
|
|
156
|
+
f"exceeded {_MAX_REDIRECTS} redirects, aborted before requesting {current_url}"
|
|
157
|
+
),
|
|
158
|
+
)
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
[importlinter]
|
|
2
|
-
root_package = fluidattacks_core
|
|
2
|
+
root_package = fluidattacks_core.http
|
|
3
3
|
include_external_packages = True
|
|
4
4
|
|
|
5
|
-
[importlinter:contract:portion]
|
|
6
|
-
name = Single Portion
|
|
7
|
-
type = layers
|
|
8
|
-
containers =
|
|
9
|
-
fluidattacks_core
|
|
10
|
-
exhaustive = true
|
|
11
|
-
layers =
|
|
12
|
-
http
|
|
13
|
-
|
|
14
5
|
[importlinter:contract:dag]
|
|
15
6
|
name = Direct Acyclic Graph
|
|
16
7
|
type = layers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fluidattacks_core_http"
|
|
3
|
-
version = "12.
|
|
3
|
+
version = "12.1.0"
|
|
4
4
|
description = "Fluid Attacks Core HTTP Library"
|
|
5
5
|
authors = [{ name = "Development", email = "development@fluidattacks.com" }]
|
|
6
6
|
license = { text = "MPL-2.0" }
|
|
@@ -24,6 +24,8 @@ dev = [
|
|
|
24
24
|
"deptry>=0.23.1",
|
|
25
25
|
"import-linter==2.11",
|
|
26
26
|
"mypy==1.19.1",
|
|
27
|
+
"pytest==9.0.3",
|
|
28
|
+
"pytest-asyncio>=1.1.0",
|
|
27
29
|
"ruff==0.15.0",
|
|
28
30
|
]
|
|
29
31
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Tests for fluidattacks_core.http."""
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Tests for fluidattacks_core.http.client.request."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
import pytest
|
|
7
|
+
from aiohttp import web
|
|
8
|
+
from aiohttp.test_utils import TestServer
|
|
9
|
+
|
|
10
|
+
from fluidattacks_core.http import request
|
|
11
|
+
from fluidattacks_core.http.validations import HTTPValidationError
|
|
12
|
+
|
|
13
|
+
RouteHandler = Callable[[web.Request], Awaitable[web.StreamResponse]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def _build_server(routes: dict[str, RouteHandler]) -> TestServer:
|
|
17
|
+
app = web.Application()
|
|
18
|
+
for path, handler in routes.items():
|
|
19
|
+
app.router.add_get(path, handler)
|
|
20
|
+
app.router.add_post(path, handler)
|
|
21
|
+
server = TestServer(app)
|
|
22
|
+
await server.start_server()
|
|
23
|
+
return server
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
async def _permissive_request(
|
|
27
|
+
url: str,
|
|
28
|
+
*,
|
|
29
|
+
method: str = "GET",
|
|
30
|
+
allow_redirects: bool | None = None,
|
|
31
|
+
schemes: list[str] | None = None,
|
|
32
|
+
ports: list[int] | None = None,
|
|
33
|
+
json: object | None = None,
|
|
34
|
+
) -> aiohttp.ClientResponse:
|
|
35
|
+
"""Wrapper that turns off protections irrelevant to redirect-logic tests."""
|
|
36
|
+
return await request(
|
|
37
|
+
url,
|
|
38
|
+
method=method, # type: ignore[arg-type]
|
|
39
|
+
allow_redirects=allow_redirects,
|
|
40
|
+
allow_local_network=True,
|
|
41
|
+
allow_localhost=True,
|
|
42
|
+
dns_rebind_protection=False,
|
|
43
|
+
schemes=schemes,
|
|
44
|
+
ports=ports,
|
|
45
|
+
json=json,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _make_server_url(server: TestServer, path: str) -> str:
|
|
50
|
+
return f"http://{server.host}:{server.port}{path}"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.asyncio
|
|
54
|
+
async def test_allow_redirects_default_does_not_follow_when_rebind_protected() -> None:
|
|
55
|
+
hits: list[str] = []
|
|
56
|
+
|
|
57
|
+
async def handle_start(_req: web.Request) -> web.Response:
|
|
58
|
+
hits.append("start")
|
|
59
|
+
return web.Response(status=301, headers={"Location": "/final"})
|
|
60
|
+
|
|
61
|
+
async def handle_final(_req: web.Request) -> web.Response:
|
|
62
|
+
hits.append("final")
|
|
63
|
+
return web.Response(status=200)
|
|
64
|
+
|
|
65
|
+
server = await _build_server({"/start": handle_start, "/final": handle_final})
|
|
66
|
+
try:
|
|
67
|
+
response = await request(
|
|
68
|
+
_make_server_url(server, "/start"),
|
|
69
|
+
method="GET",
|
|
70
|
+
allow_local_network=True,
|
|
71
|
+
allow_localhost=True,
|
|
72
|
+
)
|
|
73
|
+
assert response.status == 301
|
|
74
|
+
assert hits == ["start"]
|
|
75
|
+
finally:
|
|
76
|
+
await server.close()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_allow_redirects_default_follows_when_rebind_disabled() -> None:
|
|
81
|
+
async def handle_start(_req: web.Request) -> web.Response:
|
|
82
|
+
return web.Response(status=301, headers={"Location": "/final"})
|
|
83
|
+
|
|
84
|
+
async def handle_final(_req: web.Request) -> web.Response:
|
|
85
|
+
return web.Response(status=200)
|
|
86
|
+
|
|
87
|
+
server = await _build_server({"/start": handle_start, "/final": handle_final})
|
|
88
|
+
try:
|
|
89
|
+
response = await _permissive_request(_make_server_url(server, "/start"))
|
|
90
|
+
assert response.status == 200
|
|
91
|
+
finally:
|
|
92
|
+
await server.close()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.asyncio
|
|
96
|
+
async def test_allow_redirects_false_never_follows() -> None:
|
|
97
|
+
async def handle_start(_req: web.Request) -> web.Response:
|
|
98
|
+
return web.Response(status=301, headers={"Location": "/final"})
|
|
99
|
+
|
|
100
|
+
async def handle_final(_req: web.Request) -> web.Response:
|
|
101
|
+
return web.Response(status=200)
|
|
102
|
+
|
|
103
|
+
server = await _build_server({"/start": handle_start, "/final": handle_final})
|
|
104
|
+
try:
|
|
105
|
+
response = await _permissive_request(
|
|
106
|
+
_make_server_url(server, "/start"), allow_redirects=False
|
|
107
|
+
)
|
|
108
|
+
assert response.status == 301
|
|
109
|
+
finally:
|
|
110
|
+
await server.close()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@pytest.mark.asyncio
|
|
114
|
+
async def test_allow_redirects_true_follows_canonical_chain_to_2xx() -> None:
|
|
115
|
+
async def handle_start(_req: web.Request) -> web.Response:
|
|
116
|
+
return web.Response(status=301, headers={"Location": "/final"})
|
|
117
|
+
|
|
118
|
+
async def handle_final(_req: web.Request) -> web.Response:
|
|
119
|
+
return web.Response(status=200, text="ok")
|
|
120
|
+
|
|
121
|
+
server = await _build_server({"/start": handle_start, "/final": handle_final})
|
|
122
|
+
try:
|
|
123
|
+
response = await _permissive_request(
|
|
124
|
+
_make_server_url(server, "/start"), allow_redirects=True
|
|
125
|
+
)
|
|
126
|
+
assert response.status == 200
|
|
127
|
+
assert await response.text() == "ok"
|
|
128
|
+
finally:
|
|
129
|
+
await server.close()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.mark.asyncio
|
|
133
|
+
async def test_allow_redirects_true_validates_scheme_on_redirect_target() -> None:
|
|
134
|
+
async def handle_start(_req: web.Request) -> web.Response:
|
|
135
|
+
return web.Response(
|
|
136
|
+
status=302, headers={"Location": "ftp://outside.example.com/attack"}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
server = await _build_server({"/start": handle_start})
|
|
140
|
+
try:
|
|
141
|
+
with pytest.raises(HTTPValidationError):
|
|
142
|
+
await _permissive_request(
|
|
143
|
+
_make_server_url(server, "/start"),
|
|
144
|
+
allow_redirects=True,
|
|
145
|
+
schemes=["http"],
|
|
146
|
+
)
|
|
147
|
+
finally:
|
|
148
|
+
await server.close()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@pytest.mark.asyncio
|
|
152
|
+
async def test_allow_redirects_true_validates_port_on_redirect_target() -> None:
|
|
153
|
+
async def handle_start(_req: web.Request) -> web.Response:
|
|
154
|
+
return web.Response(
|
|
155
|
+
status=302,
|
|
156
|
+
headers={"Location": "http://outside.example.com:22/attack"},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
server = await _build_server({"/start": handle_start})
|
|
160
|
+
try:
|
|
161
|
+
with pytest.raises(HTTPValidationError):
|
|
162
|
+
await _permissive_request(
|
|
163
|
+
_make_server_url(server, "/start"),
|
|
164
|
+
allow_redirects=True,
|
|
165
|
+
ports=[80, 443],
|
|
166
|
+
)
|
|
167
|
+
finally:
|
|
168
|
+
await server.close()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.mark.asyncio
|
|
172
|
+
@pytest.mark.parametrize("status", [301, 302, 303])
|
|
173
|
+
async def test_status_rewrites_follow_up_method_to_get(status: int) -> None:
|
|
174
|
+
methods: list[str] = []
|
|
175
|
+
|
|
176
|
+
async def handle_start(req: web.Request) -> web.Response:
|
|
177
|
+
methods.append(req.method)
|
|
178
|
+
return web.Response(status=status, headers={"Location": "/final"})
|
|
179
|
+
|
|
180
|
+
async def handle_final(req: web.Request) -> web.Response:
|
|
181
|
+
methods.append(req.method)
|
|
182
|
+
return web.Response(status=200)
|
|
183
|
+
|
|
184
|
+
server = await _build_server({"/start": handle_start, "/final": handle_final})
|
|
185
|
+
try:
|
|
186
|
+
response = await _permissive_request(
|
|
187
|
+
_make_server_url(server, "/start"),
|
|
188
|
+
method="POST",
|
|
189
|
+
allow_redirects=True,
|
|
190
|
+
json={"payload": "value"},
|
|
191
|
+
)
|
|
192
|
+
assert response.status == 200
|
|
193
|
+
assert methods == ["POST", "GET"]
|
|
194
|
+
finally:
|
|
195
|
+
await server.close()
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@pytest.mark.asyncio
|
|
199
|
+
@pytest.mark.parametrize("status", [307, 308])
|
|
200
|
+
async def test_status_preserves_follow_up_method(status: int) -> None:
|
|
201
|
+
methods: list[str] = []
|
|
202
|
+
|
|
203
|
+
async def handle_start(req: web.Request) -> web.Response:
|
|
204
|
+
methods.append(req.method)
|
|
205
|
+
return web.Response(status=status, headers={"Location": "/final"})
|
|
206
|
+
|
|
207
|
+
async def handle_final(req: web.Request) -> web.Response:
|
|
208
|
+
methods.append(req.method)
|
|
209
|
+
return web.Response(status=200)
|
|
210
|
+
|
|
211
|
+
server = await _build_server({"/start": handle_start, "/final": handle_final})
|
|
212
|
+
try:
|
|
213
|
+
response = await _permissive_request(
|
|
214
|
+
_make_server_url(server, "/start"),
|
|
215
|
+
method="POST",
|
|
216
|
+
allow_redirects=True,
|
|
217
|
+
json={"payload": "value"},
|
|
218
|
+
)
|
|
219
|
+
assert response.status == 200
|
|
220
|
+
assert methods == ["POST", "POST"]
|
|
221
|
+
finally:
|
|
222
|
+
await server.close()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@pytest.mark.asyncio
|
|
226
|
+
async def test_exceeding_max_redirects_raises_too_many_redirects() -> None:
|
|
227
|
+
async def handle_hop(req: web.Request) -> web.Response:
|
|
228
|
+
return web.Response(
|
|
229
|
+
status=301, headers={"Location": f"/hop?n={req.query.get('n', '0')}"}
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
server = await _build_server({"/hop": handle_hop})
|
|
233
|
+
try:
|
|
234
|
+
with pytest.raises(aiohttp.TooManyRedirects):
|
|
235
|
+
await _permissive_request(
|
|
236
|
+
_make_server_url(server, "/hop?n=0"), allow_redirects=True
|
|
237
|
+
)
|
|
238
|
+
finally:
|
|
239
|
+
await server.close()
|
|
@@ -217,7 +217,7 @@ wheels = [
|
|
|
217
217
|
|
|
218
218
|
[[package]]
|
|
219
219
|
name = "fluidattacks-core-http"
|
|
220
|
-
version = "12.
|
|
220
|
+
version = "12.1.0"
|
|
221
221
|
source = { editable = "." }
|
|
222
222
|
dependencies = [
|
|
223
223
|
{ name = "aiohttp" },
|
|
@@ -230,6 +230,8 @@ dev = [
|
|
|
230
230
|
{ name = "deptry" },
|
|
231
231
|
{ name = "import-linter" },
|
|
232
232
|
{ name = "mypy" },
|
|
233
|
+
{ name = "pytest" },
|
|
234
|
+
{ name = "pytest-asyncio" },
|
|
233
235
|
{ name = "ruff" },
|
|
234
236
|
]
|
|
235
237
|
|
|
@@ -245,6 +247,8 @@ dev = [
|
|
|
245
247
|
{ name = "deptry", specifier = ">=0.23.1" },
|
|
246
248
|
{ name = "import-linter", specifier = "==2.11" },
|
|
247
249
|
{ name = "mypy", specifier = "==1.19.1" },
|
|
250
|
+
{ name = "pytest", specifier = "==9.0.3" },
|
|
251
|
+
{ name = "pytest-asyncio", specifier = ">=1.1.0" },
|
|
248
252
|
{ name = "ruff", specifier = "==0.15.0" },
|
|
249
253
|
]
|
|
250
254
|
|
|
@@ -465,6 +469,15 @@ wheels = [
|
|
|
465
469
|
{ url = "https://files.pythonhosted.org/packages/e9/aa/2ed2c89543632ded7196e0d93dcc6c7fe87769e88391a648c4a298ea864a/import_linter-2.11-py3-none-any.whl", hash = "sha256:3dc54cae933bae3430358c30989762b721c77aa99d424f56a08265be0eeaa465", size = 637315, upload-time = "2026-03-06T12:11:36.599Z" },
|
|
466
470
|
]
|
|
467
471
|
|
|
472
|
+
[[package]]
|
|
473
|
+
name = "iniconfig"
|
|
474
|
+
version = "2.3.0"
|
|
475
|
+
source = { registry = "https://pypi.org/simple" }
|
|
476
|
+
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
|
477
|
+
wheels = [
|
|
478
|
+
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
|
479
|
+
]
|
|
480
|
+
|
|
468
481
|
[[package]]
|
|
469
482
|
name = "librt"
|
|
470
483
|
version = "0.13.0"
|
|
@@ -744,6 +757,15 @@ wheels = [
|
|
|
744
757
|
{ url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
|
|
745
758
|
]
|
|
746
759
|
|
|
760
|
+
[[package]]
|
|
761
|
+
name = "pluggy"
|
|
762
|
+
version = "1.6.0"
|
|
763
|
+
source = { registry = "https://pypi.org/simple" }
|
|
764
|
+
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
|
765
|
+
wheels = [
|
|
766
|
+
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
|
767
|
+
]
|
|
768
|
+
|
|
747
769
|
[[package]]
|
|
748
770
|
name = "propcache"
|
|
749
771
|
version = "0.5.2"
|
|
@@ -864,6 +886,35 @@ wheels = [
|
|
|
864
886
|
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
|
865
887
|
]
|
|
866
888
|
|
|
889
|
+
[[package]]
|
|
890
|
+
name = "pytest"
|
|
891
|
+
version = "9.0.3"
|
|
892
|
+
source = { registry = "https://pypi.org/simple" }
|
|
893
|
+
dependencies = [
|
|
894
|
+
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
|
895
|
+
{ name = "iniconfig" },
|
|
896
|
+
{ name = "packaging" },
|
|
897
|
+
{ name = "pluggy" },
|
|
898
|
+
{ name = "pygments" },
|
|
899
|
+
]
|
|
900
|
+
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
|
901
|
+
wheels = [
|
|
902
|
+
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
|
903
|
+
]
|
|
904
|
+
|
|
905
|
+
[[package]]
|
|
906
|
+
name = "pytest-asyncio"
|
|
907
|
+
version = "1.4.0"
|
|
908
|
+
source = { registry = "https://pypi.org/simple" }
|
|
909
|
+
dependencies = [
|
|
910
|
+
{ name = "pytest" },
|
|
911
|
+
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
912
|
+
]
|
|
913
|
+
sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" }
|
|
914
|
+
wheels = [
|
|
915
|
+
{ url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" },
|
|
916
|
+
]
|
|
917
|
+
|
|
867
918
|
[[package]]
|
|
868
919
|
name = "requirements-parser"
|
|
869
920
|
version = "0.13.1"
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import ipaddress
|
|
2
|
-
import ssl
|
|
3
|
-
from collections.abc import Sequence
|
|
4
|
-
from typing import (
|
|
5
|
-
Any,
|
|
6
|
-
Literal,
|
|
7
|
-
)
|
|
8
|
-
|
|
9
|
-
import aiohttp
|
|
10
|
-
import certifi
|
|
11
|
-
|
|
12
|
-
from .validations import (
|
|
13
|
-
validate_local_request,
|
|
14
|
-
validate_url,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def get_secure_connector(
|
|
19
|
-
*,
|
|
20
|
-
allow_local_network: bool,
|
|
21
|
-
allow_localhost: bool,
|
|
22
|
-
) -> type[aiohttp.TCPConnector]:
|
|
23
|
-
class SecureTCPConnector(aiohttp.TCPConnector):
|
|
24
|
-
async def _resolve_host(
|
|
25
|
-
self,
|
|
26
|
-
host: str,
|
|
27
|
-
port: int,
|
|
28
|
-
traces: Sequence[aiohttp.tracing.Trace] | None = None,
|
|
29
|
-
) -> list[aiohttp.abc.ResolveResult]:
|
|
30
|
-
hosts = await super()._resolve_host(host, port, traces)
|
|
31
|
-
resolved_ips = [ipaddress.ip_address(host["host"]) for host in hosts]
|
|
32
|
-
validate_local_request(
|
|
33
|
-
resolved_ips,
|
|
34
|
-
allow_local_network=allow_local_network,
|
|
35
|
-
allow_localhost=allow_localhost,
|
|
36
|
-
)
|
|
37
|
-
return hosts
|
|
38
|
-
|
|
39
|
-
return SecureTCPConnector
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
async def request( # noqa: PLR0913
|
|
43
|
-
url: str,
|
|
44
|
-
*,
|
|
45
|
-
method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"],
|
|
46
|
-
allow_local_network: bool = False,
|
|
47
|
-
allow_localhost: bool = False,
|
|
48
|
-
ascii_only: bool = False,
|
|
49
|
-
dns_rebind_protection: bool = True,
|
|
50
|
-
enforce_sanitization: bool = False,
|
|
51
|
-
headers: dict[str, str] | None = None,
|
|
52
|
-
json: Any | None = None, # noqa: ANN401
|
|
53
|
-
ports: list[int] | None = None,
|
|
54
|
-
schemes: list[str] | None = None,
|
|
55
|
-
timeout: int = 10, # noqa: ASYNC109
|
|
56
|
-
) -> aiohttp.ClientResponse:
|
|
57
|
-
validate_url(
|
|
58
|
-
url,
|
|
59
|
-
ascii_only=ascii_only,
|
|
60
|
-
enforce_sanitization=enforce_sanitization,
|
|
61
|
-
ports=ports or [],
|
|
62
|
-
schemes=schemes or [],
|
|
63
|
-
)
|
|
64
|
-
connector = (
|
|
65
|
-
get_secure_connector(
|
|
66
|
-
allow_local_network=allow_local_network,
|
|
67
|
-
allow_localhost=allow_localhost,
|
|
68
|
-
)
|
|
69
|
-
if dns_rebind_protection
|
|
70
|
-
else aiohttp.TCPConnector
|
|
71
|
-
)
|
|
72
|
-
connection = connector(
|
|
73
|
-
ssl=ssl.create_default_context(cafile=certifi.where()),
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
async with (
|
|
77
|
-
aiohttp.ClientSession(
|
|
78
|
-
connector=connection,
|
|
79
|
-
headers=headers,
|
|
80
|
-
) as session,
|
|
81
|
-
session.request(
|
|
82
|
-
method,
|
|
83
|
-
url,
|
|
84
|
-
allow_redirects=not dns_rebind_protection,
|
|
85
|
-
json=json,
|
|
86
|
-
timeout=aiohttp.ClientTimeout(total=timeout),
|
|
87
|
-
) as response,
|
|
88
|
-
):
|
|
89
|
-
await response.read()
|
|
90
|
-
return response
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/fluidattacks_core/http/__init__.py
RENAMED
|
File without changes
|
{fluidattacks_core_http-12.0.0 → fluidattacks_core_http-12.1.0}/fluidattacks_core/http/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|