#!/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:]))