Initial commit: OpenFUT Bridge
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>
This commit is contained in:
+154
@@ -0,0 +1,154 @@
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::{Request, State},
|
||||
http::StatusCode,
|
||||
response::Response,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use serde_json::Value;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::{
|
||||
capture::{save_capture, CapturedRequest},
|
||||
config::Config,
|
||||
error::BridgeResult,
|
||||
mapper::{map_to_core, placeholder_response},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ProxyState {
|
||||
pub config: Arc<Config>,
|
||||
pub http_client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl ProxyState {
|
||||
pub fn new(config: Config) -> Self {
|
||||
Self {
|
||||
config: Arc::new(config),
|
||||
http_client: reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.expect("failed to build HTTP client"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn catch_all_handler(
|
||||
State(state): State<ProxyState>,
|
||||
req: Request,
|
||||
) -> BridgeResult<Response<Body>> {
|
||||
let method = req.method().to_string();
|
||||
let uri = req.uri().clone();
|
||||
let path = uri.path().to_string();
|
||||
let query = uri.query().map(String::from);
|
||||
|
||||
let headers: Vec<(String, String)> = req
|
||||
.headers()
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("<binary>").to_string()))
|
||||
.collect();
|
||||
|
||||
// Extract body via axum's built-in mechanism
|
||||
let (_parts, body) = req.into_parts();
|
||||
let body_bytes: Bytes = axum::body::to_bytes(body, 1024 * 1024)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let body_str = if body_bytes.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(String::from_utf8_lossy(&body_bytes).to_string())
|
||||
};
|
||||
|
||||
tracing::info!(
|
||||
"→ {} {}{}",
|
||||
method,
|
||||
path,
|
||||
query
|
||||
.as_deref()
|
||||
.map(|q| format!("?{q}"))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
|
||||
let mut capture =
|
||||
CapturedRequest::new(&method, &path, query.as_deref(), headers, body_str.clone());
|
||||
|
||||
let (response_body, status_code): (Value, u16) =
|
||||
if let Some(mapping) = map_to_core(&method, &path) {
|
||||
tracing::info!(
|
||||
" ↳ Mapped to Core: {} {}",
|
||||
mapping.method,
|
||||
mapping.core_path
|
||||
);
|
||||
capture.mapped_to_core = Some(mapping.core_path.to_string());
|
||||
|
||||
match forward_to_core(
|
||||
&state,
|
||||
mapping.method,
|
||||
mapping.core_path,
|
||||
body_str.as_deref(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((body, status)) => (body, status),
|
||||
Err(e) => {
|
||||
tracing::error!("Core request failed: {e}");
|
||||
(serde_json::json!({ "error": e.to_string() }), 502)
|
||||
}
|
||||
}
|
||||
} else if state.config.placeholder_mode {
|
||||
(placeholder_response(&method, &path), 200)
|
||||
} else {
|
||||
(serde_json::json!({ "error": "endpoint not mapped" }), 404)
|
||||
};
|
||||
|
||||
capture = capture.with_response(status_code, Some(response_body.to_string()));
|
||||
|
||||
let captures_dir = state.config.captures_dir.clone();
|
||||
let capture_clone = capture.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = save_capture(&captures_dir, &capture_clone) {
|
||||
tracing::warn!("Failed to save capture: {e}");
|
||||
}
|
||||
});
|
||||
|
||||
let status = StatusCode::from_u16(status_code).unwrap_or(StatusCode::OK);
|
||||
let json_bytes =
|
||||
serde_json::to_vec(&response_body).map_err(crate::error::BridgeError::Serialization)?;
|
||||
|
||||
let response = Response::builder()
|
||||
.status(status)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(json_bytes))
|
||||
.map_err(|e| anyhow::anyhow!("response build error: {e}"))?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn forward_to_core(
|
||||
state: &ProxyState,
|
||||
method: &str,
|
||||
path: &str,
|
||||
body: Option<&str>,
|
||||
) -> anyhow::Result<(Value, u16)> {
|
||||
let url = format!("{}{}", state.config.core_url, path);
|
||||
let builder = match method {
|
||||
"POST" => state.http_client.post(&url),
|
||||
"PUT" => state.http_client.put(&url),
|
||||
"DELETE" => state.http_client.delete(&url),
|
||||
_ => state.http_client.get(&url),
|
||||
};
|
||||
|
||||
let builder = if let Some(b) = body {
|
||||
builder
|
||||
.header("content-type", "application/json")
|
||||
.body(b.to_string())
|
||||
} else {
|
||||
builder
|
||||
};
|
||||
|
||||
let resp = builder.send().await?;
|
||||
let status = resp.status().as_u16();
|
||||
let body: Value = resp.json().await.unwrap_or(Value::Null);
|
||||
Ok((body, status))
|
||||
}
|
||||
Reference in New Issue
Block a user