From 920485eb83c5e0f1a7e4a598711132bf1438cb79 Mon Sep 17 00:00:00 2001 From: mrwulf Date: Tue, 7 Jul 2026 14:17:39 +0200 Subject: [PATCH] web: unlisted operator-dropped documents from /site/ Generic mechanism: bare-name PDFs placed in the log directory's site/ folder are served by name, checked LAST in the route chain (can never shadow an API route), traversal-safe, noindex, and deliberately absent from the endpoint index and the docs page - the operator decides who receives a link. Tested: serve, unlisted-in-404, traversal rejected. Co-Authored-By: Claude Fable 5 --- provider/src/pacta_provider/web.py | 28 ++++++++++++++++++++++++++++ tests/test_web_and_witness.py | 20 ++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/provider/src/pacta_provider/web.py b/provider/src/pacta_provider/web.py index 0c134ec..7a2d47b 100644 --- a/provider/src/pacta_provider/web.py +++ b/provider/src/pacta_provider/web.py @@ -132,6 +132,8 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf for entry in entries ] }) + elif self._serve_site_document(route, log): + pass # operator-dropped document; checked LAST so it can never shadow an API route else: self._send(404, { "error": "unknown endpoint", @@ -150,6 +152,32 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf ], }) + def _serve_site_document(self, route: str, log_obj: TransparencyLog) -> bool: + """Serve operator-dropped PDFs from ``/site/`` by bare name. + + Deliberately UNLISTED: these documents do not appear in the + endpoint index or anywhere on the docs page - the operator + decides who receives a link. The name must be a single plain + path segment (no traversal); only ``.pdf`` payloads are served. + Returns True when it handled the request. + """ + name = route.lstrip("/") + if not name or not name.replace("-", "").replace("_", "").isalnum(): + return False + candidate = (log_obj.log_dir / "site" / f"{name}.pdf").resolve() + site_dir = (log_obj.log_dir / "site").resolve() + if site_dir not in candidate.parents or not candidate.is_file(): + return False + body = candidate.read_bytes() + self.send_response(200) + self.send_header("Content-Type", "application/pdf") + self.send_header("Content-Disposition", f'inline; filename="{name}.pdf"') + self.send_header("Content-Length", str(len(body))) + self.send_header("X-Robots-Tag", "noindex, nofollow") + self.end_headers() + self.wfile.write(body) + return True + def _send(self, code: int, payload: dict[str, Any], code_if_error: int | None = None) -> None: if code_if_error and "error" in payload: code = code_if_error diff --git a/tests/test_web_and_witness.py b/tests/test_web_and_witness.py index 298629d..ee82568 100644 --- a/tests/test_web_and_witness.py +++ b/tests/test_web_and_witness.py @@ -71,6 +71,26 @@ def test_web_endpoints_and_online_proof_roundtrip(tmp_path): page = r.read().decode() assert "BEGIN PUBLIC KEY" in page assert "pin this key" in page.lower() + # operator-dropped documents: served by bare name, absent from the + # endpoint index, traversal-safe + site = tmp_path / "log" / "site" + site.mkdir() + (site / "extra.pdf").write_bytes(b"%PDF-1.4 dummy") + with urllib.request.urlopen(base + "/extra", timeout=10) as r: + assert r.read().startswith(b"%PDF-") + assert r.headers["X-Robots-Tag"].startswith("noindex") + try: + urllib.request.urlopen(base + "/nope-not-there", timeout=10) + raise AssertionError("expected 404") + except urllib.error.HTTPError as exc: + listing = json.loads(exc.read()) + assert exc.code == 404 + assert not any("extra" in e for e in listing["endpoints"]) # unlisted + try: + urllib.request.urlopen(base + "/..%2fsth", timeout=10) + raise AssertionError("expected 404") + except urllib.error.HTTPError as exc: + assert exc.code == 404 finally: server.shutdown()