fifa17: claim five routes whose handlers were already unreachable
Read the client's COMPLETE UTAS route surface out of CardsDLL's .rdata in the
running process (new tools/url_template_probe.py) and probed every one against
staging, where the Python upstream is deliberately dead so anything the Rust host
does not own answers 502 instead of being silently proxied.
That found five routes whose handlers already existed and were dead code because
`classify` never produced their Route -- the same defect as `season/list` and
`watchList`, whose fix comments are still in the file. This is the third and
fourth time:
captcha -> handle_static_ack, which already returns the oracle's exact
{encodedImg,sequence,sizeBeforeEncode}
tfa -> handle_static_ack, {}
livemessage -> handle_static_ack, {}
activeMessage -> handle_static_ack, {}
tournament/user-> FeatureOffEmpty, {} == the oracle with FUT_MODES off
(tools/utas_server.py:1504); the client builds this literal
at CardsDLL 0x18021e540 and the bare `tournament` arm never
matched it
Route's own doc comment already claimed the first four as "Rust-owned
UNCONDITIONAL", so the documentation was wrong rather than the intent. All five
are byte-identical to the oracle, so claiming them is parity, not new behaviour.
Invisible in production because the upstream answers there.
Two tests pin the vocabularies so a handler cannot go unreachable a fifth time;
both are mutation-checked (removing the captcha arm fails the first).
Also documents the surface in docs/CLIENT_ROUTE_SURFACE.md, including the trap
that bit me repeatedly: an .rdata literal is a FRAGMENT, not a callable path.
`clientdata`, `purchasegroup`, `sbs/challenges`, `squadBuildingSets`, `club/items`
and `item` all looked unserved and are not. Only `squad/mode` is genuinely
unserved, and correctly so -- it is Draft-only, which is out of scope.
L5 finding: there is NO consumable-apply route anywhere in the binary. The only
owned-item mutations the client can express are PUT item (move/pile), DELETE
item/<id> and POST delete/item (quick sell), and PUT squad. So applying a
consumable is not a dedicated endpoint; L5/L6 must be pursued by capturing the
PUT item payload, not by implementing a route that does not exist.
Host 123 lib + 45 host_test, fmt and clippy clean. tournament/user, livemessage
and activeMessage verified 200 on staging (were 502).
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
# The client's complete UTAS route surface
|
||||
|
||||
Read out of the running client's own `.rdata` on 2026-08-21 (pid 6580) with
|
||||
`fifa17-recon/tools/url_template_probe.py`, then each route probed against
|
||||
staging. This bounds the server: FIFA 17 cannot ask for a route that is not in
|
||||
this list.
|
||||
|
||||
Staging's Python upstream is deliberately dead, so a `502` there means the Rust
|
||||
host does not own the route — which makes the coverage column a measurement
|
||||
rather than an audit of the source.
|
||||
|
||||
## Route templates in CardsDLL
|
||||
|
||||
`%s` is the sku segment, built from `game/%s` (`0x18021fac8`) → `game/fifa17`.
|
||||
|
||||
```
|
||||
ut/auth ut/delete/auth
|
||||
ut/%s/user ut/delete/%s/user ut/%s/user/list
|
||||
ut/%s/club ut/%s/clubUser
|
||||
ut/%s/item ut/%s/item/resource ut/delete/%s/item
|
||||
ut/%s/defid
|
||||
ut/%s/squad ut/delete/%s/squad ut/%s/squad/mode
|
||||
ut/%s/purchased ut/%s/store ut/v2/%s/store
|
||||
ut/%s/trade ut/delete/%s/trade
|
||||
ut/%s/tradePile ut/%s/watchList ut/delete/%s/watchList
|
||||
ut/%s/auctionhouse ut/%s/marketdata
|
||||
ut/%s/match ut/%s/sbs
|
||||
ut/%s/season ut/%s/season/user ut/%s/season/%%s/user
|
||||
ut/%s/season/%%s/reset ut/%s/season/friendly
|
||||
ut/%s/tournament ut/%s/tournament/user ut/delete/%s/tournament/user
|
||||
ut/%s/champion ut/%s/draft/mode
|
||||
ut/%s/leaderboards ut/%s/leaderboards/options
|
||||
ut/%s/activeMessage ut/%s/livemessage
|
||||
ut/%s/clientdata ut/%s/phishing ut/%s/captcha ut/%s/tfa
|
||||
```
|
||||
|
||||
Suffixes appended to the above, not standalone routes:
|
||||
`/consumables/%s`, `/items`, `/purchasegroup`, `/squadBuildingSets`,
|
||||
`/challenge/%d/squad`, `/choices/manager`, `/purchase/mode/%d/draft`,
|
||||
`/transfermarket?type=%s&start=%d&num=%d`.
|
||||
|
||||
## THE TRAP when reading this list
|
||||
|
||||
A literal in `.rdata` is a **fragment**, not necessarily a callable path. Probing
|
||||
fragments bare manufactures fake gaps. Every one of these looked unserved and was
|
||||
not:
|
||||
|
||||
| looked missing | actually |
|
||||
|---|---|
|
||||
| `clientdata` | real route is `clientdata/<key>`; served (`clientdata/userHubData` → 200) |
|
||||
| `purchasegroup` | a suffix of `store`; `store/purchasegroup/all` is served |
|
||||
| `sbs/challenges` | not a route; the real ones are `sbs/sets`, `sbs/setId/<n>/challenges`, `sbs/challenge/<n>` — all served |
|
||||
| `squadBuildingSets` | not a route in the oracle either |
|
||||
| `club/items` | `items/...` literals are ART ASSET paths, not UTAS |
|
||||
| `item` | only ever PUT (move/pile) and DELETE (quick-sell) |
|
||||
|
||||
Check a candidate gap against `tools/utas_server.py`'s regex table before
|
||||
believing it.
|
||||
|
||||
## Genuinely unserved, and why that is correct
|
||||
|
||||
* `squad/mode` — bare form is never used. The oracle only has Draft sub-paths
|
||||
(`squad/mode/draft/state`, `squad/mode/<n>/draft/choices/*`). Draft is out of
|
||||
scope, so this correctly stays on Python.
|
||||
|
||||
## Fixed by this measurement
|
||||
|
||||
Four handlers existed and were unreachable because `classify` never produced
|
||||
their route, so every request fell through to Python. This is a **recurring
|
||||
defect class** in `openfut-utas-host` — `season/list` and `watchList` were the
|
||||
first two, and their fix comments are still in the file:
|
||||
|
||||
| route | handler | was |
|
||||
|---|---|---|
|
||||
| `captcha` | `handle_static_ack`, returns the oracle's exact `{encodedImg,sequence,sizeBeforeEncode}` | fell to Python |
|
||||
| `tfa` / `livemessage` / `activeMessage` | `handle_static_ack`, `{}` | fell to Python |
|
||||
| `tournament/user` | `FeatureOffEmpty`, `{}` — the oracle's answer with `FUT_MODES` off | fell to Python |
|
||||
|
||||
`Route`'s own doc comment already claimed the first four as "Rust-owned
|
||||
UNCONDITIONAL", so the documentation had been wrong rather than the intent. All
|
||||
five are byte-identical to the oracle, so claiming them is parity, not new
|
||||
behaviour. Invisible in production (the upstream answers); a 502 on staging.
|
||||
|
||||
Two regression tests now pin the vocabularies —
|
||||
`every_static_ack_tail_is_actually_routed` and
|
||||
`the_disabled_mode_reads_are_all_claimed` — so a handler cannot go unreachable a
|
||||
fifth time.
|
||||
|
||||
## No consumable apply endpoint exists
|
||||
|
||||
Support level L5 for consumables was open, with an inherited note saying there is
|
||||
"no training/position/chemistry/manager-league endpoint at all". **The route
|
||||
table confirms it from the binary**: there is no apply/training/position/
|
||||
chemistry route anywhere in CardsDLL. The only owned-item mutations the client
|
||||
can express are:
|
||||
|
||||
```
|
||||
PUT ut/%s/item move / pile
|
||||
DELETE ut/%s/item/<id> quick sell
|
||||
POST ut/delete/%s/item bulk quick sell
|
||||
PUT ut/%s/squad squad write
|
||||
```
|
||||
|
||||
So applying a consumable is **not** a dedicated server route. If it reaches the
|
||||
server at all it must ride `PUT ut/%s/item`, and L5/L6 should be pursued by
|
||||
capturing that PUT's payload while applying a card — not by looking for an
|
||||
endpoint that does not exist.
|
||||
Reference in New Issue
Block a user