a826e5f7d3
FIFA 23 reverse-engineering proxy and integration scaffold. - Catch-all HTTP proxy that captures all incoming FIFA 23 traffic - Known-route mapper (speculative FUT paths → Core API calls) - Placeholder JSON responses for unmapped endpoints - Admin endpoints: GET /_bridge/captures, GET /_bridge/unknown - Capture persistence to captures/*.json for RE analysis - 4 unit tests passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
92 lines
2.5 KiB
Rust
92 lines
2.5 KiB
Rust
use chrono::Utc;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::path::Path;
|
|
use uuid::Uuid;
|
|
|
|
/// A captured HTTP request from the FIFA 23 client.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct CapturedRequest {
|
|
pub id: String,
|
|
pub timestamp: String,
|
|
pub method: String,
|
|
pub path: String,
|
|
pub query: Option<String>,
|
|
pub headers: Vec<(String, String)>,
|
|
pub body: Option<String>,
|
|
pub response_status: Option<u16>,
|
|
pub response_body: Option<String>,
|
|
pub mapped_to_core: Option<String>,
|
|
}
|
|
|
|
impl CapturedRequest {
|
|
pub fn new(
|
|
method: &str,
|
|
path: &str,
|
|
query: Option<&str>,
|
|
headers: Vec<(String, String)>,
|
|
body: Option<String>,
|
|
) -> Self {
|
|
Self {
|
|
id: Uuid::new_v4().to_string(),
|
|
timestamp: Utc::now().to_rfc3339(),
|
|
method: method.to_string(),
|
|
path: path.to_string(),
|
|
query: query.map(String::from),
|
|
headers,
|
|
body,
|
|
response_status: None,
|
|
response_body: None,
|
|
mapped_to_core: None,
|
|
}
|
|
}
|
|
|
|
pub fn with_response(mut self, status: u16, body: Option<String>) -> Self {
|
|
self.response_status = Some(status);
|
|
self.response_body = body;
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Persist a capture to disk as JSON.
|
|
pub fn save_capture(captures_dir: &str, capture: &CapturedRequest) -> anyhow::Result<()> {
|
|
let dir = Path::new(captures_dir);
|
|
std::fs::create_dir_all(dir)?;
|
|
|
|
let filename = format!(
|
|
"{}_{}_{}.json",
|
|
capture.timestamp.replace(':', "-"),
|
|
capture.method,
|
|
capture.id
|
|
);
|
|
let path = dir.join(filename);
|
|
|
|
let json = serde_json::to_string_pretty(capture)?;
|
|
std::fs::write(&path, json)?;
|
|
|
|
tracing::debug!("Capture saved: {:?}", path);
|
|
Ok(())
|
|
}
|
|
|
|
/// Load all captures from the captures directory.
|
|
pub fn load_all_captures(captures_dir: &str) -> anyhow::Result<Vec<CapturedRequest>> {
|
|
let dir = Path::new(captures_dir);
|
|
if !dir.exists() {
|
|
return Ok(vec![]);
|
|
}
|
|
|
|
let mut captures = Vec::new();
|
|
for entry in std::fs::read_dir(dir)? {
|
|
let entry = entry?;
|
|
let path = entry.path();
|
|
if path.extension().map(|e| e == "json").unwrap_or(false) {
|
|
let content = std::fs::read_to_string(&path)?;
|
|
if let Ok(capture) = serde_json::from_str::<CapturedRequest>(&content) {
|
|
captures.push(capture);
|
|
}
|
|
}
|
|
}
|
|
|
|
captures.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
|
|
Ok(captures)
|
|
}
|