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]>
82 lines
3.2 KiB
Python
Executable file
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:]))
|