withcache 0.8.5__tar.gz → 0.8.6__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.
- {withcache-0.8.5 → withcache-0.8.6}/PKG-INFO +1 -1
- {withcache-0.8.5 → withcache-0.8.6}/deploy/Containerfile +9 -1
- {withcache-0.8.5 → withcache-0.8.6}/shim/build.zig.zon +1 -1
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/__init__.py +1 -1
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/server.py +22 -3
- {withcache-0.8.5 → withcache-0.8.6}/tests/test_withcache.py +35 -0
- {withcache-0.8.5 → withcache-0.8.6}/.gitignore +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/LICENSE +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/README.md +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/deploy/compose.yml +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/hatch_build.py +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/pyproject.toml +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/shim/build.zig +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/shim/shim.zig +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/_shim.py +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/client.py +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/curlwithcache.py +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/oras.py +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/static/bootstrap-icons.min.css +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/static/bootstrap.min.css +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/static/fonts/bootstrap-icons.woff +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/static/fonts/bootstrap-icons.woff2 +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/static/htmx.min.js +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/src/withcache/wgetwithcache.py +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/tests/test_differential.py +0 -0
- {withcache-0.8.5 → withcache-0.8.6}/tests/test_oras.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: withcache
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.6
|
|
4
4
|
Summary: Operator-curated, URL-keyed artifact cache for a small lab (CUDA/ROCm/DOCA/firmware)
|
|
5
5
|
Project-URL: Homepage, https://github.com/safl/withcache
|
|
6
6
|
Author-email: "Simon A. F. Lund" <safl@safl.dk>
|
|
@@ -12,6 +12,14 @@ COPY pyproject.toml README.md hatch_build.py /app/
|
|
|
12
12
|
COPY src /app/src
|
|
13
13
|
RUN pip install --no-cache-dir /app
|
|
14
14
|
|
|
15
|
+
# curl: HEALTHCHECK below. ``-f`` maps any 4xx/5xx to a non-zero
|
|
16
|
+
# exit without dumping a Traceback, matching the nbdmux sibling's
|
|
17
|
+
# Pass 5 pattern; if a future /healthz ever grows a non-200 branch
|
|
18
|
+
# (e.g. cache-disk-full check), no log-noise regression.
|
|
19
|
+
RUN apt-get update \
|
|
20
|
+
&& apt-get install -y --no-install-recommends curl \
|
|
21
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
22
|
+
|
|
15
23
|
# Run as non-root; /data is the persistent volume for blobs + sqlite.
|
|
16
24
|
RUN useradd --create-home --uid 10001 app \
|
|
17
25
|
&& mkdir -p /data && chown app:app /data
|
|
@@ -25,6 +33,6 @@ VOLUME ["/data"]
|
|
|
25
33
|
# with WITHCACHE_SESSION_SECRET.
|
|
26
34
|
|
|
27
35
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \
|
|
28
|
-
CMD
|
|
36
|
+
CMD curl -fsS -o /dev/null --max-time 2 http://127.0.0.1:8081/healthz || exit 1
|
|
29
37
|
|
|
30
38
|
ENTRYPOINT ["withcache-server", "--host", "0.0.0.0", "--port", "8081", "--data-dir", "/data"]
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
.name = .withcache_shim,
|
|
3
3
|
// Zig requires a literal here; keep it in lockstep with the project's
|
|
4
4
|
// single source (src/withcache/__init__.py) via `make bump` / `make version-check`.
|
|
5
|
-
.version = "0.8.
|
|
5
|
+
.version = "0.8.6",
|
|
6
6
|
.fingerprint = 0xd7d96c5ed212ccaa,
|
|
7
7
|
.minimum_zig_version = "0.16.0",
|
|
8
8
|
.paths = .{
|
|
@@ -17,6 +17,6 @@ All modules are stdlib-only and self-contained.
|
|
|
17
17
|
from . import oras
|
|
18
18
|
from .client import blob_url, cache_base, is_cached, serve_url
|
|
19
19
|
|
|
20
|
-
__version__ = "0.8.
|
|
20
|
+
__version__ = "0.8.6"
|
|
21
21
|
|
|
22
22
|
__all__ = ["__version__", "blob_url", "cache_base", "is_cached", "oras", "serve_url"]
|
|
@@ -1087,7 +1087,13 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1087
1087
|
|
|
1088
1088
|
def do_POST(self):
|
|
1089
1089
|
parsed = urllib.parse.urlsplit(self.path)
|
|
1090
|
-
|
|
1090
|
+
try:
|
|
1091
|
+
form = self.read_form()
|
|
1092
|
+
except ValueError:
|
|
1093
|
+
# Duplicate field(s) -- reject with 400 rather than let
|
|
1094
|
+
# a first-value-wins collapse hide a payload silently.
|
|
1095
|
+
self.send_text(400, "malformed form (duplicate fields?)\n")
|
|
1096
|
+
return
|
|
1091
1097
|
if parsed.path == "/ui/login":
|
|
1092
1098
|
self.handle_login_submit(form)
|
|
1093
1099
|
elif parsed.path == "/ui/logout":
|
|
@@ -1208,8 +1214,11 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1208
1214
|
f"{Auth.COOKIE}={self.auth.make_token()}; HttpOnly; "
|
|
1209
1215
|
f"SameSite=Lax; Path=/; Max-Age={Auth.MAX_AGE}"
|
|
1210
1216
|
)
|
|
1211
|
-
|
|
1217
|
+
# Log BEFORE the redirect so the line lands even if the
|
|
1218
|
+
# client drops the connection mid-write, matching the
|
|
1219
|
+
# ordering of the failure branch below.
|
|
1212
1220
|
print(f"{self.address_string()} - login succeeded", flush=True)
|
|
1221
|
+
self.redirect("/ui/cached", set_cookie=cookie)
|
|
1213
1222
|
else:
|
|
1214
1223
|
print(f"{self.address_string()} - login failed", flush=True)
|
|
1215
1224
|
self.send_html(401, self.render_login(error="Invalid password."))
|
|
@@ -1310,9 +1319,19 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|
|
1310
1319
|
|
|
1311
1320
|
# -- helpers -----------------------------------------------------------
|
|
1312
1321
|
def read_form(self) -> dict:
|
|
1322
|
+
"""Parse form-encoded body into ``{key: value}``. Rejects
|
|
1323
|
+
duplicate keys with ValueError -- an operator-facing form
|
|
1324
|
+
never has repeated field names, so ``name=a&name=b`` is
|
|
1325
|
+
either a bug or a client trying to hide a payload from a
|
|
1326
|
+
naive read of the first value. Callers wrap the call and
|
|
1327
|
+
turn a ValueError into a 400."""
|
|
1313
1328
|
length = int(self.headers.get("Content-Length", 0) or 0)
|
|
1314
1329
|
body = self.rfile.read(length).decode("utf-8") if length else ""
|
|
1315
|
-
|
|
1330
|
+
pairs = urllib.parse.parse_qs(body)
|
|
1331
|
+
dups = [k for k, v in pairs.items() if len(v) > 1]
|
|
1332
|
+
if dups:
|
|
1333
|
+
raise ValueError(f"duplicate form field(s): {', '.join(sorted(dups))}")
|
|
1334
|
+
return {k: v[0] for k, v in pairs.items()}
|
|
1316
1335
|
|
|
1317
1336
|
def send_text(self, code: int, text: str):
|
|
1318
1337
|
data = text.encode("utf-8")
|
|
@@ -1316,5 +1316,40 @@ class TestCatalogAdminEndpoints(unittest.TestCase):
|
|
|
1316
1316
|
httpd2.server_close()
|
|
1317
1317
|
|
|
1318
1318
|
|
|
1319
|
+
class TestReadFormDuplicateKeys(unittest.TestCase):
|
|
1320
|
+
"""read_form rejects duplicate form keys so a first-value-wins
|
|
1321
|
+
collapse can't hide a payload silently -- mirrors nbdmux's
|
|
1322
|
+
Pass 6 hardening for cross-project parity."""
|
|
1323
|
+
|
|
1324
|
+
def setUp(self):
|
|
1325
|
+
self.httpd, self.store = _start_withcache()
|
|
1326
|
+
self.host = "127.0.0.1"
|
|
1327
|
+
self.port = self.httpd.server_address[1]
|
|
1328
|
+
|
|
1329
|
+
def tearDown(self):
|
|
1330
|
+
self.httpd.shutdown()
|
|
1331
|
+
self.httpd.server_close()
|
|
1332
|
+
|
|
1333
|
+
def test_duplicate_form_field_returns_400(self):
|
|
1334
|
+
import http.client
|
|
1335
|
+
|
|
1336
|
+
# Craft a raw body with duplicate ``url`` fields; urlencode +
|
|
1337
|
+
# dict argument would collapse them client-side.
|
|
1338
|
+
body = "url=https%3A%2F%2Fx%2Fa&url=https%3A%2F%2Fx%2Fb"
|
|
1339
|
+
conn = http.client.HTTPConnection(self.host, self.port)
|
|
1340
|
+
try:
|
|
1341
|
+
conn.request(
|
|
1342
|
+
"POST",
|
|
1343
|
+
"/admin/fetch",
|
|
1344
|
+
body=body,
|
|
1345
|
+
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
|
1346
|
+
)
|
|
1347
|
+
resp = conn.getresponse()
|
|
1348
|
+
self.assertEqual(resp.status, 400)
|
|
1349
|
+
resp.read()
|
|
1350
|
+
finally:
|
|
1351
|
+
conn.close()
|
|
1352
|
+
|
|
1353
|
+
|
|
1319
1354
|
if __name__ == "__main__":
|
|
1320
1355
|
unittest.main(verbosity=2)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|