open-standards/wai/cache-conformance/mutators.py
Transaction Science 39d9894e73 wai: cache conformance (public part), the wai-http workflow, and the CI wiring
W3-7, public part. cache-conformance/: tests.json (C1–C13, R1–R5 as MUST
tests of §9 and the sidecar rules, S1–S2 informative), driver.py (stages the
corpus behind an origin and runs the tests against a cache; standard library
and _blake3.py only), mutators.py (flip, gzip, truncate, vary: one §9 defect
each), run.sh (the reference origin must pass every MUST test; each control
must be caught by the tests named for it; labelled caches run where an
environment provides /opt/wai-cache, and the summary says when none did) and
a README. Reports name a cache by label, package version and configuration
digest only. The private harness image is not in this repository.

The reference origin gains what the tests need of it: If-Range by strong
comparison, a live-edge prefix served with a shorter max-age, §8.4's
Cache-Control for the key log and grants, and a request counter
(--count-hits) for the informative tests.

.github/workflows/wai-http.yml: the falsify job (every patch caught, every
Python mutation caught, the differential) and the cache-conformance job
(reports uploaded as an artifact). wai-conformance.yml: `http` joins the
build, the library tests, the corpus loop (wai_http_vectors), the
feature-alone and wasm32 checks; http_verify.py joins the independent
verifiers. wai_http_vectors verify now names a case whose evaluation panics.

http-delivery.md writes byte spans in words where a half-open interval read
as link text to the section-reference check.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-05 08:02:11 -04:00

82 lines
3.2 KiB
Python
Executable file

#!/usr/bin/env python3
"""Negative controls for driver.py: proxies that break http-delivery §9 in one way each,
so a run against them must fail the tests that rule reaches. If the driver passes one of
them, the driver is what is broken.
python3 mutators.py --mode flip|gzip|truncate|vary --listen PORT --upstream URL
- flip: changes one byte in the middle of every object body (§9 rule 1);
- gzip: applies a gzip content coding to whole objects when the request offers it
(§9 rule 1);
- truncate: drops the last byte of every whole object, and says so in Content-Length
(§9 rules 1 and 3);
- vary: adds `Vary: Origin` and drops `Access-Control-Allow-Origin` (§9 rule 4,
§5.3).
Standard library only.
"""
import gzip as gz, http.client, sys, urllib.parse
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
MODE, UPSTREAM = None, None
HOP = {"connection", "keep-alive", "proxy-connection", "transfer-encoding", "upgrade", "te", "trailer", "host"}
class Proxy(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, *a):
pass
def forward(self, method):
u = urllib.parse.urlsplit(UPSTREAM)
conn = http.client.HTTPConnection(u.hostname, u.port or 80, timeout=30)
headers = {k: v for k, v in self.headers.items() if k.lower() not in HOP}
conn.request(method, self.path, headers=headers)
r = conn.getresponse()
body = r.read()
out = [(k, v) for k, v in r.getheaders() if k.lower() not in HOP and k.lower() != "content-length"]
conn.close()
is_object = self.path.split("?")[0].endswith(".wai")
if MODE == "flip" and is_object and len(body) > 1:
body = bytearray(body)
body[len(body) // 2] ^= 0x01
body = bytes(body)
elif MODE == "gzip" and is_object and r.status == 200 and "gzip" in (self.headers.get("Accept-Encoding") or ""):
body = gz.compress(body, mtime=0)
out = [(k, v) for k, v in out if k.lower() != "content-encoding"] + [("Content-Encoding", "gzip")]
elif MODE == "truncate" and is_object and r.status == 200 and body:
body = body[:-1]
elif MODE == "vary":
out = [(k, v) for k, v in out if k.lower() != "access-control-allow-origin"] + [("Vary", "Origin")]
self.send_response(r.status)
for k, v in out:
self.send_header(k, v)
self.send_header("Content-Length", str(len(body)))
self.send_header("Connection", "close")
self.end_headers()
if method != "HEAD":
self.wfile.write(body)
def do_GET(self):
self.forward("GET")
def do_HEAD(self):
self.forward("HEAD")
def do_OPTIONS(self):
self.forward("OPTIONS")
def main(argv):
global MODE, UPSTREAM
opt = lambda f: argv[argv.index(f) + 1]
MODE, UPSTREAM, port = opt("--mode"), opt("--upstream"), int(opt("--listen"))
if MODE not in ("flip", "gzip", "truncate", "vary"):
print(__doc__)
return 2
ThreadingHTTPServer(("127.0.0.1", port), Proxy).serve_forever()
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))