b1bc7a764e
Next component in the migration order (Roster -> LSX -> UTAS). Adapter layer
only: no host, no runtime replacement, nothing armed.
The response is shaped as much by http.server.BaseHTTPRequestHandler as by the
oracle's handler code, so it is captured over the wire rather than reasoned
about:
* HTTP/1.0 status line -- protocol_version is left at its default, so the
reply is 1.0 even though the client asks for 1.1
* send_response injects Server: and Date: BEFORE the handler's own headers
* POST answers with headers only: the handler writes the body `if method ==
"GET"`, so a POST advertises Content-Length: 67 and then sends nothing
That last one is preserved, not corrected. It looks like a bug, but "obviously a
bug" has been the wrong call before in this port, and a test now asserts it so a
future cleanup has to argue with something.
Date and Server are volatile and are MASKED in the fixture rather than dropped,
so their presence and position are still asserted. Server is additionally
recorded verbatim: it carries the container's Python version, so a drift away
from roster::ORACLE_SERVER fails a test instead of silently changing every byte
we emit.
generate_roster.py --check FAILS when it cannot reach the oracle rather than
passing, and mutation-testing the mutation harness itself caught two "surviving"
mutations that were really sed no-ops. With application verified, all four
mutations (header order, Content-Length, XML body, Connection) are killed.
124 lines
4.7 KiB
Python
124 lines
4.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Capture the roster oracle's responses byte-for-byte.
|
|
|
|
generate_roster.py [--check] [host:port]
|
|
|
|
Unlike `generate.py`, which imports the Blaze responder and calls its pure
|
|
functions, this captures over the wire. The roster response is shaped as much by
|
|
`http.server.BaseHTTPRequestHandler` as by the handler code -- HTTP/1.0 status
|
|
line, `Server:`/`Date:` injected ahead of the handler's own headers, POST
|
|
answered without a body -- and only the real socket shows all of that.
|
|
|
|
Two fields are volatile and are MASKED rather than recorded:
|
|
|
|
Date: changes every second
|
|
Server: carries the container's Python version
|
|
|
|
They are masked, not dropped, so their presence and position are still asserted.
|
|
The Server string is additionally recorded verbatim under `observed_server`, so
|
|
a drift between the container's Python and the adapter's `ORACLE_SERVER`
|
|
constant is visible rather than silent.
|
|
|
|
`--check` re-captures and compares. If the oracle is unreachable it FAILS rather
|
|
than passing: a check that cannot check must not report success.
|
|
"""
|
|
import json
|
|
import os
|
|
import re
|
|
import socket
|
|
import ssl
|
|
import sys
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
OUT = os.path.join(HERE, "roster.json")
|
|
PATH = "/fifa17/fut/rosterupdate.xml"
|
|
|
|
DATE_RE = re.compile(rb"^Date: .+?\r\n", re.M)
|
|
SERVER_RE = re.compile(rb"^Server: (.+?)\r\n", re.M)
|
|
|
|
|
|
def fetch(host, port, method, body=None):
|
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
|
ctx.check_hostname = False
|
|
ctx.verify_mode = ssl.CERT_NONE
|
|
ctx.set_ciphers("ALL:@SECLEVEL=0")
|
|
s = ctx.wrap_socket(socket.create_connection((host, port), timeout=8),
|
|
server_hostname="fixture")
|
|
req = "%s %s HTTP/1.1\r\nHost: %s:%d\r\nAccept: */*\r\n" % (method, PATH, host, port)
|
|
if body is not None:
|
|
req += "Content-Length: %d\r\n" % len(body)
|
|
req += "\r\n"
|
|
s.sendall(req.encode() + (body or b""))
|
|
out = b""
|
|
while True:
|
|
chunk = s.recv(4096)
|
|
if not chunk:
|
|
break
|
|
out += chunk
|
|
s.close()
|
|
return out
|
|
|
|
|
|
def capture(host, port):
|
|
result = {"path": PATH, "responses": {}}
|
|
servers = set()
|
|
for method, body in (("GET", None), ("HEAD", None), ("POST", b"probe=1")):
|
|
raw = fetch(host, port, method, body)
|
|
m = SERVER_RE.search(raw)
|
|
if m:
|
|
servers.add(m.group(1).decode())
|
|
masked = DATE_RE.sub(b"Date: <MASKED>\r\n", raw)
|
|
masked = SERVER_RE.sub(b"Server: <MASKED>\r\n", masked)
|
|
result["responses"][method] = masked.hex()
|
|
if len(servers) != 1:
|
|
raise SystemExit("oracle returned inconsistent Server headers: %r" % servers)
|
|
result["observed_server"] = servers.pop()
|
|
return result
|
|
|
|
|
|
def main():
|
|
check = "--check" in sys.argv
|
|
args = [a for a in sys.argv[1:] if not a.startswith("--")]
|
|
host, port = (args[0].split(":") if args else ("127.0.0.1", "8081"))[0], \
|
|
int((args[0].split(":")[1] if args and ":" in args[0] else "8081"))
|
|
|
|
try:
|
|
fresh = capture(host, port)
|
|
except Exception as e:
|
|
# Explicitly a failure. A --check that silently passes when it could not
|
|
# reach the oracle is exactly the class of self-confirming tooling this
|
|
# project has been bitten by repeatedly.
|
|
raise SystemExit("cannot reach the roster oracle at %s:%d (%s). "
|
|
"Refusing to report success." % (host, port, e))
|
|
|
|
if check:
|
|
if not os.path.exists(OUT):
|
|
raise SystemExit("no fixture at %s -- run without --check first" % OUT)
|
|
with open(OUT) as f:
|
|
stored = json.load(f)
|
|
if stored.get("responses") != fresh["responses"]:
|
|
for m in sorted(set(stored.get("responses", {})) | set(fresh["responses"])):
|
|
a = stored.get("responses", {}).get(m)
|
|
b = fresh["responses"].get(m)
|
|
if a != b:
|
|
print("MISMATCH %s\n stored: %s\n live : %s" % (m, a, b))
|
|
raise SystemExit("roster fixtures differ from the live oracle")
|
|
if stored.get("observed_server") != fresh["observed_server"]:
|
|
raise SystemExit(
|
|
"the oracle's Server header changed: %r -> %r.\n"
|
|
"Update roster::ORACLE_SERVER and regenerate."
|
|
% (stored.get("observed_server"), fresh["observed_server"]))
|
|
print("roster fixtures match the live oracle (%d responses, server=%r)"
|
|
% (len(fresh["responses"]), fresh["observed_server"]))
|
|
return
|
|
|
|
with open(OUT, "w") as f:
|
|
json.dump(fresh, f, indent=2, sort_keys=True)
|
|
f.write("\n")
|
|
print("wrote %s (%d responses, server=%r)"
|
|
% (OUT, len(fresh["responses"]), fresh["observed_server"]))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|