mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-03 19:53:43 +00:00
web: HEAD support — link checkers and unfurlers get 200+headers, not 501
Found while verifying what /paper serves: the stdlib handler only implemented do_GET, so every HEAD probe (mail clients, chat unfurlers, link checkers — exactly the tools that touch the links we mail around) got 501. do_HEAD now routes like GET with the body suppressed; all five body writes go through one guard; regression test asserts HEAD returns 200, correct Content-Type, nonzero Content-Length, empty body.
This commit is contained in:
parent
03de38eaac
commit
57ac2095c0
2 changed files with 45 additions and 5 deletions
|
|
@ -67,6 +67,16 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
|
|||
except Exception as exc: # noqa: BLE001 - the service must not die on a bad request
|
||||
self._send(500, {"error": f"internal error: {type(exc).__name__}"})
|
||||
|
||||
def do_HEAD(self) -> None: # noqa: N802 - link checkers and mail/chat
|
||||
# unfurlers probe with HEAD; answer with the same headers as GET
|
||||
# and no body (a 501 here makes every link look broken to them).
|
||||
self._head_only = True
|
||||
self.do_GET()
|
||||
|
||||
def _body(self, body: bytes) -> None:
|
||||
if not getattr(self, "_head_only", False):
|
||||
self.wfile.write(body)
|
||||
|
||||
def _route(self) -> None:
|
||||
parsed = urlparse(self.path)
|
||||
path = parsed.path.rstrip("/")
|
||||
|
|
@ -91,7 +101,7 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
|
|||
self.send_header("Content-Disposition", 'inline; filename="ltl.pdf"')
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
self._body(body)
|
||||
elif route in ("/log-public-key", "/log-slhdsa-public-key"):
|
||||
# TOFU mitigation depends on the key being published in two
|
||||
# independent locations; this is the site's copy (the mirror
|
||||
|
|
@ -110,7 +120,7 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
|
|||
self.send_header("Content-Type", "text/plain; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
self._body(body)
|
||||
elif route == "/openapi.json":
|
||||
self._send(200, _openapi_document(base))
|
||||
elif route == "/healthz":
|
||||
|
|
@ -222,7 +232,7 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
|
|||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("X-Robots-Tag", "noindex, nofollow")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
self._body(body)
|
||||
return True
|
||||
|
||||
def _send(self, code: int, payload: dict[str, Any], code_if_error: int | None = None) -> None:
|
||||
|
|
@ -234,7 +244,7 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
|
|||
self.send_header("Content-Length", str(len(body)))
|
||||
self.send_header("Cache-Control", "no-store")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
self._body(body)
|
||||
|
||||
def _send_html(self, html: str) -> None:
|
||||
body = html.encode("utf-8")
|
||||
|
|
@ -242,7 +252,7 @@ def make_handler(log: TransparencyLog, base_path: str, docs_html: str, paper_pdf
|
|||
self.send_header("Content-Type", "text/html; charset=utf-8")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
self._body(body)
|
||||
|
||||
def log_message(self, fmt: str, *args: Any) -> None: # quiet by default
|
||||
pass
|
||||
|
|
|
|||
|
|
@ -223,3 +223,33 @@ def test_openapi_document_served_and_valid():
|
|||
assert doc["openapi"].startswith("3.")
|
||||
assert "/v1/sth" in doc["paths"] and "/log-public-key" in doc["paths"]
|
||||
_json.dumps(doc) # serializable
|
||||
|
||||
|
||||
def test_head_requests_answer_like_get_without_body(tmp_path):
|
||||
# Link checkers and mail/chat unfurlers probe with HEAD; a 501 made
|
||||
# /paper look broken to them (found 2026-08-22 while verifying what
|
||||
# the paper link serves).
|
||||
import http.client
|
||||
import shutil
|
||||
|
||||
_make_log(tmp_path)
|
||||
shutil.copy2(tmp_path / "k.pub", tmp_path / "log" / "provider.ed25519.pub")
|
||||
server = serve(str(tmp_path / "log"), port=0)
|
||||
port = server.server_address[1]
|
||||
import threading
|
||||
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
|
||||
for route, ctype in [("/", "text/html"), ("/v1/sth", "application/json"),
|
||||
("/log-public-key", "text/plain")]:
|
||||
conn.request("HEAD", route)
|
||||
r = conn.getresponse()
|
||||
body = r.read()
|
||||
assert r.status == 200, (route, r.status)
|
||||
assert ctype in r.getheader("Content-Type", ""), route
|
||||
assert body == b"", (route, len(body))
|
||||
assert int(r.getheader("Content-Length", "0")) > 0, route
|
||||
finally:
|
||||
server.shutdown()
|
||||
|
|
|
|||
Loading…
Reference in a new issue