5 Commits

Author SHA1 Message Date
funman300 658dbe2f24 wip(bridge): retained FIFA request/response mapper + docker packaging
RETAINED PRE-EXISTING WIP (brought forward after verification, not authored
here, not production-ready). Substantive mapper.rs proxy expansion plus a
multi-stage Dockerfile/.dockerignore (rustls, self-signed cert at startup).
No secrets/captures staged. Preserved off the detached base c58e7326a1.
2026-08-20 16:08:24 +00:00
funman300 c58e7326a1 Path A: add jmpscan; document FIFA-side flow blocked with live toolkit
protossl-scan: add `jmpscan` (find function-entry E9 detours leaving a module).
Used to try to locate EbisuSDK::GoOnline in FIFA23.exe, but the 505MB image is
mostly embedded data => ~3875 false positives, no clean detour cluster. Combined
with the no-string and worker-thread blocks, the FIFA-side online-flow is not
cleanly reachable with the live-memory toolkit. Documented the verdict in
connection-gate-findings.md: playable FUT is research-grade (needs interactive
IDA/Ghidra GUI + full EA-online/Blaze emulator); the clean-room spec is the
finished deliverable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 21:07:17 -07:00
funman300 5aec83ce97 M2: confirm worker-thread + event-driven gate (architectural wall)
Add a bounded manual stack-scan in the GoOnline detour (FIFA-frame finder).
Result: zero FIFA23.exe frames, sp ~2.4KB below stack top => GoOnline runs on an
anadius worker thread (queued), not FIFA's thread. Three-way corroboration that
the gate is an async "online established" event anadius (offline-only) never
pushes; FIFA waits/retries. No handler-response flip can cross it. Documented in
connection-gate-findings.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 20:21:42 -07:00
funman300 e2c6ae8e5b M2: flips fire but gate is async event (not a poll)
- Hook: force GetInternetConnectedState->connected (flags +0xCAB1A/+0xCAB1B) and
  flip GoOnline to report "1" (+0xAF530) instead of "0" (+0xADE64).
- protossl-scan: add `read` mode (hex/ascii dump at addr|module+off).
- Finding: neither flip unblocks the game; it keeps retrying GoOnline every ~7s.
  The FUT-online flow is event-driven -- the game waits for an async "online
  established" event anadius (offline-only) never pushes. Documented in
  connection-gate-findings.md. Next: trace FIFA-side flow (Ghidra) / anadius
  event-send to inject the online event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:45:15 -07:00
funman300 550b23bb26 M1 COMPLETE: gate named = GetInternetConnectedState @ anadius64.dll+0x27790
Found without Ghidra, via anadius's LSX command-registration table (handler list
is offset-by-one from the names; verified by +0x27060 = GetProfile). The gate is
GetInternetConnectedState @ anadius64.dll+0x27790: its LSX "connected" attribute
= (byte[+0xCAB1B]||byte[+0xCAB1A]) ? str(+0xAF530) : str(+0xADE64); both flags
default 0 -> reports offline. That is why the client aborts at "connecting".

M1 answers: (1) ProtoSSLConnect not reached (gate upstream); (2) connection-state
function named; (3) single actionable gate (token already provided by anadius
FakeAuth / GoOnline passes). M2 (next session) = make GetInternetConnectedState
report connected, then watch for the real Blaze connect.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:27:20 -07:00
15 changed files with 799 additions and 195 deletions
+12
View File
@@ -0,0 +1,12 @@
target/
**/target/
*.db
*.db-shm
*.db-wal
.env
.env.local
Dockerfile
.dockerignore
.git
.gitignore
captures/
+2
View File
@@ -1,5 +1,7 @@
# OpenFUT Bridge — Claude Code project context # OpenFUT Bridge — Claude Code project context
> ⚠️ **Legacy / historical (FIFA 23).** OpenFUT's working target is now **FIFA 17**; this bridge (FIFA 23 integration) is superseded per `../docs/PROJECT_STATE.md`. Kept for reference. Canonical server: `../fifa17-recon/docker/fifa17-python` (`docker compose up -d`).
Read this first. It's the standing context for every task in this project. Each Read this first. It's the standing context for every task in this project. Each
working session will give you ONE bounded task plus a verification clause; this working session will give you ONE bounded task plus a verification clause; this
file is the background that stays true across all of them. file is the background that stays true across all of them.
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
authors = ["OpenFUT Contributors"] authors = ["OpenFUT Contributors"]
description = "FIFA 23 integration layer and reverse-engineering proxy" description = "FIFA 23 integration layer and reverse-engineering proxy"
license = "MIT" license = "MIT"
repository = "https://github.com/openfut/openfut-bridge" repository = "https://git.aleshym.co/funman300/OpenFUT-Bridge.git"
[lib] [lib]
name = "openfut_bridge" name = "openfut_bridge"
+52
View File
@@ -0,0 +1,52 @@
# syntax=docker/dockerfile:1
# ---- OpenFUT Bridge: FIFA integration / reverse-engineering proxy ----
# Multi-stage build; slim Debian runtime. reqwest uses rustls-tls and the
# bridge self-signs its own cert at startup (rcgen) — no OpenSSL needed.
FROM rust:1-bookworm AS builder
WORKDIR /build
COPY Cargo.toml Cargo.lock* ./
RUN mkdir -p src src/bin \
&& echo 'fn main() {}' > src/main.rs \
&& echo '' > src/lib.rs \
&& echo 'fn main() {}' > src/bin/replay.rs \
&& cargo build --release --bin openfut-bridge 2>/dev/null || true
RUN rm -rf src
COPY src ./src
RUN touch src/main.rs src/lib.rs \
&& cargo build --release --bin openfut-bridge
# ---- Runtime ----
FROM debian:bookworm-slim AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*
RUN useradd --system --uid 10002 --create-home --home-dir /app openfut
WORKDIR /app
COPY --from=builder /build/target/release/openfut-bridge /usr/local/bin/openfut-bridge
# Captures are written at runtime — keep them on a named volume.
RUN mkdir -p /app/captures && chown openfut:openfut /app/captures
USER openfut
ENV BRIDGE_LISTEN_ADDR=0.0.0.0:8443 \
CORE_URL=http://core:8080 \
CAPTURES_DIR=/app/captures \
PLACEHOLDER_MODE=true \
TLS_ENABLED=true \
RUST_LOG=openfut_bridge=info,tower_http=info
EXPOSE 8443
VOLUME ["/app/captures"]
# The bridge serves TLS; -k because the cert is self-signed. /_bridge/health
# is the bridge's own admin endpoint (not a proxied FIFA route).
HEALTHCHECK --interval=15s --timeout=4s --start-period=8s --retries=5 \
CMD curl -fsSk https://127.0.0.1:8443/_bridge/health || exit 1
ENTRYPOINT ["openfut-bridge"]
+118
View File
@@ -142,3 +142,121 @@ Name the downstream gate. Two complementary routes:
strings in `FIFA23.exe` to find the FUT online-flow code that issues `GoOnline` strings in `FIFA23.exe` to find the FUT online-flow code that issues `GoOnline`
then waits for the token/status, and decompile it. FIFA isn't ASLR'd, so Ghidra then waits for the token/status, and decompile it. FIFA isn't ASLR'd, so Ghidra
addresses (image base `0x140000000`) map 1:1 to our recorded offsets. addresses (image base `0x140000000`) map 1:1 to our recorded offsets.
---
## M1 COMPLETE — the gate is `GetInternetConnectedState`
Found without Ghidra, by reading anadius's LSX **command-registration** function
(`anadius64.dll+0x14C0`), which lists every command-name → handler inline. NB the
`lea rax,[handler]` is **offset by one** from the `lea rdx,[name]` in that listing
(verified empirically: `+0x27060` decompiles to `GetProfile` — it builds a
`GetProfileResponse` for persona "fun"). Corrected handler map:
| anadius LSX command | handler (anadius64.dll + …) |
|---|---|
| GetProfile | 0x27060 |
| **GetInternetConnectedState** | **0x27790** |
| GoOnline | 0x2BB90 |
| GetAuthCode | 0x2BBC0 |
### The gate, named: `GetInternetConnectedState` @ `anadius64.dll+0x27790`
It serializes an LSX `InternetConnectedState` response with a `connected`
attribute whose value is:
```
connected = (byte[+0xCAB1B] != 0 || byte[+0xCAB1A] != 0) ? str(+0xAF530)
: str(+0xADE64)
```
Both flag bytes **default to 0**, so it reports the offline value (`+0xADE64`).
That is precisely why the client sits at "connecting to the EA Servers" and falls
back offline.
### Answers to the three M1 points
1. **ProtoSSLConnect reached on the abort? NO — gate upstream.** Confirmed.
2. **The connection-state function:** `GetInternetConnectedState` @
`anadius64.dll+0x27790`. Decision = the two-flag branch above.
3. **Gate count: ONE actionable gate.** The auth token is already satisfied —
`GoOnline` (`+0x2BB90`) is called during the attempt and returns success, and
anadius provides a fake auth code; the blocker is purely the connection-state.
So M2 = make `GetInternetConnectedState` report **connected** (then the game
proceeds with its existing token).
### M2 attempt results — the gate is an async EVENT, not a poll
Tried (read/write, EAAC neutralized):
- Forced `GetInternetConnectedState` → connected (set flags +0xCAB1A/+0xCAB1B → value
`"1"`; strings confirmed: +0xADE64 = `"0"` offline, +0xAF530 = `"1"` connected).
- Flipped `GoOnline` to report `"1"` (replicated its builder `+0x25BE0` with the
connected string instead of `"0"`).
**Neither made the game proceed.** Both handlers fire, no crash — but the game
**keeps retrying `GoOnline` every ~7s** and never attempts the Blaze connect.
That retry-on-timeout pattern means the FUT-online flow is **event-driven**: the
game submits `GoOnline`, gets success, then **waits for an async "online
established" event** (ONLINE_STATUS_EVENT-class) that anadius — being offline-only
— never pushes (the only `<Event sender="EALS">` it ever sends is the Challenge
handshake). So flipping poll/return values can't unblock it.
**Implication:** getting past "connecting" requires **emulating the EA-app online
event sequence** the game waits for (inject the online-status event over LSX, in
the format/order EbisuSDK expects), not a single function flip. This is a
substantially deeper task (and precedes the Blaze backend emulation).
Next: trace the FIFA-side FUT online-flow (what the game does after `GoOnline`
and exactly which event/condition it waits on) — Ghidra on FIFA23.exe (import
saved at `C:\openfut\gh-proj`), or RE anadius's LSX event-send path. `TODO/CONFIRM`.
### M2 deeper finding — worker-thread + event architecture (confirmed)
A live call-stack capture from inside the GoOnline detour (manual stack scan,
bounded by `GetCurrentThreadStackLimits`) found **zero FIFA23.exe frames** and
showed `sp` sitting ~2.4 KB below the thread's stack top. So the handler runs at
the top of a short stack — i.e. on an **anadius worker thread** (IOCP/threadpool),
not FIFA's calling thread. anadius **queues** the GoOnline command and a worker
services it.
Combined with the retry behaviour, the architecture is now clear and three-way
corroborated: **FIFA calls `EbisuSDK::GoOnline` → anadius queues it → returns →
FIFA waits for an async "online established" event → anadius (offline-only, no
online-event code) never pushes it → timeout/retry.** No handler-response flip
can unblock this; the game waits on a *push* anadius never produces.
**Conclusion:** crossing this gate requires emulating the EA-app online-event
sequence (synthesize + inject the online-status event on the worker→game callback
/ LSX path, in EbisuSDK's expected format) — a research-grade emulation effort,
preceding the Blaze backend. The cheap in-process flips are exhausted.
Reaching the FIFA-side flow would need either: (a) locate `EbisuSDK::GoOnline` in
FIFA23.exe via anadius's detour table, then `callers` to the online-flow; or
(b) find anadius's LSX event-send path and reverse the online-event format. Both
are deep. `TODO/CONFIRM`.
### Path A attempt: reach the FIFA-side online-flow (blocked with live toolkit)
Goal: find `EbisuSDK::GoOnline` in FIFA23.exe → `callers` → the game's online-flow
→ read what event it waits on. Every angle our live-memory toolkit offers is
blocked:
- **String xref:** FIFA23.exe contains no `"GoOnline"` string (typed SDK call,
not a string-built command).
- **Call-stack from the handler:** GoOnline runs on an anadius worker thread; a
bounded stack scan finds zero FIFA frames.
- **Detour scan (`jmpscan`):** scanning FIFA23.exe for function-entry `E9` jumps
leaving the module yields ~3875 hits — overwhelmingly false positives, because
the 505 MB image is mostly embedded *data* (not code), and the real detours
don't cleanly cluster. (A .text-section-only scan would help but the chain
after — isolate GoOnline → callers → event format → emulate — remains long and
each link is gated by SDK abstraction / anadius indirection / worker threads.)
**Verdict:** crossing this gate to *playable* FUT is research-grade. It needs an
interactive disassembler (IDA/Ghidra GUI, human-driven) to trace the EbisuSDK
online-flow, and then a full EA-online + Blaze emulator. The live-memory toolkit
(string/xref/disasm/callers/read/jmpscan) has been exhausted for the FIFA side.
The clean-room spec (this document) is the finished, valuable artifact.
- Check whether the flags at `+0xCAB1A` / `+0xCAB1B` are settable via anadius
config / a hidden option (cheapest flip).
- Else out-detour `GetInternetConnectedState` in our `version.dll` to force the
`+0xAF530` ("connected") path.
- Then watch for the client to attempt the real Blaze connect (our `connect`
redirect + ProtoSSL hook capture the first plaintext — milestone M3).
- `TODO/CONFIRM`: exact text of the offline/connected value strings
(`+0xADE64` / `+0xAF530`); whether any secondary check gates the attempt after
the state flips.
+118 -9
View File
@@ -31,7 +31,7 @@ use windows::Win32::System::ProcessStatus::{GetModuleInformation, MODULEINFO};
use windows::Win32::System::SystemInformation::GetLocalTime; use windows::Win32::System::SystemInformation::GetLocalTime;
use windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH; use windows::Win32::System::SystemServices::DLL_PROCESS_ATTACH;
use windows::Win32::System::Threading::{ use windows::Win32::System::Threading::{
CreateThread, GetCurrentProcess, Sleep, THREAD_CREATION_FLAGS, CreateThread, GetCurrentProcess, GetCurrentThreadStackLimits, Sleep, THREAD_CREATION_FLAGS,
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -185,6 +185,7 @@ unsafe extern "system" fn hooked(
unsafe extern "system" fn init_thread(_: *mut c_void) -> u32 { unsafe extern "system" fn init_thread(_: *mut c_void) -> u32 {
// Connection capture first. Listen on the LSX port (gate 1, so the launcher // Connection capture first. Listen on the LSX port (gate 1, so the launcher
// bootstrap succeeds) and on the redirect port (for external TLS/Blaze). // bootstrap succeeds) and on the redirect port (for external TLS/Blaze).
store_exe_range();
start_listener(LSX_PORT, "LSX"); start_listener(LSX_PORT, "LSX");
start_listener(LOCAL_PORT, "BLZ"); start_listener(LOCAL_PORT, "BLZ");
hook_dns(); hook_dns();
@@ -418,13 +419,114 @@ unsafe fn install_detour_at(addr: usize, detour: *const (), slot: &AtomicUsize,
// --- M1 read-only probe: anadius GoOnline handler ------------------------- // --- M1 read-only probe: anadius GoOnline handler -------------------------
static ORIG_GOONLINE: AtomicUsize = AtomicUsize::new(0); static ORIG_GOONLINE: AtomicUsize = AtomicUsize::new(0);
static EXE_BASE: AtomicUsize = AtomicUsize::new(0);
static EXE_SIZE: AtomicUsize = AtomicUsize::new(0);
static STACK_LOGGED: AtomicUsize = AtomicUsize::new(0);
/// Read-only detour on anadius's GoOnline handler (anadius64.dll+0x2BB90): log /// Record FIFA23.exe's base + size so we can recognise its frames in a backtrace.
/// that it was reached, then call the original unchanged. Tells us whether the unsafe fn store_exe_range() {
/// game even asks to go online during the "connecting" attempt. if let Ok(h) = GetModuleHandleW(PCWSTR::null()) {
unsafe extern "system" fn hooked_goonline(a: usize, b: usize, c: usize, d: usize) -> usize { let mut mi = MODULEINFO::default();
log(&format!("PROBE anadius GoOnline CALLED (rcx=0x{a:X} rdx=0x{b:X})")); if GetModuleInformation(
GetCurrentProcess(),
h,
&mut mi,
core::mem::size_of::<MODULEINFO>() as u32,
)
.is_ok()
{
EXE_BASE.store(h.0 as usize, Ordering::SeqCst);
EXE_SIZE.store(mi.SizeOfImage as usize, Ordering::SeqCst);
}
}
}
/// Scan the raw stack for values that land in FIFA23.exe (the game-side
/// online-flow return addresses that called into GoOnline). Unwind-free, so it
/// survives the detour trampolines that break RtlCaptureStackBackTrace.
/// One-shot to avoid log spam.
unsafe fn log_fifa_callstack(tag: &str) {
if STACK_LOGGED.swap(1, Ordering::SeqCst) != 0 {
return;
}
let base = EXE_BASE.load(Ordering::SeqCst);
let size = EXE_SIZE.load(Ordering::SeqCst);
if base == 0 || size == 0 {
log(&format!("{tag} stack scan skipped (exe range unknown)"));
return;
}
// Address of a local ~= current rsp; the stack grows down, so callers'
// return addresses sit at HIGHER addresses. Scan upward, but NEVER past the
// committed stack top (reading beyond it faults — that crashed the game).
let mut low: usize = 0;
let mut high: usize = 0;
GetCurrentThreadStackLimits(&mut low, &mut high);
let probe: usize = 0;
let sp = &probe as *const usize as usize;
let end = high; // scan the whole rest of the stack (committed, safe)
let mut line = format!(
"{tag} stack[low=0x{low:X} high=0x{high:X} sp=0x{sp:X}] FIFA23.exe refs:"
);
let mut count = 0;
let mut p = sp;
while p + 8 <= end {
let val = *(p as *const usize);
if val >= base && val < base + size {
line.push_str(&format!(" +0x{:X}", val - base));
count += 1;
if count >= 40 {
break;
}
}
p += 8;
}
log(&line);
}
/// M2 flip on anadius's GoOnline handler (anadius64.dll+0x2BB90). The original
/// handler is `mov rcx,rdx; lea r8,[+0xADD73]; lea rdx,[+0xADE64 = "0"]; call
/// +0x25BE0; mov al,1` — i.e. it builds its ErrorSuccess response with the value
/// "0" (offline). We replicate it but pass "1" (+0xAF530 = the connected value),
/// so GoOnline reports online, then return success (al=1).
unsafe extern "system" fn hooked_goonline(_a: usize, b: usize, _c: usize, _d: usize) -> usize {
log_fifa_callstack("GoOnline");
let base = ANADIUS_BASE.load(Ordering::SeqCst);
if base != 0 {
log("FLIP GoOnline -> reporting online (\"1\")");
let builder: unsafe extern "system" fn(usize, usize, usize) -> usize =
core::mem::transmute(base + 0x25BE0);
// 0x25BE0(rcx = handler's rdx, rdx = "1", r8 = +0xADD73)
builder(b, base + 0xAF530, base + 0xADD73);
return 1;
}
let orig = ORIG_GOONLINE.load(Ordering::SeqCst); let orig = ORIG_GOONLINE.load(Ordering::SeqCst);
if orig != 0 {
let f: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
core::mem::transmute(orig);
f(_a, b, _c, _d)
} else {
0
}
}
// --- M2 flip: force GetInternetConnectedState to report "connected" --------
static ORIG_ICS: AtomicUsize = AtomicUsize::new(0);
static ANADIUS_BASE: AtomicUsize = AtomicUsize::new(0);
/// anadius's GetInternetConnectedState handler (anadius64.dll+0x27790) builds an
/// LSX response whose `connected` value is:
/// (byte[+0xCAB1B] || byte[+0xCAB1A]) ? connected : offline
/// Both default to 0 → offline → the game aborts at "connecting". We force both
/// flags to 1 before the original runs, so it builds the "connected" response.
unsafe extern "system" fn hooked_ics(a: usize, b: usize, c: usize, d: usize) -> usize {
let base = ANADIUS_BASE.load(Ordering::SeqCst);
if base != 0 {
core::ptr::write_volatile((base + 0xCAB1A) as *mut u8, 1u8);
core::ptr::write_volatile((base + 0xCAB1B) as *mut u8, 1u8);
}
log("FLIP GetInternetConnectedState -> forcing connected (flags set)");
let orig = ORIG_ICS.load(Ordering::SeqCst);
if orig != 0 { if orig != 0 {
let f: unsafe extern "system" fn(usize, usize, usize, usize) -> usize = let f: unsafe extern "system" fn(usize, usize, usize, usize) -> usize =
core::mem::transmute(orig); core::mem::transmute(orig);
@@ -434,21 +536,28 @@ unsafe extern "system" fn hooked_goonline(a: usize, b: usize, c: usize, d: usize
} }
} }
/// Resolve anadius64.dll's runtime base and detour the GoOnline handler. /// Resolve anadius64.dll's runtime base, detour the GoOnline probe, and install
/// Returns true once installed (or if anadius isn't present and we should stop /// the M2 GetInternetConnectedState flip. Returns false if anadius isn't loaded.
/// retrying is decided by the caller). Returns false if anadius isn't loaded yet.
unsafe fn hook_anadius_probes() -> bool { unsafe fn hook_anadius_probes() -> bool {
let base = match GetModuleHandleW(PCWSTR(wide("anadius64.dll").as_ptr())) { let base = match GetModuleHandleW(PCWSTR(wide("anadius64.dll").as_ptr())) {
Ok(m) => m.0 as usize, Ok(m) => m.0 as usize,
Err(_) => return false, // not loaded yet Err(_) => return false, // not loaded yet
}; };
ANADIUS_BASE.store(base, Ordering::SeqCst);
log(&format!("anadius64.dll base = 0x{base:X}")); log(&format!("anadius64.dll base = 0x{base:X}"));
install_detour_at( install_detour_at(
base + 0x2BB90, base + 0x2BB90,
hooked_goonline as *const (), hooked_goonline as *const (),
&ORIG_GOONLINE, &ORIG_GOONLINE,
"PROBE anadius GoOnline @ +0x2BB90", "PROBE anadius GoOnline @ +0x2BB90",
); );
install_detour_at(
base + 0x27790,
hooked_ics as *const (),
&ORIG_ICS,
"FLIP anadius GetInternetConnectedState @ +0x27790",
);
true true
} }
+8 -2
View File
@@ -59,9 +59,15 @@ async fn main() -> ExitCode {
for capture in &captures { for capture in &captures {
let result = replay_one(&client, bridge_url, capture).await; let result = replay_one(&client, bridge_url, capture).await;
match result { match result {
Ok(status) => println!(" [{}] {} {}{status}", capture.id, capture.method, capture.path), Ok(status) => println!(
" [{}] {} {}{status}",
capture.id, capture.method, capture.path
),
Err(e) => { Err(e) => {
eprintln!(" [{}] {} {} → ERROR: {e}", capture.id, capture.method, capture.path); eprintln!(
" [{}] {} {} → ERROR: {e}",
capture.id, capture.method, capture.path
);
failures += 1; failures += 1;
} }
} }
+9 -6
View File
@@ -102,7 +102,7 @@ async fn serve_tls(
let listener = tokio::net::TcpListener::bind(addr).await?; let listener = tokio::net::TcpListener::bind(addr).await?;
loop { loop {
let (tcp, _peer) = listener.accept().await?; let (tcp, peer) = listener.accept().await?;
let acceptor = acceptor.clone(); let acceptor = acceptor.clone();
let app = app.clone(); let app = app.clone();
@@ -110,18 +110,21 @@ async fn serve_tls(
let tls_stream = match acceptor.accept(tcp).await { let tls_stream = match acceptor.accept(tcp).await {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
tracing::warn!("TLS handshake failed: {e}"); if e.kind() == std::io::ErrorKind::UnexpectedEof {
tracing::debug!(%peer, "TCP probe closed before ClientHello");
} else {
tracing::warn!(%peer, "TLS handshake failed: {e}");
}
return; return;
} }
}; };
let io = TokioIo::new(tls_stream); let io = TokioIo::new(tls_stream);
let svc = hyper::service::service_fn( let svc =
move |req: hyper::Request<hyper::body::Incoming>| { hyper::service::service_fn(move |req: hyper::Request<hyper::body::Incoming>| {
let app = app.clone(); let app = app.clone();
async move { app.oneshot(req.map(axum::body::Body::new)).await } async move { app.oneshot(req.map(axum::body::Body::new)).await }
}, });
);
if let Err(e) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) if let Err(e) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection(io, svc) .serve_connection(io, svc)
+269 -133
View File
@@ -22,363 +22,495 @@ struct ExactRoute {
const EXACT: &[ExactRoute] = &[ const EXACT: &[ExactRoute] = &[
// ── Auth ───────────────────────────────────────────────────────────────── // ── Auth ─────────────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/auth", ea_method: "POST",
core_method: "POST", core_path: "/auth/local", ea_path: "/ut/auth",
core_method: "POST",
core_path: "/auth/local",
notes: "FUT login → Core local auth", notes: "FUT login → Core local auth",
}, },
// ── Profile / Settings ─────────────────────────────────────────────────── // ── Profile / Settings ───────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/user/settings", ea_method: "GET",
core_method: "GET", core_path: "/profile", ea_path: "/ut/game/fut/user/settings",
core_method: "GET",
core_path: "/profile",
notes: "FUT user settings → Core profile", notes: "FUT user settings → Core profile",
}, },
ExactRoute { ExactRoute {
ea_method: "PUT", ea_path: "/ut/game/fut/user/settings", ea_method: "PUT",
core_method: "PUT", core_path: "/settings", ea_path: "/ut/game/fut/user/settings",
core_method: "PUT",
core_path: "/settings",
notes: "FUT update settings → Core settings", notes: "FUT update settings → Core settings",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/user/accountinfo", ea_method: "GET",
core_method: "GET", core_path: "/profile", ea_path: "/ut/game/fut/user/accountinfo",
core_method: "GET",
core_path: "/profile",
notes: "FUT account info → Core profile", notes: "FUT account info → Core profile",
}, },
// ── Club / Mass info ────────────────────────────────────────────────────── // ── Club / Mass info ──────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/usermassinfo", ea_method: "GET",
core_method: "GET", core_path: "/club", ea_path: "/ut/game/fut/usermassinfo",
core_method: "GET",
core_path: "/club",
notes: "FUT mass info (club + coins) → Core club", notes: "FUT mass info (club + coins) → Core club",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/club", ea_method: "GET",
core_method: "GET", core_path: "/club", ea_path: "/ut/game/fut/club",
core_method: "GET",
core_path: "/club",
notes: "FUT club info → Core club", notes: "FUT club info → Core club",
}, },
ExactRoute { ExactRoute {
ea_method: "PUT", ea_path: "/ut/game/fut/club", ea_method: "PUT",
core_method: "PUT", core_path: "/club", ea_path: "/ut/game/fut/club",
core_method: "PUT",
core_path: "/club",
notes: "FUT update club → Core update club", notes: "FUT update club → Core update club",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/club/stats", ea_method: "GET",
core_method: "GET", core_path: "/statistics", ea_path: "/ut/game/fut/club/stats",
core_method: "GET",
core_path: "/statistics",
notes: "FUT club stats → Core statistics", notes: "FUT club stats → Core statistics",
}, },
// ── Cards / Collection ──────────────────────────────────────────────────── // ── Cards / Collection ────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/item", ea_method: "GET",
core_method: "GET", core_path: "/collection", ea_path: "/ut/game/fut/item",
core_method: "GET",
core_path: "/collection",
notes: "FUT collection → Core owned cards", notes: "FUT collection → Core owned cards",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/item/search", ea_method: "GET",
core_method: "GET", core_path: "/cards", ea_path: "/ut/game/fut/item/search",
core_method: "GET",
core_path: "/cards",
notes: "FUT item search → Core card catalogue (query params forwarded)", notes: "FUT item search → Core card catalogue (query params forwarded)",
}, },
// ── Squad ───────────────────────────────────────────────────────────────── // ── Squad ─────────────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/squad/active", ea_method: "GET",
core_method: "GET", core_path: "/squad", ea_path: "/ut/game/fut/squad/active",
core_method: "GET",
core_path: "/squad",
notes: "FUT active squad → Core squad", notes: "FUT active squad → Core squad",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/squad/0", ea_method: "GET",
core_method: "GET", core_path: "/squad", ea_path: "/ut/game/fut/squad/0",
core_method: "GET",
core_path: "/squad",
notes: "FUT squad by slot 0 → Core squad (first squad)", notes: "FUT squad by slot 0 → Core squad (first squad)",
}, },
ExactRoute { ExactRoute {
ea_method: "PUT", ea_path: "/ut/game/fut/squad/active", ea_method: "PUT",
core_method: "POST", core_path: "/squad", ea_path: "/ut/game/fut/squad/active",
core_method: "POST",
core_path: "/squad",
notes: "FUT save squad → Core save squad", notes: "FUT save squad → Core save squad",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/squad/chemistry", ea_method: "GET",
core_method: "GET", core_path: "/squad", ea_path: "/ut/game/fut/squad/chemistry",
core_method: "GET",
core_path: "/squad",
notes: "FUT squad chemistry → Core squad (chemistry included in response)", notes: "FUT squad chemistry → Core squad (chemistry included in response)",
}, },
// ── Packs ───────────────────────────────────────────────────────────────── // ── Packs ─────────────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/store/packdetails", ea_method: "GET",
core_method: "GET", core_path: "/packs", ea_path: "/ut/game/fut/store/packdetails",
core_method: "GET",
core_path: "/packs",
notes: "FUT pack store → Core pack list", notes: "FUT pack store → Core pack list",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/store/purchase", ea_method: "POST",
core_method: "POST", core_path: "/packs/buy", ea_path: "/ut/game/fut/store/purchase",
core_method: "POST",
core_path: "/packs/buy",
notes: "FUT pack purchase → Core pack buy", notes: "FUT pack purchase → Core pack buy",
}, },
// ── Transfer Market ─────────────────────────────────────────────────────── // ── Transfer Market ───────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/transfermarket", ea_method: "GET",
core_method: "GET", core_path: "/market", ea_path: "/ut/game/fut/transfermarket",
core_method: "GET",
core_path: "/market",
notes: "FUT transfer market search → Core NPC market", notes: "FUT transfer market search → Core NPC market",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/trade/bid", ea_method: "POST",
core_method: "POST", core_path: "/market/buy", ea_path: "/ut/game/fut/trade/bid",
core_method: "POST",
core_path: "/market/buy",
notes: "FUT bid/buy now → Core market buy", notes: "FUT bid/buy now → Core market buy",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/trade/watchlist", ea_method: "GET",
core_method: "GET", core_path: "/market", ea_path: "/ut/game/fut/trade/watchlist",
core_method: "GET",
core_path: "/market",
notes: "FUT watchlist → Core market (approximation)", notes: "FUT watchlist → Core market (approximation)",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/trade/tradepile", ea_method: "GET",
core_method: "GET", core_path: "/market/my-listings", ea_path: "/ut/game/fut/trade/tradepile",
core_method: "GET",
core_path: "/market/my-listings",
notes: "FUT trade pile (my listings) → Core my-listings", notes: "FUT trade pile (my listings) → Core my-listings",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/auctionhouse", ea_method: "POST",
core_method: "POST", core_path: "/market/sell", ea_path: "/ut/game/fut/auctionhouse",
core_method: "POST",
core_path: "/market/sell",
notes: "FUT list card on AH → Core sell card", notes: "FUT list card on AH → Core sell card",
}, },
// ── Objectives ──────────────────────────────────────────────────────────── // ── Objectives ────────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/objectives", ea_method: "GET",
core_method: "GET", core_path: "/objectives", ea_path: "/ut/game/fut/objectives",
core_method: "GET",
core_path: "/objectives",
notes: "FUT objectives → Core objectives list", notes: "FUT objectives → Core objectives list",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/objectives/claim", ea_method: "POST",
core_method: "POST", core_path: "/objectives/claim", ea_path: "/ut/game/fut/objectives/claim",
core_method: "POST",
core_path: "/objectives/claim",
notes: "FUT claim objective → Core claim", notes: "FUT claim objective → Core claim",
}, },
// ── Events ──────────────────────────────────────────────────────────────── // ── Events ────────────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/events", ea_method: "GET",
core_method: "GET", core_path: "/events", ea_path: "/ut/game/fut/events",
core_method: "GET",
core_path: "/events",
notes: "FUT events → Core events list", notes: "FUT events → Core events list",
}, },
// ── Matches / Squad Battles ─────────────────────────────────────────────── // ── Matches / Squad Battles ───────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/squadbattle/opponent", ea_method: "GET",
core_method: "GET", core_path: "/matches/opponent", ea_path: "/ut/game/fut/squadbattle/opponent",
core_method: "GET",
core_path: "/matches/opponent",
notes: "Squad battles opponent → Core match opponent generator", notes: "Squad battles opponent → Core match opponent generator",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/result", ea_method: "POST",
core_method: "POST", core_path: "/matches/result", ea_path: "/ut/game/fut/result",
core_method: "POST",
core_path: "/matches/result",
notes: "FUT match result submit → Core match result", notes: "FUT match result submit → Core match result",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/matches", ea_method: "GET",
core_method: "GET", core_path: "/matches", ea_path: "/ut/game/fut/matches",
core_method: "GET",
core_path: "/matches",
notes: "FUT match history → Core match list", notes: "FUT match history → Core match list",
}, },
// ── SBC ────────────────────────────────────────────────────────────────── // ── SBC ──────────────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/sbc", ea_method: "GET",
core_method: "GET", core_path: "/sbc", ea_path: "/ut/game/fut/sbc",
core_method: "GET",
core_path: "/sbc",
notes: "FUT SBC list → Core SBC list", notes: "FUT SBC list → Core SBC list",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/sbc/challenges", ea_method: "GET",
core_method: "GET", core_path: "/sbc", ea_path: "/ut/game/fut/sbc/challenges",
core_method: "GET",
core_path: "/sbc",
notes: "FUT SBC challenges → Core SBC list", notes: "FUT SBC challenges → Core SBC list",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/sbc/submission", ea_method: "POST",
core_method: "POST", core_path: "/sbc/submit", ea_path: "/ut/game/fut/sbc/submission",
core_method: "POST",
core_path: "/sbc/submit",
notes: "FUT SBC submission → Core SBC submit", notes: "FUT SBC submission → Core SBC submit",
}, },
// ── Draft ───────────────────────────────────────────────────────────────── // ── Draft ─────────────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/draft", ea_method: "GET",
core_method: "GET", core_path: "/draft/squad", ea_path: "/ut/game/fut/draft",
core_method: "GET",
core_path: "/draft/squad",
notes: "FUT draft view → Core draft squad", notes: "FUT draft view → Core draft squad",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/draft/new", ea_method: "POST",
core_method: "POST", core_path: "/draft/start", ea_path: "/ut/game/fut/draft/new",
core_method: "POST",
core_path: "/draft/start",
notes: "FUT new draft → Core draft start", notes: "FUT new draft → Core draft start",
}, },
// ── Division / Season ───────────────────────────────────────────────────── // ── Division / Season ─────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/division/rivals", ea_method: "GET",
core_method: "GET", core_path: "/division", ea_path: "/ut/game/fut/division/rivals",
core_method: "GET",
core_path: "/division",
notes: "FUT Division Rivals status → Core division", notes: "FUT Division Rivals status → Core division",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/division/rivals/claim", ea_method: "POST",
core_method: "POST", core_path: "/rivals/claim-weekly", ea_path: "/ut/game/fut/division/rivals/claim",
core_method: "POST",
core_path: "/rivals/claim-weekly",
notes: "FUT rivals weekly claim → Core rivals claim", notes: "FUT rivals weekly claim → Core rivals claim",
}, },
// ── FUT Champions ───────────────────────────────────────────────────────── // ── FUT Champions ─────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/champs", ea_method: "GET",
core_method: "GET", core_path: "/fut-champs", ea_path: "/ut/game/fut/champs",
core_method: "GET",
core_path: "/fut-champs",
notes: "FUT Champions status → Core fut-champs", notes: "FUT Champions status → Core fut-champs",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/champs/start", ea_method: "POST",
core_method: "POST", core_path: "/fut-champs/start", ea_path: "/ut/game/fut/champs/start",
core_method: "POST",
core_path: "/fut-champs/start",
notes: "FUT Champions start week → Core fut-champs start", notes: "FUT Champions start week → Core fut-champs start",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/champs/history", ea_method: "GET",
core_method: "GET", core_path: "/fut-champs/history", ea_path: "/ut/game/fut/champs/history",
core_method: "GET",
core_path: "/fut-champs/history",
notes: "FUT Champions history → Core history", notes: "FUT Champions history → Core history",
}, },
// ── Card Upgrades ───────────────────────────────────────────────────────── // ── Card Upgrades ─────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/chemistry", ea_method: "GET",
core_method: "GET", core_path: "/chemistry-styles", ea_path: "/ut/game/fut/chemistry",
core_method: "GET",
core_path: "/chemistry-styles",
notes: "FUT chemistry styles → Core chemistry styles list", notes: "FUT chemistry styles → Core chemistry styles list",
}, },
// ── Notifications ───────────────────────────────────────────────────────── // ── Notifications ─────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/notification", ea_method: "GET",
core_method: "GET", core_path: "/notifications", ea_path: "/ut/game/fut/notification",
core_method: "GET",
core_path: "/notifications",
notes: "FUT notifications → Core notifications", notes: "FUT notifications → Core notifications",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/notifications", ea_method: "GET",
core_method: "GET", core_path: "/notifications", ea_path: "/ut/game/fut/notifications",
core_method: "GET",
core_path: "/notifications",
notes: "FUT notifications (plural form) → Core notifications", notes: "FUT notifications (plural form) → Core notifications",
}, },
// ── Squad list ──────────────────────────────────────────────────────────── // ── Squad list ────────────────────────────────────────────────────────────
// ── Division / Season history / Leaderboard ─────────────────────────────── // ── Division / Season history / Leaderboard ───────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/division/history", ea_method: "GET",
core_method: "GET", core_path: "/division/history", ea_path: "/ut/game/fut/division/history",
core_method: "GET",
core_path: "/division/history",
notes: "FUT division history → Core season history", notes: "FUT division history → Core season history",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/division/leaderboard", ea_method: "GET",
core_method: "GET", core_path: "/division/leaderboard", ea_path: "/ut/game/fut/division/leaderboard",
core_method: "GET",
core_path: "/division/leaderboard",
notes: "FUT division leaderboard → Core seeded NPC leaderboard", notes: "FUT division leaderboard → Core seeded NPC leaderboard",
}, },
// ── Market trade history ────────────────────────────────────────────────── // ── Market trade history ──────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/trade/history", ea_method: "GET",
core_method: "GET", core_path: "/market/trade-history", ea_path: "/ut/game/fut/trade/history",
core_method: "GET",
core_path: "/market/trade-history",
notes: "FUT trade history → Core market trade history", notes: "FUT trade history → Core market trade history",
}, },
// ── Daily check-in ──────────────────────────────────────────────────────── // ── Daily check-in ────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/dailyObjective", ea_method: "GET",
core_method: "GET", core_path: "/club/checkin", ea_path: "/ut/game/fut/dailyObjective",
core_method: "GET",
core_path: "/club/checkin",
notes: "FUT daily objective status → Core check-in status", notes: "FUT daily objective status → Core check-in status",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/dailyObjective/claim", ea_method: "POST",
core_method: "POST", core_path: "/club/checkin", ea_path: "/ut/game/fut/dailyObjective/claim",
core_method: "POST",
core_path: "/club/checkin",
notes: "FUT daily objective claim → Core check-in claim", notes: "FUT daily objective claim → Core check-in claim",
}, },
// ── Club milestones ─────────────────────────────────────────────────────── // ── Club milestones ───────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/milestones", ea_method: "GET",
core_method: "GET", core_path: "/club/milestones", ea_path: "/ut/game/fut/milestones",
core_method: "GET",
core_path: "/club/milestones",
notes: "FUT milestones → Core club milestones", notes: "FUT milestones → Core club milestones",
}, },
// ── Squad list ──────────────────────────────────────────────────────────── // ── Squad list ────────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/squad/list", ea_method: "GET",
core_method: "GET", core_path: "/squads", ea_path: "/ut/game/fut/squad/list",
core_method: "GET",
core_path: "/squads",
notes: "FUT squad list → Core all squads", notes: "FUT squad list → Core all squads",
}, },
ExactRoute { ExactRoute {
ea_method: "DELETE", ea_path: "/ut/game/fut/squad/active", ea_method: "DELETE",
core_method: "DELETE", core_path: "/squad", ea_path: "/ut/game/fut/squad/active",
core_method: "DELETE",
core_path: "/squad",
notes: "FUT delete active squad → Core delete squad (best-effort)", notes: "FUT delete active squad → Core delete squad (best-effort)",
}, },
// ── Achievements / Trophies ─────────────────────────────────────────────── // ── Achievements / Trophies ───────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/trophies", ea_method: "GET",
core_method: "GET", core_path: "/achievements", ea_path: "/ut/game/fut/trophies",
core_method: "GET",
core_path: "/achievements",
notes: "FUT trophies → Core achievements", notes: "FUT trophies → Core achievements",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/trophy", ea_method: "GET",
core_method: "GET", core_path: "/achievements", ea_path: "/ut/game/fut/trophy",
core_method: "GET",
core_path: "/achievements",
notes: "FUT trophy (singular) → Core achievements", notes: "FUT trophy (singular) → Core achievements",
}, },
// ── Rivals extra endpoints ──────────────────────────────────────────────── // ── Rivals extra endpoints ────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/rivals/rank", ea_method: "GET",
core_method: "GET", core_path: "/division", ea_path: "/ut/game/fut/rivals/rank",
core_method: "GET",
core_path: "/division",
notes: "FUT rivals rank → Core division", notes: "FUT rivals rank → Core division",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/rivals/result", ea_method: "POST",
core_method: "POST", core_path: "/matches/result", ea_path: "/ut/game/fut/rivals/result",
core_method: "POST",
core_path: "/matches/result",
notes: "FUT rivals match result → Core match result", notes: "FUT rivals match result → Core match result",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/rivals/leaderboard", ea_method: "GET",
core_method: "GET", core_path: "/statistics", ea_path: "/ut/game/fut/rivals/leaderboard",
core_method: "GET",
core_path: "/statistics",
notes: "FUT rivals leaderboard → Core statistics (offline approximation)", notes: "FUT rivals leaderboard → Core statistics (offline approximation)",
}, },
// ── Objectives sub-groups ───────────────────────────────────────────────── // ── Objectives sub-groups ─────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/objectives/group", ea_method: "GET",
core_method: "GET", core_path: "/objectives", ea_path: "/ut/game/fut/objectives/group",
core_method: "GET",
core_path: "/objectives",
notes: "FUT objectives group → Core objectives", notes: "FUT objectives group → Core objectives",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/objectives/daily", ea_method: "GET",
core_method: "GET", core_path: "/objectives", ea_path: "/ut/game/fut/objectives/daily",
core_method: "GET",
core_path: "/objectives",
notes: "FUT daily objectives → Core objectives", notes: "FUT daily objectives → Core objectives",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/objectives/weekly", ea_method: "GET",
core_method: "GET", core_path: "/objectives", ea_path: "/ut/game/fut/objectives/weekly",
core_method: "GET",
core_path: "/objectives",
notes: "FUT weekly objectives → Core objectives", notes: "FUT weekly objectives → Core objectives",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/objectives/group/claim", ea_method: "POST",
core_method: "POST", core_path: "/objectives/claim", ea_path: "/ut/game/fut/objectives/group/claim",
core_method: "POST",
core_path: "/objectives/claim",
notes: "FUT objectives group claim → Core objectives claim", notes: "FUT objectives group claim → Core objectives claim",
}, },
// ── Catalogue / Card search ─────────────────────────────────────────────── // ── Catalogue / Card search ───────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/catalogue/item", ea_method: "GET",
core_method: "GET", core_path: "/cards", ea_path: "/ut/game/fut/catalogue/item",
core_method: "GET",
core_path: "/cards",
notes: "FUT catalogue items → Core card catalogue", notes: "FUT catalogue items → Core card catalogue",
}, },
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/catalogue", ea_method: "GET",
core_method: "GET", core_path: "/cards", ea_path: "/ut/game/fut/catalogue",
core_method: "GET",
core_path: "/cards",
notes: "FUT catalogue → Core card catalogue", notes: "FUT catalogue → Core card catalogue",
}, },
// ── Store / Pricing ─────────────────────────────────────────────────────── // ── Store / Pricing ───────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/store/pricetiers", ea_method: "GET",
core_method: "GET", core_path: "/packs/store", ea_path: "/ut/game/fut/store/pricetiers",
core_method: "GET",
core_path: "/packs/store",
notes: "FUT price tiers → Core pack store definitions", notes: "FUT price tiers → Core pack store definitions",
}, },
// ── Consumables / Chemistry / Fitness ───────────────────────────────────── // ── Consumables / Chemistry / Fitness ─────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/consumables", ea_method: "GET",
core_method: "GET", core_path: "/chemistry-styles", ea_path: "/ut/game/fut/consumables",
core_method: "GET",
core_path: "/chemistry-styles",
notes: "FUT consumables → Core chemistry styles (approximation)", notes: "FUT consumables → Core chemistry styles (approximation)",
}, },
ExactRoute { ExactRoute {
ea_method: "POST", ea_path: "/ut/game/fut/fitness", ea_method: "POST",
core_method: "GET", core_path: "/club", ea_path: "/ut/game/fut/fitness",
core_method: "GET",
core_path: "/club",
notes: "FUT fitness apply → Core club (placeholder, fitness not tracked)", notes: "FUT fitness apply → Core club (placeholder, fitness not tracked)",
}, },
// ── Loan items ──────────────────────────────────────────────────────────── // ── Loan items ────────────────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/loanitems", ea_method: "GET",
core_method: "GET", core_path: "/collection", ea_path: "/ut/game/fut/loanitems",
core_method: "GET",
core_path: "/collection",
notes: "FUT loan items → Core collection (client filters is_loan)", notes: "FUT loan items → Core collection (client filters is_loan)",
}, },
// ── Customization / Kit ─────────────────────────────────────────────────── // ── Customization / Kit ───────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/customization", ea_method: "GET",
core_method: "GET", core_path: "/settings", ea_path: "/ut/game/fut/customization",
core_method: "GET",
core_path: "/settings",
notes: "FUT customization → Core settings", notes: "FUT customization → Core settings",
}, },
ExactRoute { ExactRoute {
ea_method: "PUT", ea_path: "/ut/game/fut/customization", ea_method: "PUT",
core_method: "PUT", core_path: "/settings", ea_path: "/ut/game/fut/customization",
core_method: "PUT",
core_path: "/settings",
notes: "FUT save customization → Core save settings", notes: "FUT save customization → Core save settings",
}, },
// ── Active messages / MOTD ──────────────────────────────────────────────── // ── Active messages / MOTD ────────────────────────────────────────────────
ExactRoute { ExactRoute {
ea_method: "GET", ea_path: "/ut/game/fut/activeMessage", ea_method: "GET",
core_method: "GET", core_path: "/notifications", ea_path: "/ut/game/fut/activeMessage",
core_method: "GET",
core_path: "/notifications",
notes: "FUT active messages → Core notifications (mapped to nearest equivalent)", notes: "FUT active messages → Core notifications (mapped to nearest equivalent)",
}, },
]; ];
@@ -732,7 +864,11 @@ mod tests {
#[test] #[test]
fn test_total_exact_routes_count() { fn test_total_exact_routes_count() {
assert!(EXACT.len() >= 61, "expected at least 61 exact mappings, got {}", EXACT.len()); assert!(
EXACT.len() >= 61,
"expected at least 61 exact mappings, got {}",
EXACT.len()
);
} }
// ── Phase 22 new mappings ───────────────────────────────────────────────── // ── Phase 22 new mappings ─────────────────────────────────────────────────
+7 -2
View File
@@ -107,8 +107,13 @@ pub async fn catch_all_handler(
.unwrap_or_default() .unwrap_or_default()
); );
let mut capture = let mut capture = CapturedRequest::new(
CapturedRequest::new(&method, &path, query.as_deref(), headers.clone(), body_str.clone()); &method,
&path,
query.as_deref(),
headers.clone(),
body_str.clone(),
);
let (response_body, status_code): (Value, u16) = let (response_body, status_code): (Value, u16) =
if let Some(mapping) = map_to_core(&method, &path) { if let Some(mapping) = map_to_core(&method, &path) {
+21 -21
View File
@@ -158,10 +158,12 @@ pub async fn get_capture_diff(
captures.iter().find(|c| c.id == id) captures.iter().find(|c| c.id == id)
}; };
let cap_a = find(&params.a) let cap_a = find(&params.a).ok_or_else(|| {
.ok_or_else(|| BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.a)))?; BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.a))
let cap_b = find(&params.b) })?;
.ok_or_else(|| BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.b)))?; let cap_b = find(&params.b).ok_or_else(|| {
BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.b))
})?;
// Structural JSON diff of request bodies // Structural JSON diff of request bodies
let body_diff = diff_json_strings(cap_a.body.as_deref(), cap_b.body.as_deref()); let body_diff = diff_json_strings(cap_a.body.as_deref(), cap_b.body.as_deref());
@@ -184,13 +186,17 @@ pub async fn get_capture_diff(
.headers .headers
.iter() .iter()
.filter_map(|(k, va)| { .filter_map(|(k, va)| {
cap_b.headers.iter().find(|(kb, _)| kb == k).and_then(|(_, vb)| { cap_b
if va != vb { .headers
Some(json!({ "header": k, "a": va, "b": vb })) .iter()
} else { .find(|(kb, _)| kb == k)
None .and_then(|(_, vb)| {
} if va != vb {
}) Some(json!({ "header": k, "a": va, "b": vb }))
} else {
None
}
})
}) })
.collect(); .collect();
@@ -226,10 +232,8 @@ fn diff_json_strings(a: Option<&str>, b: Option<&str>) -> Value {
return json!({ "same": true }); return json!({ "same": true });
} }
// Try to parse as JSON objects for a structured diff // Try to parse as JSON objects for a structured diff
let va: Option<serde_json::Map<String, Value>> = let va: Option<serde_json::Map<String, Value>> = serde_json::from_str(a_str).ok();
serde_json::from_str(a_str).ok(); let vb: Option<serde_json::Map<String, Value>> = serde_json::from_str(b_str).ok();
let vb: Option<serde_json::Map<String, Value>> =
serde_json::from_str(b_str).ok();
match (va, vb) { match (va, vb) {
(Some(ma), Some(mb)) => { (Some(ma), Some(mb)) => {
@@ -282,8 +286,7 @@ pub async fn post_replay_capture(
State(state): State<ProxyState>, State(state): State<ProxyState>,
Path(capture_id): Path<String>, Path(capture_id): Path<String>,
) -> BridgeResult<Json<Value>> { ) -> BridgeResult<Json<Value>> {
let captures = let captures = load_all_captures(&state.config.captures_dir).map_err(BridgeError::Internal)?;
load_all_captures(&state.config.captures_dir).map_err(BridgeError::Internal)?;
let capture = captures let capture = captures
.iter() .iter()
@@ -305,10 +308,7 @@ pub async fn post_replay_capture(
) )
.await .await
{ {
Ok((body, status)) => ( Ok((body, status)) => (status, crate::shaper::shape_response(&m.core_path, body)),
status,
crate::shaper::shape_response(&m.core_path, body),
),
Err(e) => ( Err(e) => (
502u16, 502u16,
json!({ "error": format!("core forwarding failed: {e}") }), json!({ "error": format!("core forwarding failed: {e}") }),
+6 -1
View File
@@ -69,7 +69,12 @@ pub async fn get_guide(State(state): State<ProxyState>) -> Html<String> {
.unwrap_or("localhost"); .unwrap_or("localhost");
let tls = state.config.tls_enabled; let tls = state.config.tls_enabled;
let scheme = if tls { "https" } else { "http" }; let scheme = if tls { "https" } else { "http" };
let port = state.config.listen_addr.split(':').next_back().unwrap_or("8080"); let port = state
.config
.listen_addr
.split(':')
.next_back()
.unwrap_or("8080");
let bridge_url = format!("{scheme}://{host}:{port}"); let bridge_url = format!("{scheme}://{host}:{port}");
let core_url = &state.config.core_url; let core_url = &state.config.core_url;
+1 -4
View File
@@ -157,10 +157,7 @@ fn shape_squad_response(core: Value) -> Value {
/// Wraps Core market response in the FUT transfer market envelope. /// Wraps Core market response in the FUT transfer market envelope.
fn shape_market_response(core: Value) -> Value { fn shape_market_response(core: Value) -> Value {
let listings = core.get("listings").cloned().unwrap_or(json!([])); let listings = core.get("listings").cloned().unwrap_or(json!([]));
let total = core let total = core.get("total").and_then(|v| v.as_u64()).unwrap_or(0);
.get("total")
.and_then(|v| v.as_u64())
.unwrap_or(0);
json!({ json!({
"auctionInfo": listings, "auctionInfo": listings,
+59 -16
View File
@@ -1,3 +1,4 @@
use axum::routing::{any, delete, get, post};
use axum::{ use axum::{
body::Body, body::Body,
http::{Request, StatusCode}, http::{Request, StatusCode},
@@ -9,7 +10,6 @@ use openfut_bridge::{
proxy::ProxyState, proxy::ProxyState,
routes, routes,
}; };
use axum::routing::{any, delete, get, post};
use tower::ServiceExt; use tower::ServiceExt;
// ── Unit tests ──────────────────────────────────────────────────────────────── // ── Unit tests ────────────────────────────────────────────────────────────────
@@ -96,9 +96,15 @@ fn build_test_app() -> axum::Router {
.route("/_bridge/dashboard", get(routes::health::get_dashboard)) .route("/_bridge/dashboard", get(routes::health::get_dashboard))
.route("/_bridge/captures", get(routes::admin::get_captures)) .route("/_bridge/captures", get(routes::admin::get_captures))
.route("/_bridge/captures", delete(routes::admin::delete_captures)) .route("/_bridge/captures", delete(routes::admin::delete_captures))
.route("/_bridge/unknown", get(routes::admin::get_unknown_endpoints)) .route(
"/_bridge/unknown",
get(routes::admin::get_unknown_endpoints),
)
.route("/_bridge/status", get(routes::admin::get_endpoint_status)) .route("/_bridge/status", get(routes::admin::get_endpoint_status))
.route("/_bridge/captures/:id/replay", post(routes::admin::post_replay_capture)) .route(
"/_bridge/captures/:id/replay",
post(routes::admin::post_replay_capture),
)
.fallback(any(openfut_bridge::proxy::catch_all_handler)) .fallback(any(openfut_bridge::proxy::catch_all_handler))
.with_state(state) .with_state(state)
} }
@@ -107,11 +113,18 @@ fn build_test_app() -> axum::Router {
async fn test_bridge_health_endpoint() { async fn test_bridge_health_endpoint() {
let app = build_test_app(); let app = build_test_app();
let resp = app let resp = app
.oneshot(Request::builder().uri("/_bridge/health").body(Body::empty()).unwrap()) .oneshot(
Request::builder()
.uri("/_bridge/health")
.body(Body::empty())
.unwrap(),
)
.await .await
.unwrap(); .unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["status"], "ok"); assert_eq!(json["status"], "ok");
assert_eq!(json["service"], "openfut-bridge"); assert_eq!(json["service"], "openfut-bridge");
@@ -131,7 +144,9 @@ async fn test_bridge_placeholder_mode_returns_ok() {
.await .await
.unwrap(); .unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(json["status"], "ok"); assert_eq!(json["status"], "ok");
assert!(json["openfut_note"].is_string()); assert!(json["openfut_note"].is_string());
@@ -150,7 +165,9 @@ async fn test_bridge_captures_endpoint_returns_list() {
.await .await
.unwrap(); .unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json["captures"].is_array()); assert!(json["captures"].is_array());
assert!(json["total"].is_number()); assert!(json["total"].is_number());
@@ -170,7 +187,9 @@ async fn test_bridge_delete_captures() {
.await .await
.unwrap(); .unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json["deleted"].is_number()); assert!(json["deleted"].is_number());
} }
@@ -188,7 +207,9 @@ async fn test_bridge_status_endpoint() {
.await .await
.unwrap(); .unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let json: serde_json::Value = serde_json::from_slice(&body).unwrap(); let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert!(json["endpoints"].is_array()); assert!(json["endpoints"].is_array());
} }
@@ -208,15 +229,25 @@ async fn test_dashboard_returns_html() {
.await .await
.unwrap(); .unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
let ct = resp.headers().get("content-type").unwrap().to_str().unwrap(); let ct = resp
.headers()
.get("content-type")
.unwrap()
.to_str()
.unwrap();
assert!(ct.contains("text/html"), "expected text/html, got {ct}"); assert!(ct.contains("text/html"), "expected text/html, got {ct}");
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let html = String::from_utf8(body.to_vec()).unwrap(); let html = String::from_utf8(body.to_vec()).unwrap();
assert!(html.contains("OpenFUT Dashboard"), "title missing"); assert!(html.contains("OpenFUT Dashboard"), "title missing");
assert!(html.contains("const CORE ="), "core URL injection missing"); assert!(html.contains("const CORE ="), "core URL injection missing");
// Placeholder URL injected in test mode // Placeholder URL injected in test mode
assert!(html.contains("127.0.0.1:9999"), "core URL not injected"); assert!(html.contains("127.0.0.1:9999"), "core URL not injected");
assert!(!html.contains("{{CORE_URL}}"), "template placeholder was not replaced"); assert!(
!html.contains("{{CORE_URL}}"),
"template placeholder was not replaced"
);
} }
#[tokio::test] #[tokio::test]
@@ -231,7 +262,9 @@ async fn test_dashboard_contains_key_sections() {
) )
.await .await
.unwrap(); .unwrap();
let body = axum::body::to_bytes(resp.into_body(), usize::MAX).await.unwrap(); let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let html = String::from_utf8(body.to_vec()).unwrap(); let html = String::from_utf8(body.to_vec()).unwrap();
// Verify all major tab sections are present // Verify all major tab sections are present
assert!(html.contains("tab-club"), "club tab missing"); assert!(html.contains("tab-club"), "club tab missing");
@@ -250,8 +283,14 @@ async fn test_dashboard_contains_key_sections() {
assert!(html.contains("tab-statistics"), "statistics tab missing"); assert!(html.contains("tab-statistics"), "statistics tab missing");
assert!(html.contains("tab-catalog"), "card catalog tab missing"); assert!(html.contains("tab-catalog"), "card catalog tab missing");
assert!(html.contains("tab-settings"), "settings tab missing"); assert!(html.contains("tab-settings"), "settings tab missing");
assert!(html.contains("tab-notifications"), "notifications tab missing"); assert!(
assert!(html.contains("tab-achievements"), "achievements tab missing"); html.contains("tab-notifications"),
"notifications tab missing"
);
assert!(
html.contains("tab-achievements"),
"achievements tab missing"
);
} }
#[tokio::test] #[tokio::test]
@@ -272,5 +311,9 @@ async fn test_tls_cert_generation() {
async fn test_tls_acceptor_construction() { async fn test_tls_acceptor_construction() {
let (cert_pem, key_pem) = openfut_bridge::tls::generate_self_signed_cert().unwrap(); let (cert_pem, key_pem) = openfut_bridge::tls::generate_self_signed_cert().unwrap();
let acceptor = openfut_bridge::tls::make_tls_acceptor(&cert_pem, &key_pem); let acceptor = openfut_bridge::tls::make_tls_acceptor(&cert_pem, &key_pem);
assert!(acceptor.is_ok(), "acceptor construction failed: {:?}", acceptor.err()); assert!(
acceptor.is_ok(),
"acceptor construction failed: {:?}",
acceptor.err()
);
} }
+116
View File
@@ -136,9 +136,67 @@ fn main() {
let _ = CloseHandle(process); let _ = CloseHandle(process);
} }
} }
// read <hex-addr | module+0xoffset> [len] [pid|name]
// Dump raw bytes (hex + ASCII) at an address — to read short strings /
// data the disassembler doesn't resolve.
Some("read") => {
let arg = match args.get(1) {
Some(a) => a.clone(),
None => {
eprintln!("Usage: protossl-scan read <hex-addr | module+0xoffset> [len] [pid]");
std::process::exit(1);
}
};
let len = args
.get(2)
.and_then(|s| s.parse::<usize>().ok().or_else(|| parse_hex(s)))
.unwrap_or(64);
let pid = resolve_pid(args.get(3).map(|s| s.as_str()));
let process = open_for_read(pid);
let modules = enumerate_modules(pid);
let target = match resolve_target(&arg, &modules) {
Some(t) => t,
None => {
eprintln!("Could not resolve '{arg}'");
std::process::exit(1);
}
};
println!("== read {} len {len} ==", describe(target, &modules));
match read_bytes(process, target, len) {
Some(b) => {
for off in (0..b.len()).step_by(16) {
let row = &b[off..(off + 16).min(b.len())];
let hexp: String = row.iter().map(|x| format!("{x:02X} ")).collect();
let asc: String = row
.iter()
.map(|&x| if (0x20..=0x7e).contains(&x) { x as char } else { '.' })
.collect();
println!(" 0x{:016X} {:<48} {}", target + off, hexp, asc);
}
}
None => println!(" (could not read memory at that address)"),
}
unsafe {
let _ = CloseHandle(process);
}
}
// callers <hex-addr | module+0xoffset> [pid|name] // callers <hex-addr | module+0xoffset> [pid|name]
// Find direct call/jmp sites that target an address — walks up the call // Find direct call/jmp sites that target an address — walks up the call
// graph (e.g. from a connect helper to the code that gates it). // graph (e.g. from a connect helper to the code that gates it).
// jmpscan [module] [pid|name]
// Find E9 rel32 jumps inside a module whose target leaves the module —
// i.e. inline-detour entry points (MS Detours hooks). Default module:
// FIFA23.exe; targets reveal the detoured EbisuSDK functions.
Some("jmpscan") => {
let modname = args.get(1).cloned().unwrap_or_else(|| "FIFA23".to_string());
let pid = resolve_pid(args.get(2).map(|s| s.as_str()));
let process = open_for_read(pid);
let modules = enumerate_modules(pid);
run_jmpscan(process, &modules, &modname);
unsafe {
let _ = CloseHandle(process);
}
}
Some("callers") => { Some("callers") => {
let arg = match args.get(1) { let arg = match args.get(1) {
Some(a) => a.clone(), Some(a) => a.clone(),
@@ -427,6 +485,64 @@ fn dump_neighbours(process: HANDLE, at: usize, modules: &[ModuleInfo]) {
/// Scan app-module executable memory for near `call`/`jmp` (E8/E9 + rel32) /// Scan app-module executable memory for near `call`/`jmp` (E8/E9 + rel32)
/// instructions whose target is `target`. This walks UP the call graph — e.g. /// instructions whose target is `target`. This walks UP the call graph — e.g.
/// from a connect helper to the code that decides whether to call it. /// from a connect helper to the code that decides whether to call it.
/// Scan a module's executable memory for `E9 rel32` near-jumps whose target is
/// OUTSIDE the module — the signature of an inline detour (function entry patched
/// to jump to an external trampoline). Reports source -> target for each.
fn run_jmpscan(process: HANDLE, modules: &[ModuleInfo], modname: &str) {
let want = modname.to_ascii_lowercase();
let want = want.strip_suffix(".dll").unwrap_or(&want);
let want = want.strip_suffix(".exe").unwrap_or(want);
let m = match modules.iter().find(|m| {
let n = m.name.to_ascii_lowercase();
n.starts_with(want)
}) {
Some(m) => m,
None => {
println!("module '{modname}' not found");
return;
}
};
let base = m.base;
let end = m.base + m.size;
println!("== jmpscan {} [0x{base:X}..0x{end:X}] ==\n", m.name);
let allow = [(base, end)];
let mut hits: Vec<(usize, usize)> = Vec::new();
walk_regions(process, true, 4, Some(&allow), |chunk_base, bytes| {
if bytes.len() < 5 {
return;
}
for i in 1..=bytes.len() - 5 {
// Real detours patch a function entry, which MSVC pads with int3
// (0xCC) just before it. Requiring that preceding 0xCC filters out
// the flood of 0xE9 data bytes that aren't real instructions.
if bytes[i] != 0xE9 || bytes[i - 1] != 0xCC {
continue;
}
let rel = i32::from_le_bytes([bytes[i + 1], bytes[i + 2], bytes[i + 3], bytes[i + 4]]);
let src = chunk_base + i;
let tgt = (src + 5).wrapping_add(rel as i64 as usize);
if tgt < base || tgt >= end {
hits.push((src, tgt));
}
}
});
hits.sort_unstable();
hits.dedup();
if hits.is_empty() {
println!(" no out-of-module E9 jumps found");
return;
}
println!("-- {} out-of-module E9 jump(s) (detour entry candidates) --", hits.len());
for (src, tgt) in hits.iter().take(80) {
println!(" {} -> {}", describe(*src, modules), describe(*tgt, modules));
}
if hits.len() > 80 {
println!(" ... and {} more", hits.len() - 80);
}
}
fn run_callers(process: HANDLE, modules: &[ModuleInfo], target: usize) { fn run_callers(process: HANDLE, modules: &[ModuleInfo], target: usize) {
println!("== protossl-scan : callers of 0x{target:X} =="); println!("== protossl-scan : callers of 0x{target:X} ==");
println!("({})\n", describe(target, modules)); println!("({})\n", describe(target, modules));