Commit Graph

10 Commits

Author SHA1 Message Date
funman300 afc909fd3b fix(fixtures): sanitize lab subnet from committed UTAS captures
The capture sanitizer redacted tokens/device ids but left the lab Host
address (10.10.0.x) in three committed UTAS fixtures, tripping
scripts/check-no-lab-addresses.sh. Replace with an RFC 5737 TEST-NET
address (192.0.2.120) per the guard's own doctrine. Host header is
plaintext metadata only (tests decode body_b64), so no test is affected.
Pre-existing leak (fixtures committed at 0b66662/eb8311a); no lab address
was introduced this session.
2026-08-12 03:59:15 +00:00
funman300 e09344490f feat(adapter): single FIFA17 squad projector + fixture round-trip tests
fut::squad_projection is the ONE projector for every squad read shape.
project_squad(canonical squad + Fresh extension + owned items) -> the FIFA
17 squad wire object; user_mass_info_squad and squad_list are envelope-only
wrappers over the same output (no per-endpoint domain model).

Design guarantees exercised by tests:
  - purity / no N+1: consumes a host-assembled input (read_squad_with_ext +
    one batch owned-cards fetch + in-memory card defs); no per-slot lookup
  - shared shaper: every occupied slot is shaped by fut::item::shape_item,
    so squad items and /club items cannot drift
  - Fresh -> full projection; Stale -> never applied (verdict surfaced);
    Missing -> explicit, never fabricated
  - captain projects as the resolved WIRE id (never resourceId); index and
    formation round-trip verbatim; kit follows the player; two owned copies
    of one definition stay distinct

Adds committed sanitized fixtures decoded from the squad session capture
(swap, f433, persisted userMassInfo.squad read, squad/list) and
tests/squad_projection.rs: baseline / swap / formation-change / persisted
read-after-write round-trips asserted by ownership class (canonical,
extension, shadow, derived identity), plus one-projector no-divergence.
2026-08-12 02:28:05 +00:00
funman300 eb8311a5ee evidence: FIFA17 squad controlled retail capture (swap/formation/relaunch) sanitized fixtures 2026-08-12 01:16:46 +00:00
funman300 550a59d12c feat(adapter): FIFA17 squad full-replacement wire parser + reverse-map scaffolding (unrouted) 2026-08-12 00:53:46 +00:00
funman300 04c5043aba utas: My Squad filter corpus — every filter identified, root cause measured
Controlled retail capture, one criterion at a time, cleared between each.
47 transactions. Every filter the My Squad picker sends is now known from
the wire rather than guessed.

Route: GET /ut/game/fifa17/club -- the picker hits UTAS and reuses the
general club-inventory route.

  level=any|gold        quality      lowercase, ALWAYS present
  rare=SP               "Special"    uppercase, OMITTED when off
  position=ST           position     uppercase, omitted when off
  nation=52             entity id    numeric
  league=13             entity id    numeric
  team=5                entity id    numeric, NESTED under league
  sort=desc                          client constant; the UI has no sort control
  start=/count=11       pagination

Two encoding families: short string enums, and numeric FIFA ids. The ids
must never reach Core. Filters compose as plain ANDs in one query --
string and id filters alike -- so each maps independently.

THE ROOT CAUSE IS SELF-AMPLIFYING.

club_route honours type, team and league; it never reads start, count,
level, sort or year. Because start is ignored, every page returns the
same full set, so the client concludes the page was full and asks for the
next one. One scroll produced 22 requests and 6.2 MB, stopping at
start=200 only because the client gave up -- against a filtered set of 32
items that should have been three pages.

That also explains why the bug reads as erratic rather than broken:
league=13&position=ST returns every Premier League player instead of
Premier League strikers. Plausible, wrongly sized, hard to notice.

Measured filtered sets, from the real cluttered club -- these are the
acceptance test for the fix:

  unfiltered            1962
  league=13              350
  league=13&team=5        32

Two client behaviours worth carrying forward: the picker fires a query
per highlighted entry, not per selection (two requests for one club
pick), and parameter ORDER is not stable, so parsing must be key-value.

FIXTURE SIZE: bodies over 4 KB are truncated in the committed fixture,
with body_full_len and body_full_sha256 retained, because the same 1.1 MB
club response repeats ~25 times and its hash already proves identity.
13.3 MB -> 247 KB. The raw .ofcap keeps every byte, privately and
gitignored. Truncation is recorded per transaction so a trimmed fixture
is never mistaken for a whole response.

Audited across all three identifier surfaces -- headers, JSON bodies,
query strings -- before and after the size change: no leaks. 6/6
sanitiser mutations still killed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 19:32:57 +00:00
funman300 0b66662525 utas: first real corpus, and two sanitiser gaps the audit caught
24 transactions across 11 connections from a retail session: login,
hub, one pack open, two squad saves, a quick-sell, with before/after
state manifests. Raw .ofcap stays gitignored at 0600; the sanitized
corpus is committed as adapter fixtures.

TWO GAPS FOUND BY AUDITING THE OUTPUT, NOT BY TRUSTING THE SANITISER.

1. `POST /ut/auth` carries `macAddress` and `deviceId`. Session tokens
   were being redacted correctly and these were not. A committed fixture
   is a published fixture.

2. Then, with those fixed, the audit fired AGAIN on the file about to be
   committed: `GET .../phishing/trusteddevice?deviceId=...` puts the id in
   the QUERY STRING. Three input surfaces carry identifiers -- headers,
   JSON bodies, and query strings -- and the sanitiser knew about two.

Both fixed in the tool rather than by editing the file, with a
regression test and a mutation for the query path.

AND A THIRD ARTEFACT MIX-UP, in the mutation harness itself. It reported
the query-redaction mutation as SURVIVED while a hand-run of the same
mutation killed it. Cause: the harness pointed at a stale scratchpad copy
of the test that pre-dated the query assertion, so it was faithfully
testing the mutated tool against a test that could not detect the
mutation. That is the same class as the build guard checking the wrong
binary and cargo reusing a binary compiled from mutated source -- the
third instance today of measuring the wrong artifact. The harness now
resolves ROOT from its own location and runs the COMMITTED test; the
stale copy is deleted.

Harness committed as scripts/mutate-utas-observe.py so this is repeatable
rather than a thing that happened once in a scratch directory. 6/6 killed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 18:33:59 +00:00
funman300 b1bc7a764e adapter: port the FUT roster-update response, held to the live oracle's bytes
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.
2026-08-11 05:19:29 +00:00
funman300 f451406058 audit: eliminate deployment-address hardcoding; single typed endpoint config
Mandatory OpenFUT architecture audit. Two real defects found and fixed, plus
the config surface tightened so neither class can recur.

DEFECT 1 -- hidden localhost fallback. The Rust host defaulted POW hosts to
127.0.0.1 while every other URL followed OPENFUT_ADVERTISE, so a remote
deployment would emit loopback POW URLs and fail far from the cause. It also
diverged from the deployed Python entrypoint, which derives them
(POW_HOST="${POW_HOST:-$ADV:8094}"). POW endpoints now derive from the
advertised address; explicit overrides still win.

DEFECT 2 -- Default gave loopback silently. `Endpoints::default()` and
`AdapterConfig::default()` supplied 127.0.0.1, so anything constructing a
config by omission got loopback with no signal. Both `Default` impls are
REMOVED. Loopback is now `Endpoints::loopback()` / `AdapterConfig::loopback()`:
an explicit, greppable decision. Production uses `advertising(host)`.

CONFIGURABILITY. `blaze_port` and `utas_port` are now config, not literals.
The advertised Blaze port is our choice -- the client goes wherever
<serverinstanceinfo> sends it -- and 8099 is the client's own built-in default
but still deployment config. A bad port value is an error, not a silent
fallback to the previous one.

TEST-NET EVERYWHERE. Committed fixtures and tests used the lab's real LAN
address; a test that passes because its constant matches the current lab
proves nothing about relocatability. Redirector fixtures regenerated on
RFC 5737 TEST-NET-1/2/3 plus loopback. Harness scripts no longer default the
client IP to the lab address -- client-state.sh now requires it.

SEVEN REQUIRED TESTS in tests/deployment_config.rs plus host-side coverage:
remote config never silently becomes localhost; missing advertise fails
clearly; bind may differ from advertise; changing the Blaze port changes the
redirect; changing the host updates all 200+ generated URLs with no
stragglers; no helper bypasses central config; mutations are detectable.

MUTATION TESTED, and it found a hole in the audit tests themselves. Hardcoding
utas_base, reverting the POW derivation and re-hardcoding the Blaze port were
all caught. Making the redirector read `bind` instead of `advertise` was NOT:
`advertising()` sets bind == advertise, so the two sources were
indistinguishable. That is the single most likely bypass -- the oracle really
does read bind for nucleusConnect -- so the test now forces bind != advertise
and asserts the bind address never reaches the wire. Re-mutated: caught.

Wire behaviour unchanged: oracle fixtures still current, 153 tests green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 02:55:50 +00:00
funman300 8aab2c0d41 adapter: FIFA 17 redirector response; Nucleus deliberately not ported
REDIRECTOR. The first hop's <serverinstanceinfo> XML, byte-for-byte against
the oracle across three advertised addresses. Owns the response only; TLS and
HTTP transport belong to a host, exactly as the Blaze adapter owns dispatch
while the sidecar owns the socket.

The <secure>0</secure> field is the client being told the second hop is
plaintext -- independent corroboration of the plaintext Blaze finding, now
expressed in code.

NUCLEUS IS NOT PORTED, and that is a finding rather than an omission.
Instrumented across every live session:

  listener bound            YES  0.0.0.0:42131 since 00:21:32
  handler logs on connect   YES  unconditional, before any parsing
  client received the URL   YES  OSDK_NUCLEUS fetched 10+ times
  client connected          NO   zero requests, including 4 full FUT flows

So the long-standing nucleusConnect=0.0.0.0 anomaly is explained: FIFA never
follows that URL on this path. The invalid address has never mattered because
nothing dials it. Porting the stub would add an untested component for no
parity gain.

TLS CONSTRAINT RECORDED, NOT RESOLVED. All 9 observed handshakes negotiated
AES256-GCM-SHA384 = TLS 1.2 with STATIC RSA key exchange. rustls supports only
forward-secret (EC)DHE suites and cannot serve that. Whether the client also
OFFERS ECDHE is unknown -- OpenSSL follows client preference by default, so
preferring static RSA does not prove it is the only option. This must be
instrumented from a real ClientHello before a TLS stack is chosen; the module
docs say so rather than guessing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 02:48:12 +00:00
funman300 cf961603fe openfut-adapter-fifa17: FIFA 17 Blaze adapter, oracle-tested
The second migration step: the layer above the codec, deciding WHAT to say
rather than how to encode it. Sits on openfut-protocol-blaze and supplies
what that crate deliberately refuses to know.

  blaze/ids.rs           component/command/notification tables
  blaze/config.rs        injectable identity + endpoints, nothing hardcoded
  blaze/session.rs       per-connection state
  blaze/client_config.rs the fetchClientConfig tables
  blaze/responses.rs     16 Blaze::* response bodies
  blaze/dispatch.rs      (component, command) -> Vec<Frame>

Parity is tested, not asserted. fixtures/generate.py drives the real
blaze_responder_v3b.dispatch() and records 49 request->response(s)
transactions, replayed in order against a shared session per connection so
ordering-dependent behaviour is exercised: preAuth captures the locale later
ALOC fields echo, login sets the auth code getAuthToken returns. Comparison
is byte-for-byte including frame count and order.

98 tests green across both crates; clippy clean.

MUTATION TESTED, and it found a real defect in this commit's own design.
Swapping two post-login notifications and flipping one enum inside
AccountInfo both turned the suite red as intended. Hardcoding an address in
utas_base() did NOT -- the config templating substituted raw hosts directly,
making those helpers dead code that merely looked load-bearing. The table now
templates on URL-level tokens ({utas_base}, {nucleus_base},
{pow_content_url}) so they are the single place a URL shape is defined, and
the mutation is caught.

The client config table (227-243 rows per CFID) is generated from the oracle
rather than transcribed: it is reverse-engineered data, not logic, and 400
hand-copied string literals would add a typo class no reviewer can catch. The
generator substitutes real addresses back in and diffs against the oracle for
every section before writing, so the templating is verified rather than
assumed.

Reproduces one known defect deliberately: nucleusConnect is built from BIND,
not advertise, so the live split deployment tells a client on another machine
to reach Nucleus at http://0.0.0.0:42131. Confirmed against the running
container. Reproduced because it is what the only proven-working config does;
fixing it needs live validation and is a separate change. It also implies the
Nucleus stub is not reached in the current remote flow.

Blaze carries no FUT domain state -- no coins, packs, clubs or squads on this
wire -- so Session stays a session key, locale, service name, auth code and a
flag. That boundary will need defending when UTAS is migrated.

Not wired into anything. The crate answers frames; it opens no socket and
owns no runtime. The Python backend remains the live service and the oracle,
and is unmodified (contract suite still green).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 01:18:29 +00:00