diff --git a/src/web/http/decoy.rs b/src/web/http/decoy.rs index 3d194ea..4fbe5e2 100644 --- a/src/web/http/decoy.rs +++ b/src/web/http/decoy.rs @@ -17,6 +17,7 @@ use crate::config::{WebRuntimeDecoy, WebRuntimeVhost}; use crate::web::manager::WebProcessRuntime; /// Serves the configured ordinary site after optionally removing carrier material. +/// Transport-sanitized static fallbacks remain uncacheable after query removal. pub(super) async fn serve_decoy( mut request: Request, vhost: Arc, @@ -41,7 +42,15 @@ where }; let request = Request::from_parts(parts, body); match &vhost.decoy { - WebRuntimeDecoy::StaticDirectory(site) => serve_static(request, site), + WebRuntimeDecoy::StaticDirectory(site) => { + let mut response = serve_static(request, site); + if sanitize_transport { + response + .headers_mut() + .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store")); + } + response + } WebRuntimeDecoy::HttpUpstream { addr, authority } => { proxy_to_upstream( request, diff --git a/src/web/http/tests.rs b/src/web/http/tests.rs index 9b19fd8..7ed5f02 100644 --- a/src/web/http/tests.rs +++ b/src/web/http/tests.rs @@ -209,6 +209,36 @@ async fn https_carrier_bootstraps_and_closes_one_session() { replacement.stop_background_tasks().await; } +#[tokio::test] +async fn rejected_bridge_bootstrap_falls_back_to_uncacheable_static_index() { + let capability = [15u8; 32]; + let generation = test_runtime_generation(1, runtime_config(capability, WebCarrier::Https)); + let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation))); + let runtime = WebProcessRuntime::start(active_runtime); + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability); + let bridge_request = || { + format!( + "GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nConnection: close\r\n\r\n" + ) + .into_bytes() + }; + + let first_response = request(&listener, &runtime, bridge_request()).await; + let (_, first_body) = split_response(&first_response); + assert!(first_body.windows(11).any(|value| value == b"bootstrap='")); + + let fallback_response = request(&listener, &runtime, bridge_request()).await; + let (fallback_headers, fallback_body) = split_response(&fallback_response); + assert!(fallback_headers.starts_with(b"HTTP/1.1 200")); + assert_eq!(response_header(fallback_headers, "cache-control"), "no-store"); + assert_eq!(fallback_body, b"decoy"); + + runtime.shutdown().await; + generation.stop_sessions().await; + generation.stop_background_tasks().await; +} + #[tokio::test] async fn bootstrap_survives_client_address_family_change() { let capability = [8u8; 32];