Files
OpenFUT-Bridge/src/bin/replay.rs
T
funman300 3b7a4928c9 feat: Phase 6 — proxy polish, TLS, admin UI, replay CLI
## TLS (#11)
- Self-signed cert generated at startup via rcgen (covers localhost, 127.0.0.1,
  fut.ea.com, utas.mob.v4.fut.ea.com); activated with TLS_ENABLED=true
- Custom accept loop: tokio-rustls acceptor → hyper-util auto Builder → axum
  Router (no axum-server dependency — uses hyper 1.x natively)

## Replay CLI (#12)
- New binary: openfut-bridge-replay <file.json|dir> [bridge-url]
- Replays single capture or entire directory against Bridge
- Accepts self-signed certs automatically

## Capture quality (#13, #14)
- DELETE /_bridge/captures — wipe all capture files from disk
- Deduplication: same method+path within 1 s is skipped (configurable constant)

## Admin UI (#21, #22, #23)
- GET /_bridge/admin — embedded HTML dashboard; auto-refresh every 10 s
  Shows: live stats, SSE log of incoming traffic, endpoint status table,
  recent captures list with delete button
- GET /_bridge/status — per-endpoint mapped/known/unknown status
- GET /_bridge/captures/stream — SSE stream; event: capture on each request
  Uses tokio::sync::broadcast channel (capacity 256) in ProxyState

## Tests (#24, #25)
- 9 new tests: placeholder format, full HTTP integration (health, placeholder,
  captures list, delete captures, status), TLS cert generation + acceptor build
- Total bridge tests: 13/13 passing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 16:15:48 -07:00

114 lines
3.3 KiB
Rust

//! openfut-bridge-replay — replay a captured request file against the bridge.
//!
//! Usage:
//! openfut-bridge-replay \<capture.json\> [bridge-url]
//! openfut-bridge-replay captures/ (replay all captures in a dir)
//!
//! The bridge URL defaults to `http://127.0.0.1:8443`.
//! Self-signed TLS certificates are accepted automatically.
use openfut_bridge::capture::CapturedRequest;
use std::process::ExitCode;
#[tokio::main]
async fn main() -> ExitCode {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("Usage: openfut-bridge-replay <capture.json|dir> [bridge-url]");
eprintln!(" bridge-url defaults to http://127.0.0.1:8443");
return ExitCode::FAILURE;
}
let path_arg = &args[1];
let bridge_url = args
.get(2)
.map(|s| s.as_str())
.unwrap_or("http://127.0.0.1:8443");
let client = match reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(10))
.build()
{
Ok(c) => c,
Err(e) => {
eprintln!("Failed to build HTTP client: {e}");
return ExitCode::FAILURE;
}
};
let captures = match collect_captures(path_arg) {
Ok(c) => c,
Err(e) => {
eprintln!("Error loading captures: {e}");
return ExitCode::FAILURE;
}
};
if captures.is_empty() {
eprintln!("No capture files found at {path_arg}");
return ExitCode::FAILURE;
}
println!(
"Replaying {} capture(s) against {bridge_url}",
captures.len()
);
let mut failures = 0u32;
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),
Err(e) => {
eprintln!(" [{}] {} {} → ERROR: {e}", capture.id, capture.method, capture.path);
failures += 1;
}
}
}
if failures > 0 {
eprintln!("{failures} replay(s) failed.");
ExitCode::FAILURE
} else {
println!("All replays succeeded.");
ExitCode::SUCCESS
}
}
fn collect_captures(path_arg: &str) -> anyhow::Result<Vec<CapturedRequest>> {
let path = std::path::Path::new(path_arg);
if path.is_dir() {
openfut_bridge::capture::load_all_captures(path_arg)
} else {
let content = std::fs::read_to_string(path)?;
let capture: CapturedRequest = serde_json::from_str(&content)?;
Ok(vec![capture])
}
}
async fn replay_one(
client: &reqwest::Client,
bridge_url: &str,
capture: &CapturedRequest,
) -> anyhow::Result<u16> {
let url = format!("{}{}", bridge_url, capture.path);
let builder = match capture.method.to_uppercase().as_str() {
"POST" => client.post(&url),
"PUT" => client.put(&url),
"DELETE" => client.delete(&url),
_ => client.get(&url),
};
let builder = if let Some(body) = &capture.body {
builder
.header("content-type", "application/json")
.body(body.clone())
} else {
builder
};
let resp = builder.send().await?;
Ok(resp.status().as_u16())
}