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:
@@ -245,6 +245,18 @@ pub fn classify(method: &str, path: &str) -> Route {
|
||||
Some("hub") if get => Route::Hub,
|
||||
Some("store") => Route::StaticAck,
|
||||
Some("match/keepalive") => Route::StaticAck,
|
||||
// THIRD instance of the `watchList` defect below: `handle_static_ack`
|
||||
// has answered these four since it was written — `captcha` with the
|
||||
// oracle's exact `{encodedImg,sequence,sizeBeforeEncode}` and the other
|
||||
// three with `{}` (`tools/utas_server.py:1525-1529`) — and `Route`'s own
|
||||
// doc comment claims them as "Rust-owned UNCONDITIONAL". But no arm ever
|
||||
// produced the route, so every one fell through to the Python upstream.
|
||||
// Invisible in production, where that upstream answers; on staging, where
|
||||
// it is deliberately dead, all four are a 502.
|
||||
Some("captcha") => Route::StaticAck,
|
||||
Some("tfa") => Route::StaticAck,
|
||||
Some("livemessage") => Route::StaticAck,
|
||||
Some("activeMessage") => Route::StaticAck,
|
||||
// `watchList` has had a Rust handler all along, but nothing ever produced
|
||||
// this route, so `Route::WatchList` was unreachable and every request fell
|
||||
// through to Passthrough — the same defect class as `season/list`. The
|
||||
@@ -264,6 +276,13 @@ pub fn classify(method: &str, path: &str) -> Route {
|
||||
Route::Season
|
||||
}
|
||||
Some("tournament") if get => Route::FeatureOffEmpty,
|
||||
// FOURTH instance of the same defect: the client builds
|
||||
// `ut/%s/tournament/user` (literal at CardsDLL 0x18021e540) and the bare
|
||||
// `tournament` arm does not match it, so it fell through to Python. The
|
||||
// oracle answers it with `{}` whenever FUT_MODES is off
|
||||
// (`tools/utas_server.py:1504`), which is exactly what FeatureOffEmpty
|
||||
// returns — so claiming it is byte-identical parity, not new behaviour.
|
||||
Some("tournament/user") if get => Route::FeatureOffEmpty,
|
||||
Some("champion") if get => Route::FeatureOffEmpty,
|
||||
Some("clubUser") if get => Route::FeatureOffEmpty,
|
||||
Some("user/list") if get => Route::FeatureOffEmpty,
|
||||
@@ -5759,6 +5778,50 @@ mod tests {
|
||||
}
|
||||
// ── Session/capability vertical (this slice) ────────────────────────────
|
||||
|
||||
/// Every tail `handle_static_ack` can answer MUST also be produced by
|
||||
/// `classify`, or the handler is dead code and the request silently falls
|
||||
/// through to the Python upstream. That has now happened three times in this
|
||||
/// file (`season/list`, `watchList`, and these four), so it gets a test.
|
||||
#[test]
|
||||
fn every_static_ack_tail_is_actually_routed() {
|
||||
for tail in [
|
||||
"store",
|
||||
"match/keepalive",
|
||||
"captcha",
|
||||
"tfa",
|
||||
"livemessage",
|
||||
"activeMessage",
|
||||
] {
|
||||
assert_eq!(
|
||||
classify("GET", &format!("/ut/game/fifa17/{tail}")),
|
||||
Route::StaticAck,
|
||||
"{tail} must reach handle_static_ack, not Python"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The mode reads the client can actually build MUST all be claimed. The
|
||||
/// tails come from CardsDLL's own route literals (`ut/%s/tournament/user` at
|
||||
/// 0x18021e540), read out of the live binary with
|
||||
/// `fifa17-recon/tools/url_template_probe.py` — not from guesswork about
|
||||
/// what the client might ask for.
|
||||
#[test]
|
||||
fn the_disabled_mode_reads_are_all_claimed() {
|
||||
for tail in [
|
||||
"tournament",
|
||||
"tournament/user",
|
||||
"champion",
|
||||
"clubUser",
|
||||
"user/list",
|
||||
] {
|
||||
assert_eq!(
|
||||
classify("GET", &format!("/ut/game/fifa17/{tail}")),
|
||||
Route::FeatureOffEmpty,
|
||||
"{tail} must be Rust-owned, not proxied"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_routes_session_vertical() {
|
||||
assert_eq!(classify("POST", "/ut/auth"), Route::Auth);
|
||||
|
||||
Reference in New Issue
Block a user