diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4d39c66 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +target/ +**/target/ +*.db +*.db-shm +*.db-wal +.env +.env.local +Dockerfile +.dockerignore +.git +.gitignore +captures/ diff --git a/CLAUDE.md b/CLAUDE.md index 1ae256c..c6b2537 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,5 +1,7 @@ # 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 working session will give you ONE bounded task plus a verification clause; this file is the background that stays true across all of them. diff --git a/Cargo.toml b/Cargo.toml index f5eda60..d426f8d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition = "2021" authors = ["OpenFUT Contributors"] description = "FIFA 23 integration layer and reverse-engineering proxy" license = "MIT" -repository = "https://github.com/openfut/openfut-bridge" +repository = "https://git.aleshym.co/funman300/OpenFUT-Bridge.git" [lib] name = "openfut_bridge" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2758079 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/src/bin/replay.rs b/src/bin/replay.rs index 8997f90..187c119 100644 --- a/src/bin/replay.rs +++ b/src/bin/replay.rs @@ -59,9 +59,15 @@ async fn main() -> ExitCode { for capture in &captures { let result = replay_one(&client, bridge_url, capture).await; match result { - Ok(status) => println!(" [{}] {} {} → {status}", capture.id, capture.method, capture.path), + Ok(status) => println!( + " [{}] {} {} → {status}", + capture.id, capture.method, capture.path + ), Err(e) => { - eprintln!(" [{}] {} {} → ERROR: {e}", capture.id, capture.method, capture.path); + eprintln!( + " [{}] {} {} → ERROR: {e}", + capture.id, capture.method, capture.path + ); failures += 1; } } diff --git a/src/main.rs b/src/main.rs index 386974d..ea686a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -102,7 +102,7 @@ async fn serve_tls( let listener = tokio::net::TcpListener::bind(addr).await?; loop { - let (tcp, _peer) = listener.accept().await?; + let (tcp, peer) = listener.accept().await?; let acceptor = acceptor.clone(); let app = app.clone(); @@ -110,18 +110,21 @@ async fn serve_tls( let tls_stream = match acceptor.accept(tcp).await { Ok(s) => s, 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; } }; let io = TokioIo::new(tls_stream); - let svc = hyper::service::service_fn( - move |req: hyper::Request| { + let svc = + hyper::service::service_fn(move |req: hyper::Request| { let app = app.clone(); async move { app.oneshot(req.map(axum::body::Body::new)).await } - }, - ); + }); if let Err(e) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) .serve_connection(io, svc) diff --git a/src/mapper.rs b/src/mapper.rs index d9cb0a1..c7fc16b 100644 --- a/src/mapper.rs +++ b/src/mapper.rs @@ -22,363 +22,495 @@ struct ExactRoute { const EXACT: &[ExactRoute] = &[ // ── Auth ───────────────────────────────────────────────────────────────── ExactRoute { - ea_method: "POST", ea_path: "/ut/auth", - core_method: "POST", core_path: "/auth/local", + ea_method: "POST", + ea_path: "/ut/auth", + core_method: "POST", + core_path: "/auth/local", notes: "FUT login → Core local auth", }, // ── Profile / Settings ─────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/user/settings", - core_method: "GET", core_path: "/profile", + ea_method: "GET", + ea_path: "/ut/game/fut/user/settings", + core_method: "GET", + core_path: "/profile", notes: "FUT user settings → Core profile", }, ExactRoute { - ea_method: "PUT", ea_path: "/ut/game/fut/user/settings", - core_method: "PUT", core_path: "/settings", + ea_method: "PUT", + ea_path: "/ut/game/fut/user/settings", + core_method: "PUT", + core_path: "/settings", notes: "FUT update settings → Core settings", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/user/accountinfo", - core_method: "GET", core_path: "/profile", + ea_method: "GET", + ea_path: "/ut/game/fut/user/accountinfo", + core_method: "GET", + core_path: "/profile", notes: "FUT account info → Core profile", }, // ── Club / Mass info ────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/usermassinfo", - core_method: "GET", core_path: "/club", + ea_method: "GET", + ea_path: "/ut/game/fut/usermassinfo", + core_method: "GET", + core_path: "/club", notes: "FUT mass info (club + coins) → Core club", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/club", - core_method: "GET", core_path: "/club", + ea_method: "GET", + ea_path: "/ut/game/fut/club", + core_method: "GET", + core_path: "/club", notes: "FUT club info → Core club", }, ExactRoute { - ea_method: "PUT", ea_path: "/ut/game/fut/club", - core_method: "PUT", core_path: "/club", + ea_method: "PUT", + ea_path: "/ut/game/fut/club", + core_method: "PUT", + core_path: "/club", notes: "FUT update club → Core update club", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/club/stats", - core_method: "GET", core_path: "/statistics", + ea_method: "GET", + ea_path: "/ut/game/fut/club/stats", + core_method: "GET", + core_path: "/statistics", notes: "FUT club stats → Core statistics", }, // ── Cards / Collection ──────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/item", - core_method: "GET", core_path: "/collection", + ea_method: "GET", + ea_path: "/ut/game/fut/item", + core_method: "GET", + core_path: "/collection", notes: "FUT collection → Core owned cards", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/item/search", - core_method: "GET", core_path: "/cards", + ea_method: "GET", + ea_path: "/ut/game/fut/item/search", + core_method: "GET", + core_path: "/cards", notes: "FUT item search → Core card catalogue (query params forwarded)", }, // ── Squad ───────────────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/squad/active", - core_method: "GET", core_path: "/squad", + ea_method: "GET", + ea_path: "/ut/game/fut/squad/active", + core_method: "GET", + core_path: "/squad", notes: "FUT active squad → Core squad", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/squad/0", - core_method: "GET", core_path: "/squad", + ea_method: "GET", + ea_path: "/ut/game/fut/squad/0", + core_method: "GET", + core_path: "/squad", notes: "FUT squad by slot 0 → Core squad (first squad)", }, ExactRoute { - ea_method: "PUT", ea_path: "/ut/game/fut/squad/active", - core_method: "POST", core_path: "/squad", + ea_method: "PUT", + ea_path: "/ut/game/fut/squad/active", + core_method: "POST", + core_path: "/squad", notes: "FUT save squad → Core save squad", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/squad/chemistry", - core_method: "GET", core_path: "/squad", + ea_method: "GET", + ea_path: "/ut/game/fut/squad/chemistry", + core_method: "GET", + core_path: "/squad", notes: "FUT squad chemistry → Core squad (chemistry included in response)", }, // ── Packs ───────────────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/store/packdetails", - core_method: "GET", core_path: "/packs", + ea_method: "GET", + ea_path: "/ut/game/fut/store/packdetails", + core_method: "GET", + core_path: "/packs", notes: "FUT pack store → Core pack list", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/store/purchase", - core_method: "POST", core_path: "/packs/buy", + ea_method: "POST", + ea_path: "/ut/game/fut/store/purchase", + core_method: "POST", + core_path: "/packs/buy", notes: "FUT pack purchase → Core pack buy", }, // ── Transfer Market ─────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/transfermarket", - core_method: "GET", core_path: "/market", + ea_method: "GET", + ea_path: "/ut/game/fut/transfermarket", + core_method: "GET", + core_path: "/market", notes: "FUT transfer market search → Core NPC market", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/trade/bid", - core_method: "POST", core_path: "/market/buy", + ea_method: "POST", + ea_path: "/ut/game/fut/trade/bid", + core_method: "POST", + core_path: "/market/buy", notes: "FUT bid/buy now → Core market buy", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/trade/watchlist", - core_method: "GET", core_path: "/market", + ea_method: "GET", + ea_path: "/ut/game/fut/trade/watchlist", + core_method: "GET", + core_path: "/market", notes: "FUT watchlist → Core market (approximation)", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/trade/tradepile", - core_method: "GET", core_path: "/market/my-listings", + ea_method: "GET", + ea_path: "/ut/game/fut/trade/tradepile", + core_method: "GET", + core_path: "/market/my-listings", notes: "FUT trade pile (my listings) → Core my-listings", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/auctionhouse", - core_method: "POST", core_path: "/market/sell", + ea_method: "POST", + ea_path: "/ut/game/fut/auctionhouse", + core_method: "POST", + core_path: "/market/sell", notes: "FUT list card on AH → Core sell card", }, // ── Objectives ──────────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/objectives", - core_method: "GET", core_path: "/objectives", + ea_method: "GET", + ea_path: "/ut/game/fut/objectives", + core_method: "GET", + core_path: "/objectives", notes: "FUT objectives → Core objectives list", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/objectives/claim", - core_method: "POST", core_path: "/objectives/claim", + ea_method: "POST", + ea_path: "/ut/game/fut/objectives/claim", + core_method: "POST", + core_path: "/objectives/claim", notes: "FUT claim objective → Core claim", }, // ── Events ──────────────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/events", - core_method: "GET", core_path: "/events", + ea_method: "GET", + ea_path: "/ut/game/fut/events", + core_method: "GET", + core_path: "/events", notes: "FUT events → Core events list", }, // ── Matches / Squad Battles ─────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/squadbattle/opponent", - core_method: "GET", core_path: "/matches/opponent", + ea_method: "GET", + ea_path: "/ut/game/fut/squadbattle/opponent", + core_method: "GET", + core_path: "/matches/opponent", notes: "Squad battles opponent → Core match opponent generator", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/result", - core_method: "POST", core_path: "/matches/result", + ea_method: "POST", + ea_path: "/ut/game/fut/result", + core_method: "POST", + core_path: "/matches/result", notes: "FUT match result submit → Core match result", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/matches", - core_method: "GET", core_path: "/matches", + ea_method: "GET", + ea_path: "/ut/game/fut/matches", + core_method: "GET", + core_path: "/matches", notes: "FUT match history → Core match list", }, // ── SBC ────────────────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/sbc", - core_method: "GET", core_path: "/sbc", + ea_method: "GET", + ea_path: "/ut/game/fut/sbc", + core_method: "GET", + core_path: "/sbc", notes: "FUT SBC list → Core SBC list", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/sbc/challenges", - core_method: "GET", core_path: "/sbc", + ea_method: "GET", + ea_path: "/ut/game/fut/sbc/challenges", + core_method: "GET", + core_path: "/sbc", notes: "FUT SBC challenges → Core SBC list", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/sbc/submission", - core_method: "POST", core_path: "/sbc/submit", + ea_method: "POST", + ea_path: "/ut/game/fut/sbc/submission", + core_method: "POST", + core_path: "/sbc/submit", notes: "FUT SBC submission → Core SBC submit", }, // ── Draft ───────────────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/draft", - core_method: "GET", core_path: "/draft/squad", + ea_method: "GET", + ea_path: "/ut/game/fut/draft", + core_method: "GET", + core_path: "/draft/squad", notes: "FUT draft view → Core draft squad", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/draft/new", - core_method: "POST", core_path: "/draft/start", + ea_method: "POST", + ea_path: "/ut/game/fut/draft/new", + core_method: "POST", + core_path: "/draft/start", notes: "FUT new draft → Core draft start", }, // ── Division / Season ───────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/division/rivals", - core_method: "GET", core_path: "/division", + ea_method: "GET", + ea_path: "/ut/game/fut/division/rivals", + core_method: "GET", + core_path: "/division", notes: "FUT Division Rivals status → Core division", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/division/rivals/claim", - core_method: "POST", core_path: "/rivals/claim-weekly", + ea_method: "POST", + ea_path: "/ut/game/fut/division/rivals/claim", + core_method: "POST", + core_path: "/rivals/claim-weekly", notes: "FUT rivals weekly claim → Core rivals claim", }, // ── FUT Champions ───────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/champs", - core_method: "GET", core_path: "/fut-champs", + ea_method: "GET", + ea_path: "/ut/game/fut/champs", + core_method: "GET", + core_path: "/fut-champs", notes: "FUT Champions status → Core fut-champs", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/champs/start", - core_method: "POST", core_path: "/fut-champs/start", + ea_method: "POST", + ea_path: "/ut/game/fut/champs/start", + core_method: "POST", + core_path: "/fut-champs/start", notes: "FUT Champions start week → Core fut-champs start", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/champs/history", - core_method: "GET", core_path: "/fut-champs/history", + ea_method: "GET", + ea_path: "/ut/game/fut/champs/history", + core_method: "GET", + core_path: "/fut-champs/history", notes: "FUT Champions history → Core history", }, // ── Card Upgrades ───────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/chemistry", - core_method: "GET", core_path: "/chemistry-styles", + ea_method: "GET", + ea_path: "/ut/game/fut/chemistry", + core_method: "GET", + core_path: "/chemistry-styles", notes: "FUT chemistry styles → Core chemistry styles list", }, // ── Notifications ───────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/notification", - core_method: "GET", core_path: "/notifications", + ea_method: "GET", + ea_path: "/ut/game/fut/notification", + core_method: "GET", + core_path: "/notifications", notes: "FUT notifications → Core notifications", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/notifications", - core_method: "GET", core_path: "/notifications", + ea_method: "GET", + ea_path: "/ut/game/fut/notifications", + core_method: "GET", + core_path: "/notifications", notes: "FUT notifications (plural form) → Core notifications", }, // ── Squad list ──────────────────────────────────────────────────────────── // ── Division / Season history / Leaderboard ─────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/division/history", - core_method: "GET", core_path: "/division/history", + ea_method: "GET", + ea_path: "/ut/game/fut/division/history", + core_method: "GET", + core_path: "/division/history", notes: "FUT division history → Core season history", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/division/leaderboard", - core_method: "GET", core_path: "/division/leaderboard", + ea_method: "GET", + ea_path: "/ut/game/fut/division/leaderboard", + core_method: "GET", + core_path: "/division/leaderboard", notes: "FUT division leaderboard → Core seeded NPC leaderboard", }, // ── Market trade history ────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/trade/history", - core_method: "GET", core_path: "/market/trade-history", + ea_method: "GET", + ea_path: "/ut/game/fut/trade/history", + core_method: "GET", + core_path: "/market/trade-history", notes: "FUT trade history → Core market trade history", }, // ── Daily check-in ──────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/dailyObjective", - core_method: "GET", core_path: "/club/checkin", + ea_method: "GET", + ea_path: "/ut/game/fut/dailyObjective", + core_method: "GET", + core_path: "/club/checkin", notes: "FUT daily objective status → Core check-in status", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/dailyObjective/claim", - core_method: "POST", core_path: "/club/checkin", + ea_method: "POST", + ea_path: "/ut/game/fut/dailyObjective/claim", + core_method: "POST", + core_path: "/club/checkin", notes: "FUT daily objective claim → Core check-in claim", }, // ── Club milestones ─────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/milestones", - core_method: "GET", core_path: "/club/milestones", + ea_method: "GET", + ea_path: "/ut/game/fut/milestones", + core_method: "GET", + core_path: "/club/milestones", notes: "FUT milestones → Core club milestones", }, // ── Squad list ──────────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/squad/list", - core_method: "GET", core_path: "/squads", + ea_method: "GET", + ea_path: "/ut/game/fut/squad/list", + core_method: "GET", + core_path: "/squads", notes: "FUT squad list → Core all squads", }, ExactRoute { - ea_method: "DELETE", ea_path: "/ut/game/fut/squad/active", - core_method: "DELETE", core_path: "/squad", + ea_method: "DELETE", + ea_path: "/ut/game/fut/squad/active", + core_method: "DELETE", + core_path: "/squad", notes: "FUT delete active squad → Core delete squad (best-effort)", }, // ── Achievements / Trophies ─────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/trophies", - core_method: "GET", core_path: "/achievements", + ea_method: "GET", + ea_path: "/ut/game/fut/trophies", + core_method: "GET", + core_path: "/achievements", notes: "FUT trophies → Core achievements", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/trophy", - core_method: "GET", core_path: "/achievements", + ea_method: "GET", + ea_path: "/ut/game/fut/trophy", + core_method: "GET", + core_path: "/achievements", notes: "FUT trophy (singular) → Core achievements", }, // ── Rivals extra endpoints ──────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/rivals/rank", - core_method: "GET", core_path: "/division", + ea_method: "GET", + ea_path: "/ut/game/fut/rivals/rank", + core_method: "GET", + core_path: "/division", notes: "FUT rivals rank → Core division", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/rivals/result", - core_method: "POST", core_path: "/matches/result", + ea_method: "POST", + ea_path: "/ut/game/fut/rivals/result", + core_method: "POST", + core_path: "/matches/result", notes: "FUT rivals match result → Core match result", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/rivals/leaderboard", - core_method: "GET", core_path: "/statistics", + ea_method: "GET", + ea_path: "/ut/game/fut/rivals/leaderboard", + core_method: "GET", + core_path: "/statistics", notes: "FUT rivals leaderboard → Core statistics (offline approximation)", }, // ── Objectives sub-groups ───────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/objectives/group", - core_method: "GET", core_path: "/objectives", + ea_method: "GET", + ea_path: "/ut/game/fut/objectives/group", + core_method: "GET", + core_path: "/objectives", notes: "FUT objectives group → Core objectives", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/objectives/daily", - core_method: "GET", core_path: "/objectives", + ea_method: "GET", + ea_path: "/ut/game/fut/objectives/daily", + core_method: "GET", + core_path: "/objectives", notes: "FUT daily objectives → Core objectives", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/objectives/weekly", - core_method: "GET", core_path: "/objectives", + ea_method: "GET", + ea_path: "/ut/game/fut/objectives/weekly", + core_method: "GET", + core_path: "/objectives", notes: "FUT weekly objectives → Core objectives", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/objectives/group/claim", - core_method: "POST", core_path: "/objectives/claim", + ea_method: "POST", + ea_path: "/ut/game/fut/objectives/group/claim", + core_method: "POST", + core_path: "/objectives/claim", notes: "FUT objectives group claim → Core objectives claim", }, // ── Catalogue / Card search ─────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/catalogue/item", - core_method: "GET", core_path: "/cards", + ea_method: "GET", + ea_path: "/ut/game/fut/catalogue/item", + core_method: "GET", + core_path: "/cards", notes: "FUT catalogue items → Core card catalogue", }, ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/catalogue", - core_method: "GET", core_path: "/cards", + ea_method: "GET", + ea_path: "/ut/game/fut/catalogue", + core_method: "GET", + core_path: "/cards", notes: "FUT catalogue → Core card catalogue", }, // ── Store / Pricing ─────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/store/pricetiers", - core_method: "GET", core_path: "/packs/store", + ea_method: "GET", + ea_path: "/ut/game/fut/store/pricetiers", + core_method: "GET", + core_path: "/packs/store", notes: "FUT price tiers → Core pack store definitions", }, // ── Consumables / Chemistry / Fitness ───────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/consumables", - core_method: "GET", core_path: "/chemistry-styles", + ea_method: "GET", + ea_path: "/ut/game/fut/consumables", + core_method: "GET", + core_path: "/chemistry-styles", notes: "FUT consumables → Core chemistry styles (approximation)", }, ExactRoute { - ea_method: "POST", ea_path: "/ut/game/fut/fitness", - core_method: "GET", core_path: "/club", + ea_method: "POST", + ea_path: "/ut/game/fut/fitness", + core_method: "GET", + core_path: "/club", notes: "FUT fitness apply → Core club (placeholder, fitness not tracked)", }, // ── Loan items ──────────────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/loanitems", - core_method: "GET", core_path: "/collection", + ea_method: "GET", + ea_path: "/ut/game/fut/loanitems", + core_method: "GET", + core_path: "/collection", notes: "FUT loan items → Core collection (client filters is_loan)", }, // ── Customization / Kit ─────────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/customization", - core_method: "GET", core_path: "/settings", + ea_method: "GET", + ea_path: "/ut/game/fut/customization", + core_method: "GET", + core_path: "/settings", notes: "FUT customization → Core settings", }, ExactRoute { - ea_method: "PUT", ea_path: "/ut/game/fut/customization", - core_method: "PUT", core_path: "/settings", + ea_method: "PUT", + ea_path: "/ut/game/fut/customization", + core_method: "PUT", + core_path: "/settings", notes: "FUT save customization → Core save settings", }, // ── Active messages / MOTD ──────────────────────────────────────────────── ExactRoute { - ea_method: "GET", ea_path: "/ut/game/fut/activeMessage", - core_method: "GET", core_path: "/notifications", + ea_method: "GET", + ea_path: "/ut/game/fut/activeMessage", + core_method: "GET", + core_path: "/notifications", notes: "FUT active messages → Core notifications (mapped to nearest equivalent)", }, ]; @@ -732,7 +864,11 @@ mod tests { #[test] 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 ───────────────────────────────────────────────── diff --git a/src/proxy.rs b/src/proxy.rs index 2901776..07c8288 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -107,8 +107,13 @@ pub async fn catch_all_handler( .unwrap_or_default() ); - let mut capture = - CapturedRequest::new(&method, &path, query.as_deref(), headers.clone(), body_str.clone()); + let mut capture = CapturedRequest::new( + &method, + &path, + query.as_deref(), + headers.clone(), + body_str.clone(), + ); let (response_body, status_code): (Value, u16) = if let Some(mapping) = map_to_core(&method, &path) { diff --git a/src/routes/admin.rs b/src/routes/admin.rs index 794f55b..2db5670 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -158,10 +158,12 @@ pub async fn get_capture_diff( captures.iter().find(|c| c.id == id) }; - let cap_a = find(¶ms.a) - .ok_or_else(|| BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.a)))?; - let cap_b = find(¶ms.b) - .ok_or_else(|| BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.b)))?; + let cap_a = find(¶ms.a).ok_or_else(|| { + BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.a)) + })?; + let cap_b = find(¶ms.b).ok_or_else(|| { + BridgeError::Internal(anyhow::anyhow!("capture '{}' not found", params.b)) + })?; // Structural JSON diff of request bodies 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 .iter() .filter_map(|(k, va)| { - cap_b.headers.iter().find(|(kb, _)| kb == k).and_then(|(_, vb)| { - if va != vb { - Some(json!({ "header": k, "a": va, "b": vb })) - } else { - None - } - }) + cap_b + .headers + .iter() + .find(|(kb, _)| kb == k) + .and_then(|(_, vb)| { + if va != vb { + Some(json!({ "header": k, "a": va, "b": vb })) + } else { + None + } + }) }) .collect(); @@ -226,10 +232,8 @@ fn diff_json_strings(a: Option<&str>, b: Option<&str>) -> Value { return json!({ "same": true }); } // Try to parse as JSON objects for a structured diff - let va: Option> = - serde_json::from_str(a_str).ok(); - let vb: Option> = - serde_json::from_str(b_str).ok(); + let va: Option> = serde_json::from_str(a_str).ok(); + let vb: Option> = serde_json::from_str(b_str).ok(); match (va, vb) { (Some(ma), Some(mb)) => { @@ -282,8 +286,7 @@ pub async fn post_replay_capture( State(state): State, Path(capture_id): Path, ) -> BridgeResult> { - let captures = - load_all_captures(&state.config.captures_dir).map_err(BridgeError::Internal)?; + let captures = load_all_captures(&state.config.captures_dir).map_err(BridgeError::Internal)?; let capture = captures .iter() @@ -305,10 +308,7 @@ pub async fn post_replay_capture( ) .await { - Ok((body, status)) => ( - status, - crate::shaper::shape_response(&m.core_path, body), - ), + Ok((body, status)) => (status, crate::shaper::shape_response(&m.core_path, body)), Err(e) => ( 502u16, json!({ "error": format!("core forwarding failed: {e}") }), diff --git a/src/routes/health.rs b/src/routes/health.rs index 50aae9a..46ad8bc 100644 --- a/src/routes/health.rs +++ b/src/routes/health.rs @@ -69,7 +69,12 @@ pub async fn get_guide(State(state): State) -> Html { .unwrap_or("localhost"); let tls = state.config.tls_enabled; 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 core_url = &state.config.core_url; diff --git a/src/shaper.rs b/src/shaper.rs index 7d4c21b..d4c8172 100644 --- a/src/shaper.rs +++ b/src/shaper.rs @@ -157,10 +157,7 @@ fn shape_squad_response(core: Value) -> Value { /// Wraps Core market response in the FUT transfer market envelope. fn shape_market_response(core: Value) -> Value { let listings = core.get("listings").cloned().unwrap_or(json!([])); - let total = core - .get("total") - .and_then(|v| v.as_u64()) - .unwrap_or(0); + let total = core.get("total").and_then(|v| v.as_u64()).unwrap_or(0); json!({ "auctionInfo": listings, diff --git a/tests/proxy_test.rs b/tests/proxy_test.rs index a2df291..4af1bce 100644 --- a/tests/proxy_test.rs +++ b/tests/proxy_test.rs @@ -1,3 +1,4 @@ +use axum::routing::{any, delete, get, post}; use axum::{ body::Body, http::{Request, StatusCode}, @@ -9,7 +10,6 @@ use openfut_bridge::{ proxy::ProxyState, routes, }; -use axum::routing::{any, delete, get, post}; use tower::ServiceExt; // ── Unit tests ──────────────────────────────────────────────────────────────── @@ -96,9 +96,15 @@ fn build_test_app() -> axum::Router { .route("/_bridge/dashboard", get(routes::health::get_dashboard)) .route("/_bridge/captures", get(routes::admin::get_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/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)) .with_state(state) } @@ -107,11 +113,18 @@ fn build_test_app() -> axum::Router { async fn test_bridge_health_endpoint() { let app = build_test_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 .unwrap(); 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(); assert_eq!(json["status"], "ok"); assert_eq!(json["service"], "openfut-bridge"); @@ -131,7 +144,9 @@ async fn test_bridge_placeholder_mode_returns_ok() { .await .unwrap(); 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(); assert_eq!(json["status"], "ok"); assert!(json["openfut_note"].is_string()); @@ -150,7 +165,9 @@ async fn test_bridge_captures_endpoint_returns_list() { .await .unwrap(); 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(); assert!(json["captures"].is_array()); assert!(json["total"].is_number()); @@ -170,7 +187,9 @@ async fn test_bridge_delete_captures() { .await .unwrap(); 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(); assert!(json["deleted"].is_number()); } @@ -188,7 +207,9 @@ async fn test_bridge_status_endpoint() { .await .unwrap(); 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(); assert!(json["endpoints"].is_array()); } @@ -208,15 +229,25 @@ async fn test_dashboard_returns_html() { .await .unwrap(); 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}"); - 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(); assert!(html.contains("OpenFUT Dashboard"), "title missing"); assert!(html.contains("const CORE ="), "core URL injection missing"); // Placeholder URL injected in test mode 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] @@ -231,7 +262,9 @@ async fn test_dashboard_contains_key_sections() { ) .await .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(); // Verify all major tab sections are present 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-catalog"), "card catalog tab missing"); assert!(html.contains("tab-settings"), "settings tab missing"); - assert!(html.contains("tab-notifications"), "notifications tab missing"); - assert!(html.contains("tab-achievements"), "achievements tab missing"); + assert!( + html.contains("tab-notifications"), + "notifications tab missing" + ); + assert!( + html.contains("tab-achievements"), + "achievements tab missing" + ); } #[tokio::test] @@ -272,5 +311,9 @@ async fn test_tls_cert_generation() { async fn test_tls_acceptor_construction() { 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); - assert!(acceptor.is_ok(), "acceptor construction failed: {:?}", acceptor.err()); + assert!( + acceptor.is_ok(), + "acceptor construction failed: {:?}", + acceptor.err() + ); }