mirror of
https://github.com/saymrwulf/proof-aware-crypto-tooling-agent.git
synced 2026-09-04 20:03:40 +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
|
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__}"})
|
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:
|
def _route(self) -> None:
|
||||||
parsed = urlparse(self.path)
|
parsed = urlparse(self.path)
|
||||||
path = parsed.path.rstrip("/")
|
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-Disposition", 'inline; filename="ltl.pdf"')
|
||||||
self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Length", str(len(body)))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
self._body(body)
|
||||||
elif route in ("/log-public-key", "/log-slhdsa-public-key"):
|
elif route in ("/log-public-key", "/log-slhdsa-public-key"):
|
||||||
# TOFU mitigation depends on the key being published in two
|
# TOFU mitigation depends on the key being published in two
|
||||||
# independent locations; this is the site's copy (the mirror
|
# 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-Type", "text/plain; charset=utf-8")
|
||||||
self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Length", str(len(body)))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
self._body(body)
|
||||||
elif route == "/openapi.json":
|
elif route == "/openapi.json":
|
||||||
self._send(200, _openapi_document(base))
|
self._send(200, _openapi_document(base))
|
||||||
elif route == "/healthz":
|
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("Content-Length", str(len(body)))
|
||||||
self.send_header("X-Robots-Tag", "noindex, nofollow")
|
self.send_header("X-Robots-Tag", "noindex, nofollow")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
self._body(body)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _send(self, code: int, payload: dict[str, Any], code_if_error: int | None = None) -> None:
|
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("Content-Length", str(len(body)))
|
||||||
self.send_header("Cache-Control", "no-store")
|
self.send_header("Cache-Control", "no-store")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
self._body(body)
|
||||||
|
|
||||||
def _send_html(self, html: str) -> None:
|
def _send_html(self, html: str) -> None:
|
||||||
body = html.encode("utf-8")
|
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-Type", "text/html; charset=utf-8")
|
||||||
self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Length", str(len(body)))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
self._body(body)
|
||||||
|
|
||||||
def log_message(self, fmt: str, *args: Any) -> None: # quiet by default
|
def log_message(self, fmt: str, *args: Any) -> None: # quiet by default
|
||||||
pass
|
pass
|
||||||
|
|
|
||||||
|
|
@ -223,3 +223,33 @@ def test_openapi_document_served_and_valid():
|
||||||
assert doc["openapi"].startswith("3.")
|
assert doc["openapi"].startswith("3.")
|
||||||
assert "/v1/sth" in doc["paths"] and "/log-public-key" in doc["paths"]
|
assert "/v1/sth" in doc["paths"] and "/log-public-key" in doc["paths"]
|
||||||
_json.dumps(doc) # serializable
|
_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