fluidattacks_core_http 12.1.0__tar.gz → 12.2.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.1.0 → fluidattacks_core_http-12.2.0}/PKG-INFO +1 -1
- fluidattacks_core_http-12.2.0/fluidattacks_core/http/__init__.py +7 -0
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/fluidattacks_core/http/client.py +54 -1
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/fluidattacks_core/http/validations.py +1 -1
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/pyproject.toml +1 -1
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/tests/test_client.py +222 -2
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/uv.lock +1 -1
- fluidattacks_core_http-12.1.0/build/default.nix +0 -19
- fluidattacks_core_http-12.1.0/build/filter.nix +0 -13
- fluidattacks_core_http-12.1.0/flake.lock +0 -303
- fluidattacks_core_http-12.1.0/flake.nix +0 -29
- fluidattacks_core_http-12.1.0/fluidattacks_core/http/__init__.py +0 -5
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/.envrc +0 -0
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/.gitignore +0 -0
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/fluidattacks_core/http/py.typed +0 -0
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/import-linter.cfg +0 -0
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/mypy.ini +0 -0
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/ruff.toml +0 -0
- {fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/tests/__init__.py +0 -0
{fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/fluidattacks_core/http/client.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import ipaddress
|
|
2
2
|
import ssl
|
|
3
3
|
from collections.abc import Sequence
|
|
4
|
+
from contextvars import ContextVar
|
|
4
5
|
from typing import (
|
|
5
6
|
Any,
|
|
6
7
|
Literal,
|
|
@@ -17,6 +18,51 @@ from .validations import (
|
|
|
17
18
|
|
|
18
19
|
_MAX_REDIRECTS = 10
|
|
19
20
|
_METHODS_SWITCH_TO_GET = frozenset({301, 302, 303})
|
|
21
|
+
_READ_CHUNK_BYTES = 64 * 1024
|
|
22
|
+
|
|
23
|
+
# Bounds each response so that no single request can exhaust the pod.
|
|
24
|
+
MAX_RESPONSE_BYTES = 64 * 1024 * 1024
|
|
25
|
+
|
|
26
|
+
# aiohttp builds the response object, so the caller's cap cannot be passed to
|
|
27
|
+
# it directly.
|
|
28
|
+
_CAP: ContextVar[int] = ContextVar("max_response_bytes", default=MAX_RESPONSE_BYTES)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ResponseTooLargeError(aiohttp.ClientPayloadError):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BoundedResponse(aiohttp.ClientResponse):
|
|
36
|
+
async def read(self) -> bytes:
|
|
37
|
+
if self._body is None:
|
|
38
|
+
max_response_bytes = _CAP.get()
|
|
39
|
+
try:
|
|
40
|
+
self._reject_oversized_content_length(max_response_bytes)
|
|
41
|
+
self._body = await self._accumulate_capped(max_response_bytes)
|
|
42
|
+
except BaseException:
|
|
43
|
+
self.close()
|
|
44
|
+
raise
|
|
45
|
+
# Deferring to the parent awaits the request writer through internals
|
|
46
|
+
# a subclass cannot name.
|
|
47
|
+
return await super().read()
|
|
48
|
+
|
|
49
|
+
def _reject_oversized_content_length(self, max_response_bytes: int) -> None:
|
|
50
|
+
declared = self.content_length
|
|
51
|
+
if declared is not None and declared > max_response_bytes:
|
|
52
|
+
msg = f"Response declares {declared} bytes, over the {max_response_bytes} limit"
|
|
53
|
+
raise ResponseTooLargeError(msg)
|
|
54
|
+
|
|
55
|
+
async def _accumulate_capped(self, max_response_bytes: int) -> bytes:
|
|
56
|
+
# Counting decoded bytes as they arrive is the only bound that
|
|
57
|
+
# survives a compressed or chunked body, where the declared length
|
|
58
|
+
# says nothing about what decoding produces.
|
|
59
|
+
body = bytearray()
|
|
60
|
+
async for chunk in self.content.iter_chunked(_READ_CHUNK_BYTES):
|
|
61
|
+
body.extend(chunk)
|
|
62
|
+
if len(body) > max_response_bytes:
|
|
63
|
+
msg = f"Response exceeds the {max_response_bytes} byte limit"
|
|
64
|
+
raise ResponseTooLargeError(msg)
|
|
65
|
+
return bytes(body)
|
|
20
66
|
|
|
21
67
|
|
|
22
68
|
def get_secure_connector(
|
|
@@ -55,6 +101,7 @@ async def request( # noqa: PLR0913
|
|
|
55
101
|
enforce_sanitization: bool = False,
|
|
56
102
|
headers: dict[str, str] | None = None,
|
|
57
103
|
json: Any | None = None, # noqa: ANN401
|
|
104
|
+
max_response_bytes: int = MAX_RESPONSE_BYTES,
|
|
58
105
|
ports: list[int] | None = None,
|
|
59
106
|
schemes: list[str] | None = None,
|
|
60
107
|
timeout: int = 10, # noqa: ASYNC109
|
|
@@ -77,6 +124,7 @@ async def request( # noqa: PLR0913
|
|
|
77
124
|
connection = connector(
|
|
78
125
|
ssl=ssl.create_default_context(cafile=certifi.where()),
|
|
79
126
|
)
|
|
127
|
+
_CAP.set(max_response_bytes)
|
|
80
128
|
if allow_redirects is True:
|
|
81
129
|
return await _request_with_validated_redirects(
|
|
82
130
|
connection=connection,
|
|
@@ -98,6 +146,7 @@ async def request( # noqa: PLR0913
|
|
|
98
146
|
aiohttp.ClientSession(
|
|
99
147
|
connector=connection,
|
|
100
148
|
headers=headers,
|
|
149
|
+
response_class=BoundedResponse,
|
|
101
150
|
) as session,
|
|
102
151
|
session.request(
|
|
103
152
|
method,
|
|
@@ -130,7 +179,11 @@ async def _request_with_validated_redirects( # noqa: PLR0913
|
|
|
130
179
|
current_url = initial_url
|
|
131
180
|
current_method: Literal["GET", "POST", "PUT", "PATCH", "DELETE"] = initial_method
|
|
132
181
|
current_json: Any = json
|
|
133
|
-
async with aiohttp.ClientSession(
|
|
182
|
+
async with aiohttp.ClientSession(
|
|
183
|
+
connector=connection,
|
|
184
|
+
headers=headers,
|
|
185
|
+
response_class=BoundedResponse,
|
|
186
|
+
) as session:
|
|
134
187
|
for _ in range(_MAX_REDIRECTS + 1):
|
|
135
188
|
async with session.request(
|
|
136
189
|
current_method,
|
|
@@ -10,7 +10,7 @@ class HTTPValidationError(Exception):
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def validate_scheme(scheme: str | None, schemes: list[str]) -> None:
|
|
13
|
-
if scheme
|
|
13
|
+
if scheme not in schemes:
|
|
14
14
|
msg = f"Only allowed schemes are {', '.join(schemes)}"
|
|
15
15
|
raise HTTPValidationError(msg)
|
|
16
16
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Tests for fluidattacks_core.http.client.request."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import gzip
|
|
3
5
|
from collections.abc import Awaitable, Callable
|
|
4
6
|
|
|
5
7
|
import aiohttp
|
|
@@ -7,10 +9,12 @@ import pytest
|
|
|
7
9
|
from aiohttp import web
|
|
8
10
|
from aiohttp.test_utils import TestServer
|
|
9
11
|
|
|
10
|
-
from fluidattacks_core.http import request
|
|
11
|
-
from fluidattacks_core.http.validations import HTTPValidationError
|
|
12
|
+
from fluidattacks_core.http import ResponseTooLargeError, request
|
|
13
|
+
from fluidattacks_core.http.validations import HTTPValidationError, validate_url
|
|
12
14
|
|
|
13
15
|
RouteHandler = Callable[[web.Request], Awaitable[web.StreamResponse]]
|
|
16
|
+
_CAP = 64 * 1024
|
|
17
|
+
_MAX_HOPS = 11
|
|
14
18
|
|
|
15
19
|
|
|
16
20
|
async def _build_server(routes: dict[str, RouteHandler]) -> TestServer:
|
|
@@ -237,3 +241,219 @@ async def test_exceeding_max_redirects_raises_too_many_redirects() -> None:
|
|
|
237
241
|
)
|
|
238
242
|
finally:
|
|
239
243
|
await server.close()
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _gzip_bomb(decompressed_bytes: int) -> bytes:
|
|
247
|
+
return gzip.compress(b"\x00" * decompressed_bytes, compresslevel=9)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
async def _build_body_server(body: bytes, headers: dict[str, str]) -> TestServer:
|
|
251
|
+
async def handler(_req: web.Request) -> web.Response:
|
|
252
|
+
return web.Response(body=body, headers=headers)
|
|
253
|
+
|
|
254
|
+
return await _build_server({"/body": handler})
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
async def _capped_request(
|
|
258
|
+
url: str,
|
|
259
|
+
*,
|
|
260
|
+
max_response_bytes: int,
|
|
261
|
+
allow_redirects: bool | None = None,
|
|
262
|
+
) -> aiohttp.ClientResponse:
|
|
263
|
+
return await request(
|
|
264
|
+
url,
|
|
265
|
+
method="GET",
|
|
266
|
+
allow_local_network=True,
|
|
267
|
+
allow_localhost=True,
|
|
268
|
+
allow_redirects=allow_redirects,
|
|
269
|
+
dns_rebind_protection=False,
|
|
270
|
+
max_response_bytes=max_response_bytes,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@pytest.mark.asyncio
|
|
275
|
+
async def test_compressed_body_over_cap_is_rejected() -> None:
|
|
276
|
+
bomb = _gzip_bomb(8 * 1024 * 1024)
|
|
277
|
+
server = await _build_body_server(
|
|
278
|
+
bomb,
|
|
279
|
+
{"Content-Encoding": "gzip", "Content-Type": "application/json"},
|
|
280
|
+
)
|
|
281
|
+
try:
|
|
282
|
+
assert len(bomb) < 1024 * 1024
|
|
283
|
+
with pytest.raises(ResponseTooLargeError):
|
|
284
|
+
await _capped_request(
|
|
285
|
+
_make_server_url(server, "/body"),
|
|
286
|
+
max_response_bytes=1024 * 1024,
|
|
287
|
+
)
|
|
288
|
+
finally:
|
|
289
|
+
await server.close()
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@pytest.mark.asyncio
|
|
293
|
+
async def test_body_within_cap_is_still_returned() -> None:
|
|
294
|
+
server = await _build_body_server(b"payload", {})
|
|
295
|
+
try:
|
|
296
|
+
response = await _capped_request(
|
|
297
|
+
_make_server_url(server, "/body"),
|
|
298
|
+
max_response_bytes=1024 * 1024,
|
|
299
|
+
)
|
|
300
|
+
assert await response.text() == "payload"
|
|
301
|
+
finally:
|
|
302
|
+
await server.close()
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@pytest.mark.asyncio
|
|
306
|
+
async def test_cap_rejects_an_oversized_body_after_a_redirect() -> None:
|
|
307
|
+
bomb = _gzip_bomb(8 * 1024 * 1024)
|
|
308
|
+
|
|
309
|
+
async def handle_start(_req: web.Request) -> web.Response:
|
|
310
|
+
return web.Response(status=302, headers={"Location": "/final"})
|
|
311
|
+
|
|
312
|
+
async def handle_final(_req: web.Request) -> web.Response:
|
|
313
|
+
return web.Response(
|
|
314
|
+
body=bomb,
|
|
315
|
+
headers={"Content-Encoding": "gzip", "Content-Type": "application/json"},
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
server = await _build_server({"/start": handle_start, "/final": handle_final})
|
|
319
|
+
try:
|
|
320
|
+
with pytest.raises(ResponseTooLargeError):
|
|
321
|
+
await _capped_request(
|
|
322
|
+
_make_server_url(server, "/start"),
|
|
323
|
+
max_response_bytes=1024 * 1024,
|
|
324
|
+
allow_redirects=True,
|
|
325
|
+
)
|
|
326
|
+
finally:
|
|
327
|
+
await server.close()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
@pytest.mark.asyncio
|
|
331
|
+
async def test_declared_length_over_cap_is_rejected() -> None:
|
|
332
|
+
server = await _build_body_server(b"a" * (2 * 1024 * 1024), {})
|
|
333
|
+
try:
|
|
334
|
+
with pytest.raises(ResponseTooLargeError, match="declares"):
|
|
335
|
+
await _capped_request(
|
|
336
|
+
_make_server_url(server, "/body"),
|
|
337
|
+
max_response_bytes=1024 * 1024,
|
|
338
|
+
)
|
|
339
|
+
finally:
|
|
340
|
+
await server.close()
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@pytest.mark.parametrize(
|
|
344
|
+
"url",
|
|
345
|
+
[
|
|
346
|
+
"//evil.example/jwks",
|
|
347
|
+
"//evil.example:8080/jwks",
|
|
348
|
+
"evil.example/jwks",
|
|
349
|
+
"evil.example:8080/jwks",
|
|
350
|
+
],
|
|
351
|
+
)
|
|
352
|
+
def test_schemeless_url_is_rejected_when_schemes_are_restricted(url: str) -> None:
|
|
353
|
+
with pytest.raises(HTTPValidationError):
|
|
354
|
+
validate_url(
|
|
355
|
+
url,
|
|
356
|
+
ascii_only=False,
|
|
357
|
+
enforce_sanitization=False,
|
|
358
|
+
ports=[],
|
|
359
|
+
schemes=["https"],
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_allowed_scheme_still_passes() -> None:
|
|
364
|
+
validate_url(
|
|
365
|
+
"https://good.example/jwks",
|
|
366
|
+
ascii_only=False,
|
|
367
|
+
enforce_sanitization=False,
|
|
368
|
+
ports=[],
|
|
369
|
+
schemes=["https"],
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
@pytest.mark.asyncio
|
|
374
|
+
async def test_relative_redirect_keeps_the_scheme_of_its_base() -> None:
|
|
375
|
+
async def handle_start(_req: web.Request) -> web.Response:
|
|
376
|
+
return web.Response(status=302, headers={"Location": "/final"})
|
|
377
|
+
|
|
378
|
+
async def handle_final(_req: web.Request) -> web.Response:
|
|
379
|
+
return web.Response(status=200, text="ok")
|
|
380
|
+
|
|
381
|
+
server = await _build_server({"/start": handle_start, "/final": handle_final})
|
|
382
|
+
try:
|
|
383
|
+
response = await _permissive_request(
|
|
384
|
+
_make_server_url(server, "/start"),
|
|
385
|
+
allow_redirects=True,
|
|
386
|
+
schemes=["http"],
|
|
387
|
+
)
|
|
388
|
+
assert response.status == 200
|
|
389
|
+
assert await response.text() == "ok"
|
|
390
|
+
finally:
|
|
391
|
+
await server.close()
|
|
392
|
+
|
|
393
|
+
@pytest.mark.asyncio
|
|
394
|
+
@pytest.mark.parametrize("compressed", [False, True])
|
|
395
|
+
async def test_cap_admits_a_body_of_exactly_the_limit(compressed: bool) -> None:
|
|
396
|
+
body = _gzip_bomb(_CAP) if compressed else b"a" * _CAP
|
|
397
|
+
headers = {"Content-Encoding": "gzip"} if compressed else {}
|
|
398
|
+
server = await _build_body_server(body, headers)
|
|
399
|
+
try:
|
|
400
|
+
response = await _capped_request(
|
|
401
|
+
_make_server_url(server, "/body"), max_response_bytes=_CAP
|
|
402
|
+
)
|
|
403
|
+
assert len(await response.text()) == _CAP
|
|
404
|
+
finally:
|
|
405
|
+
await server.close()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
@pytest.mark.asyncio
|
|
409
|
+
async def test_cap_bounds_each_hop_rather_than_the_whole_chain() -> None:
|
|
410
|
+
# The accepted multiplier: every hop may serve just under the cap, so a
|
|
411
|
+
# full chain moves _MAX_HOPS times the limit and still succeeds.
|
|
412
|
+
body = b"a" * (_CAP - 1)
|
|
413
|
+
|
|
414
|
+
async def handle_hop(req: web.Request) -> web.Response:
|
|
415
|
+
hop = int(req.query.get("n", "0"))
|
|
416
|
+
if hop >= _MAX_HOPS - 1:
|
|
417
|
+
return web.Response(body=body)
|
|
418
|
+
return web.Response(
|
|
419
|
+
status=302, body=body, headers={"Location": f"/hop?n={hop + 1}"}
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
server = await _build_server({"/hop": handle_hop})
|
|
423
|
+
try:
|
|
424
|
+
response = await _capped_request(
|
|
425
|
+
_make_server_url(server, "/hop?n=0"),
|
|
426
|
+
max_response_bytes=_CAP,
|
|
427
|
+
allow_redirects=True,
|
|
428
|
+
)
|
|
429
|
+
assert response.status == 200
|
|
430
|
+
assert len(await response.text()) == _CAP - 1
|
|
431
|
+
finally:
|
|
432
|
+
await server.close()
|
|
433
|
+
|
|
434
|
+
def test_response_too_large_stays_catchable_as_a_client_error() -> None:
|
|
435
|
+
# Callers degrade gracefully on transport failures; narrowing this base
|
|
436
|
+
# converts their soft fallbacks into client-facing errors, and in one
|
|
437
|
+
# fire-and-forget path drops the log entirely.
|
|
438
|
+
assert issubclass(ResponseTooLargeError, aiohttp.ClientError)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@pytest.mark.asyncio
|
|
442
|
+
async def test_concurrent_requests_do_not_share_a_cap() -> None:
|
|
443
|
+
# The cap reaches the response through context, so overlapping requests
|
|
444
|
+
# must not observe each other's limit.
|
|
445
|
+
server = await _build_body_server(
|
|
446
|
+
_gzip_bomb(4 * 1024 * 1024), {"Content-Encoding": "gzip"}
|
|
447
|
+
)
|
|
448
|
+
url = _make_server_url(server, "/body")
|
|
449
|
+
try:
|
|
450
|
+
tight, generous = await asyncio.gather(
|
|
451
|
+
_capped_request(url, max_response_bytes=1024 * 1024),
|
|
452
|
+
_capped_request(url, max_response_bytes=32 * 1024 * 1024),
|
|
453
|
+
return_exceptions=True,
|
|
454
|
+
)
|
|
455
|
+
assert isinstance(tight, ResponseTooLargeError)
|
|
456
|
+
assert not isinstance(generous, BaseException)
|
|
457
|
+
assert len(await generous.text()) == 4 * 1024 * 1024
|
|
458
|
+
finally:
|
|
459
|
+
await server.close()
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
{ src, workspace_root }:
|
|
2
|
-
{
|
|
3
|
-
inherit src workspace_root;
|
|
4
|
-
root_path = "develops/fluidattacks-core/python/http";
|
|
5
|
-
module_name = "fluidattacks_core";
|
|
6
|
-
pypi_token = {
|
|
7
|
-
method = "oidc";
|
|
8
|
-
};
|
|
9
|
-
override = {
|
|
10
|
-
checks = {
|
|
11
|
-
basedpyright = false;
|
|
12
|
-
};
|
|
13
|
-
configs = {
|
|
14
|
-
import_linter = ../import-linter.cfg;
|
|
15
|
-
mypy = ../mypy.ini;
|
|
16
|
-
ruff = ../ruff.toml;
|
|
17
|
-
};
|
|
18
|
-
};
|
|
19
|
-
}
|
|
@@ -1,303 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"nodes": {
|
|
3
|
-
"flake-parts": {
|
|
4
|
-
"inputs": {
|
|
5
|
-
"nixpkgs-lib": "nixpkgs-lib"
|
|
6
|
-
},
|
|
7
|
-
"locked": {
|
|
8
|
-
"lastModified": 1762810396,
|
|
9
|
-
"narHash": "sha256-dxFVgQPG+R72dkhXTtqUm7KpxElw3u6E+YlQ2WaDgt8=",
|
|
10
|
-
"owner": "hercules-ci",
|
|
11
|
-
"repo": "flake-parts",
|
|
12
|
-
"rev": "0bdadb1b265fb4143a75bd1ec7d8c915898a9923",
|
|
13
|
-
"type": "github"
|
|
14
|
-
},
|
|
15
|
-
"original": {
|
|
16
|
-
"owner": "hercules-ci",
|
|
17
|
-
"repo": "flake-parts",
|
|
18
|
-
"type": "github"
|
|
19
|
-
}
|
|
20
|
-
},
|
|
21
|
-
"nix_filter": {
|
|
22
|
-
"locked": {
|
|
23
|
-
"lastModified": 1757882181,
|
|
24
|
-
"narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=",
|
|
25
|
-
"owner": "numtide",
|
|
26
|
-
"repo": "nix-filter",
|
|
27
|
-
"rev": "59c44d1909c72441144b93cf0f054be7fe764de5",
|
|
28
|
-
"type": "github"
|
|
29
|
-
},
|
|
30
|
-
"original": {
|
|
31
|
-
"owner": "numtide",
|
|
32
|
-
"repo": "nix-filter",
|
|
33
|
-
"type": "github"
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
"nixpkgs": {
|
|
37
|
-
"locked": {
|
|
38
|
-
"lastModified": 1758690382,
|
|
39
|
-
"narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
|
|
40
|
-
"owner": "nixos",
|
|
41
|
-
"repo": "nixpkgs",
|
|
42
|
-
"rev": "e643668fd71b949c53f8626614b21ff71a07379d",
|
|
43
|
-
"type": "github"
|
|
44
|
-
},
|
|
45
|
-
"original": {
|
|
46
|
-
"owner": "nixos",
|
|
47
|
-
"ref": "nixos-unstable",
|
|
48
|
-
"repo": "nixpkgs",
|
|
49
|
-
"type": "github"
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
"nixpkgs-lib": {
|
|
53
|
-
"locked": {
|
|
54
|
-
"lastModified": 1761765539,
|
|
55
|
-
"narHash": "sha256-b0yj6kfvO8ApcSE+QmA6mUfu8IYG6/uU28OFn4PaC8M=",
|
|
56
|
-
"owner": "nix-community",
|
|
57
|
-
"repo": "nixpkgs.lib",
|
|
58
|
-
"rev": "719359f4562934ae99f5443f20aa06c2ffff91fc",
|
|
59
|
-
"type": "github"
|
|
60
|
-
},
|
|
61
|
-
"original": {
|
|
62
|
-
"owner": "nix-community",
|
|
63
|
-
"repo": "nixpkgs.lib",
|
|
64
|
-
"type": "github"
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
"nixpkgs_2": {
|
|
68
|
-
"locked": {
|
|
69
|
-
"lastModified": 1758690382,
|
|
70
|
-
"narHash": "sha256-NY3kSorgqE5LMm1LqNwGne3ZLMF2/ILgLpFr1fS4X3o=",
|
|
71
|
-
"owner": "nixos",
|
|
72
|
-
"repo": "nixpkgs",
|
|
73
|
-
"rev": "e643668fd71b949c53f8626614b21ff71a07379d",
|
|
74
|
-
"type": "github"
|
|
75
|
-
},
|
|
76
|
-
"original": {
|
|
77
|
-
"owner": "nixos",
|
|
78
|
-
"ref": "nixos-unstable",
|
|
79
|
-
"repo": "nixpkgs",
|
|
80
|
-
"type": "github"
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
"nixpkgs_3": {
|
|
84
|
-
"locked": {
|
|
85
|
-
"lastModified": 1761373498,
|
|
86
|
-
"narHash": "sha256-Q/uhWNvd7V7k1H1ZPMy/vkx3F8C13ZcdrKjO7Jv7v0c=",
|
|
87
|
-
"owner": "nixos",
|
|
88
|
-
"repo": "nixpkgs",
|
|
89
|
-
"rev": "6a08e6bb4e46ff7fcbb53d409b253f6bad8a28ce",
|
|
90
|
-
"type": "github"
|
|
91
|
-
},
|
|
92
|
-
"original": {
|
|
93
|
-
"owner": "nixos",
|
|
94
|
-
"ref": "nixos-unstable",
|
|
95
|
-
"repo": "nixpkgs",
|
|
96
|
-
"type": "github"
|
|
97
|
-
}
|
|
98
|
-
},
|
|
99
|
-
"nixpkgs_flake": {
|
|
100
|
-
"locked": {
|
|
101
|
-
"lastModified": 1778253243,
|
|
102
|
-
"narHash": "sha256-KkH2QvU6iRLDTGGEwH5jnMqHWj4zMTuHxi1S1zdr01w=",
|
|
103
|
-
"owner": "nixos",
|
|
104
|
-
"repo": "nixpkgs",
|
|
105
|
-
"rev": "4fcda7103530b23c5ca44286344912082aad4220",
|
|
106
|
-
"type": "github"
|
|
107
|
-
},
|
|
108
|
-
"original": {
|
|
109
|
-
"owner": "nixos",
|
|
110
|
-
"repo": "nixpkgs",
|
|
111
|
-
"type": "github"
|
|
112
|
-
}
|
|
113
|
-
},
|
|
114
|
-
"pyproject-build-systems": {
|
|
115
|
-
"inputs": {
|
|
116
|
-
"nixpkgs": "nixpkgs",
|
|
117
|
-
"pyproject-nix": "pyproject-nix",
|
|
118
|
-
"uv2nix": "uv2nix"
|
|
119
|
-
},
|
|
120
|
-
"locked": {
|
|
121
|
-
"lastModified": 1761781027,
|
|
122
|
-
"narHash": "sha256-YDvxPAm2WnxrznRqWwHLjryBGG5Ey1ATEJXrON+TWt8=",
|
|
123
|
-
"owner": "pyproject-nix",
|
|
124
|
-
"repo": "build-system-pkgs",
|
|
125
|
-
"rev": "795a980d25301e5133eca37adae37283ec3c8e66",
|
|
126
|
-
"type": "github"
|
|
127
|
-
},
|
|
128
|
-
"original": {
|
|
129
|
-
"owner": "pyproject-nix",
|
|
130
|
-
"repo": "build-system-pkgs",
|
|
131
|
-
"type": "github"
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
"pyproject-nix": {
|
|
135
|
-
"inputs": {
|
|
136
|
-
"nixpkgs": [
|
|
137
|
-
"python_builder",
|
|
138
|
-
"pyproject-build-systems",
|
|
139
|
-
"nixpkgs"
|
|
140
|
-
]
|
|
141
|
-
},
|
|
142
|
-
"locked": {
|
|
143
|
-
"lastModified": 1758265079,
|
|
144
|
-
"narHash": "sha256-amLaLNwKSZPShQHzfgmc/9o76dU8xzN0743dWgvYlr8=",
|
|
145
|
-
"owner": "nix-community",
|
|
146
|
-
"repo": "pyproject.nix",
|
|
147
|
-
"rev": "02e9418fd4af638447dca4b17b1280da95527fc9",
|
|
148
|
-
"type": "github"
|
|
149
|
-
},
|
|
150
|
-
"original": {
|
|
151
|
-
"owner": "nix-community",
|
|
152
|
-
"repo": "pyproject.nix",
|
|
153
|
-
"type": "github"
|
|
154
|
-
}
|
|
155
|
-
},
|
|
156
|
-
"pyproject-nix_2": {
|
|
157
|
-
"inputs": {
|
|
158
|
-
"nixpkgs": "nixpkgs_2"
|
|
159
|
-
},
|
|
160
|
-
"locked": {
|
|
161
|
-
"lastModified": 1762427963,
|
|
162
|
-
"narHash": "sha256-CkPlAbIQ87wmjy5qHibfzk4DmMGBNqFer+lLfXjpP5M=",
|
|
163
|
-
"owner": "pyproject-nix",
|
|
164
|
-
"repo": "pyproject.nix",
|
|
165
|
-
"rev": "4540ea004e04fcd12dd2738d51383d10f956f7b9",
|
|
166
|
-
"type": "github"
|
|
167
|
-
},
|
|
168
|
-
"original": {
|
|
169
|
-
"owner": "pyproject-nix",
|
|
170
|
-
"repo": "pyproject.nix",
|
|
171
|
-
"type": "github"
|
|
172
|
-
}
|
|
173
|
-
},
|
|
174
|
-
"pyproject-nix_3": {
|
|
175
|
-
"inputs": {
|
|
176
|
-
"nixpkgs": [
|
|
177
|
-
"python_builder",
|
|
178
|
-
"uv2nix",
|
|
179
|
-
"nixpkgs"
|
|
180
|
-
]
|
|
181
|
-
},
|
|
182
|
-
"locked": {
|
|
183
|
-
"lastModified": 1760402624,
|
|
184
|
-
"narHash": "sha256-jF6UKLs2uGc2rtved8Vrt58oTWjTQoAssuYs/0578Z4=",
|
|
185
|
-
"owner": "pyproject-nix",
|
|
186
|
-
"repo": "pyproject.nix",
|
|
187
|
-
"rev": "84c4ea102127c77058ea1ed7be7300261fafc7d2",
|
|
188
|
-
"type": "github"
|
|
189
|
-
},
|
|
190
|
-
"original": {
|
|
191
|
-
"owner": "pyproject-nix",
|
|
192
|
-
"repo": "pyproject.nix",
|
|
193
|
-
"type": "github"
|
|
194
|
-
}
|
|
195
|
-
},
|
|
196
|
-
"python_builder": {
|
|
197
|
-
"inputs": {
|
|
198
|
-
"nix_filter": "nix_filter",
|
|
199
|
-
"nixpkgs_flake": "nixpkgs_flake",
|
|
200
|
-
"pyproject-build-systems": "pyproject-build-systems",
|
|
201
|
-
"pyproject-nix": "pyproject-nix_2",
|
|
202
|
-
"shell-helpers": "shell-helpers",
|
|
203
|
-
"uv2nix": "uv2nix_2"
|
|
204
|
-
},
|
|
205
|
-
"locked": {
|
|
206
|
-
"dir": "common/utils/python-builder",
|
|
207
|
-
"lastModified": 1787078362,
|
|
208
|
-
"narHash": "sha256-HYL7Rn6SuNjYxkt/PwFLksZjsauCiY+ENbOWryLsIlw=",
|
|
209
|
-
"ref": "refs/heads/trunk",
|
|
210
|
-
"rev": "09fd25a13309eb5e3b5b2a061bb340df3ae5d281",
|
|
211
|
-
"shallow": true,
|
|
212
|
-
"type": "git",
|
|
213
|
-
"url": "ssh://git@gitlab.com/fluidattacks/universe"
|
|
214
|
-
},
|
|
215
|
-
"original": {
|
|
216
|
-
"dir": "common/utils/python-builder",
|
|
217
|
-
"rev": "09fd25a13309eb5e3b5b2a061bb340df3ae5d281",
|
|
218
|
-
"shallow": true,
|
|
219
|
-
"type": "git",
|
|
220
|
-
"url": "ssh://git@gitlab.com/fluidattacks/universe"
|
|
221
|
-
}
|
|
222
|
-
},
|
|
223
|
-
"root": {
|
|
224
|
-
"inputs": {
|
|
225
|
-
"python_builder": "python_builder"
|
|
226
|
-
}
|
|
227
|
-
},
|
|
228
|
-
"shell-helpers": {
|
|
229
|
-
"inputs": {
|
|
230
|
-
"flake-parts": "flake-parts",
|
|
231
|
-
"nixpkgs": [
|
|
232
|
-
"python_builder",
|
|
233
|
-
"nixpkgs_flake"
|
|
234
|
-
]
|
|
235
|
-
},
|
|
236
|
-
"locked": {
|
|
237
|
-
"dir": "common/utils/shell-helpers",
|
|
238
|
-
"lastModified": 1762806066,
|
|
239
|
-
"narHash": "sha256-Wz99Fl7SpPU1NkTHJ36XOo6DjOEAMXCw3ThRABOS2Hs=",
|
|
240
|
-
"ref": "refs/heads/trunk",
|
|
241
|
-
"rev": "d73cfdc7ae9e7f547e22a4481d4c2ccce38b26a8",
|
|
242
|
-
"shallow": true,
|
|
243
|
-
"type": "git",
|
|
244
|
-
"url": "ssh://git@gitlab.com/fluidattacks/universe"
|
|
245
|
-
},
|
|
246
|
-
"original": {
|
|
247
|
-
"dir": "common/utils/shell-helpers",
|
|
248
|
-
"rev": "d73cfdc7ae9e7f547e22a4481d4c2ccce38b26a8",
|
|
249
|
-
"shallow": true,
|
|
250
|
-
"type": "git",
|
|
251
|
-
"url": "ssh://git@gitlab.com/fluidattacks/universe"
|
|
252
|
-
}
|
|
253
|
-
},
|
|
254
|
-
"uv2nix": {
|
|
255
|
-
"inputs": {
|
|
256
|
-
"nixpkgs": [
|
|
257
|
-
"python_builder",
|
|
258
|
-
"pyproject-build-systems",
|
|
259
|
-
"nixpkgs"
|
|
260
|
-
],
|
|
261
|
-
"pyproject-nix": [
|
|
262
|
-
"python_builder",
|
|
263
|
-
"pyproject-build-systems",
|
|
264
|
-
"pyproject-nix"
|
|
265
|
-
]
|
|
266
|
-
},
|
|
267
|
-
"locked": {
|
|
268
|
-
"lastModified": 1758933732,
|
|
269
|
-
"narHash": "sha256-HAmm1GBS1myZCFuog0DC2ZLaynvZtiUI2Crmo+cdQI0=",
|
|
270
|
-
"owner": "pyproject-nix",
|
|
271
|
-
"repo": "uv2nix",
|
|
272
|
-
"rev": "273ce18f913d8559e0d04f820d724308966d7c4d",
|
|
273
|
-
"type": "github"
|
|
274
|
-
},
|
|
275
|
-
"original": {
|
|
276
|
-
"owner": "pyproject-nix",
|
|
277
|
-
"repo": "uv2nix",
|
|
278
|
-
"type": "github"
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
"uv2nix_2": {
|
|
282
|
-
"inputs": {
|
|
283
|
-
"nixpkgs": "nixpkgs_3",
|
|
284
|
-
"pyproject-nix": "pyproject-nix_3"
|
|
285
|
-
},
|
|
286
|
-
"locked": {
|
|
287
|
-
"lastModified": 1761872265,
|
|
288
|
-
"narHash": "sha256-i25GRgp2vUOebY70L3NTAgkd+Pr1hnn5xM3qHxH0ONU=",
|
|
289
|
-
"owner": "pyproject-nix",
|
|
290
|
-
"repo": "uv2nix",
|
|
291
|
-
"rev": "74dfb62871be152ad3673b143b0cc56105a4f3c5",
|
|
292
|
-
"type": "github"
|
|
293
|
-
},
|
|
294
|
-
"original": {
|
|
295
|
-
"owner": "pyproject-nix",
|
|
296
|
-
"repo": "uv2nix",
|
|
297
|
-
"type": "github"
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
},
|
|
301
|
-
"root": "root",
|
|
302
|
-
"version": 7
|
|
303
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
description = "Fluid Attacks Core http package.";
|
|
3
|
-
|
|
4
|
-
inputs = {
|
|
5
|
-
python_builder = {
|
|
6
|
-
url = "git+ssh://git@gitlab.com/fluidattacks/universe?shallow=1&rev=09fd25a13309eb5e3b5b2a061bb340df3ae5d281&dir=common/utils/python-builder";
|
|
7
|
-
};
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
outputs =
|
|
11
|
-
{ self, ... }@inputs:
|
|
12
|
-
let
|
|
13
|
-
build_args =
|
|
14
|
-
{
|
|
15
|
-
system,
|
|
16
|
-
python_version,
|
|
17
|
-
nixpkgs,
|
|
18
|
-
builders,
|
|
19
|
-
scripts,
|
|
20
|
-
}:
|
|
21
|
-
import ./build {
|
|
22
|
-
src = import ./build/filter.nix nixpkgs.nix-filter self;
|
|
23
|
-
workspace_root = ./.;
|
|
24
|
-
};
|
|
25
|
-
in
|
|
26
|
-
{
|
|
27
|
-
packages = inputs.python_builder.outputs.build build_args;
|
|
28
|
-
};
|
|
29
|
-
}
|
|
File without changes
|
|
File without changes
|
{fluidattacks_core_http-12.1.0 → fluidattacks_core_http-12.2.0}/fluidattacks_core/http/py.typed
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|