diff --git a/openfut-blaze-host/route-ab.py b/openfut-blaze-host/route-ab.py new file mode 100755 index 0000000..9583a4d --- /dev/null +++ b/openfut-blaze-host/route-ab.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +"""Send the routes a real FIFA session used to BOTH backends and diff the replies. + + ./route-ab.py [python-host:port] [rust-host:port] + +WHY + +The gate 5-6 live session exercised 14 RPCs the recorded fixtures never covered +-- Stats, Clubs, OSDKSettings, SponsoredEvents, Messaging::fetchMessages, +Util::getTelemetryServer, the transport PING -- and every one took the +empty-reply fallback. That Python does the same was an INFERENCE from reading +its dispatch table. This turns it into evidence, using the routes FIFA actually +sent. + +None of these RPCs read their request body (they are unhandled on both sides), +so sending them with an empty payload exercises the same path the client did. + +Read-only: opens a client connection to each backend and sends requests. All +routes here are unimplemented on both sides, so nothing mutates. +""" +import socket, struct, sys + +# (component, command, msg_type) observed in the live FIFA session. +MESSAGE, PING = 0, 4 +ROUTES = [ + (0x0009, 0x0005, MESSAGE, "Util::getTelemetryServer"), + (0x0009, 0x000C, MESSAGE, "Util::userSettingsLoadAll"), + (0x0009, 0x001C, MESSAGE, "Util::setClientState"), + (0x7802, 0x0008, MESSAGE, "UserSessions::cmd:0x0008"), + (0x000F, 0x0002, MESSAGE, "Messaging::fetchMessages"), + (0x000B, 0x0640, MESSAGE, "Clubs::cmd:0x0640"), + (0x000B, 0x0A28, MESSAGE, "Clubs::cmd:0x0a28"), + (0x08C9, 0x0001, MESSAGE, "OSDKSettings::cmd:0x0001"), + (0x08C9, 0x0002, MESSAGE, "OSDKSettings::cmd:0x0002"), + (0x0007, 0x0003, MESSAGE, "Stats::cmd:0x0003"), + (0x0007, 0x000F, MESSAGE, "Stats::cmd:0x000f"), + (0x0007, 0x0014, MESSAGE, "Stats::cmd:0x0014"), + (0x081C, 0x0003, MESSAGE, "SponsoredEvents::cmd:0x0003"), + (0x0000, 0x0000, PING, "transport PING"), +] + +def frame(comp, cmd, num, mtype, payload=b""): + h = bytearray(16) + struct.pack_into(">I", h, 0, len(payload)) + struct.pack_into(">H", h, 4, 0) + struct.pack_into(">H", h, 6, comp) + struct.pack_into(">H", h, 8, cmd) + h[10], h[11], h[12] = (num >> 16) & 0xFF, (num >> 8) & 0xFF, num & 0xFF + h[13] = (mtype & 7) << 5 + return bytes(h) + payload + +def ask(host, port, routes): + s = socket.create_connection((host, port), timeout=10) + s.settimeout(10) + out, buf = [], b"" + for i, (comp, cmd, mtype, name) in enumerate(routes): + s.sendall(frame(comp, cmd, i + 1, mtype)) + while len(buf) < 16: + d = s.recv(65536) + if not d: + out.append((name, "NO REPLY")); s.close(); return out + buf += d + total = 16 + struct.unpack_from(">H", buf, 4)[0] + struct.unpack_from(">I", buf, 0)[0] + while len(buf) < total: + buf += s.recv(65536) + out.append((name, buf[:total].hex())) + buf = buf[total:] + s.close() + return out + +def endpoint(arg, default_port): + if ":" in arg: + h, p = arg.rsplit(":", 1) + return h, int(p) + return arg, default_port + +args = sys.argv[1:] +PY_HOST, PY_PORT = endpoint(args[0], 42130) if len(args) > 0 else ("127.0.0.1", 42130) +RS_HOST, RS_PORT = endpoint(args[1], 42230) if len(args) > 1 else ("127.0.0.1", 42230) + +print("python %s:%d rust %s:%d" % (PY_HOST, PY_PORT, RS_HOST, RS_PORT)) +py = ask(PY_HOST, PY_PORT, ROUTES) +rs = ask(RS_HOST, RS_PORT, ROUTES) + +bad = 0 +for (n1, a), (n2, b) in zip(py, rs): + same = (a == b) + if not same: + bad += 1 + print(" %-32s %s" % (n1, "identical" if same else "DIFFERS\n py=%s\n rs=%s" % (a, b))) +print("\n%d/%d identical" % (len(ROUTES) - bad, len(ROUTES))) +sys.exit(1 if bad else 0)