use std::convert::Infallible; use std::error::Error; use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; use bytes::Bytes; use http_body_util::BodyExt; use http_body_util::combinators::UnsyncBoxBody; use hyper::header::{self, HeaderName, HeaderValue}; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::{Method, Request, Response, StatusCode}; use hyper_util::rt::{TokioIo, TokioTimer}; use ipnetwork::IpNetwork; use tokio::net::TcpStream; use tokio_util::sync::CancellationToken; use crate::config::{WebClientIpSource, WebDecoyFastTrackMode, WebRuntimeVhost}; use crate::maestro::generation::RuntimeGeneration; use crate::web::bridge; use crate::web::manager::{ManagerError, WebProcessRuntime}; use crate::web::telemetry::WebDecoyFastTrackDisposition; // Response-body activity keeps connection idle accounting lifecycle-correct. mod activity; // Body collection retains allocation permits through request processing. mod body; // Canonical capability parsing and complete scans remain isolated from HTTP routing. mod capability; // Authentic credential containment stays independent from carrier routing. mod secrets; // Decoy routing and upstream proxying are isolated from carrier authentication. mod decoy; // Authenticated generated-bridge diagnostics remain outside carrier framing. mod diagnostic; // Downlink long-poll handling remains isolated from request routing. mod down; // Canonical request parsing rejects ambiguous credentials before routing. mod request; // Positive-only recovery representation stays separate from ordinary bridge rendering. mod recovery; // Carrier response construction and lane-header helpers are shared by handlers. mod response; // Session creation and replacement negotiation remain separate from request routing. mod session; #[cfg(test)] mod tests; // RFC 6455 upgrade validation and carrier drivers remain isolated from HTTP routing. mod websocket; // Enabled-debug integration coverage remains separate from carrier behavior tests. #[cfg(test)] #[path = "http/trace_tests.rs"] mod trace_tests; use crate::web::trace::{HttpTraceExchange, TraceDirection, TraceLifecycleEvent, TraceRoute}; use activity::{ActivityBody, ConnectionActivity, RequestActivity, RequestDeadlineHandle}; use body::{CollectBodyError, CollectedBody, RequestBody, collect_body}; use capability::bridge_candidate; use decoy::serve_decoy; use down::handle_down; use request::{ bearer_token_hash, binary_content_type, canonical_request_host, canonical_u64_header, client_ip, compatible_cookie_header, match_profile, }; use response::{ bad_gateway, carrier_empty, carrier_headers, carrier_lane, full_response, generic_not_found, insert_header, service_unavailable, }; use session::handle_session; type BoxError = Box; type HttpBody = UnsyncBoxBody; type HttpResponse = Response; const TRANSPORT_SUFFIXES: [&str; 4] = [ "api/v1/session", "api/v1/up", "api/v1/down", "api/v1/diagnostic", ]; const WEBSOCKET_SUFFIX: &str = "api/v1/ws"; /// Serves one bounded HTTP/1.1 connection accepted from an external TLS terminator. pub(crate) async fn serve_connection( stream: TcpStream, peer: SocketAddr, client_ip_source: WebClientIpSource, trusted_proxy_cidrs: Arc<[IpNetwork]>, runtime: Arc, cancellation: CancellationToken, connection_permit: tokio::sync::OwnedSemaphorePermit, ) { let config = runtime.active_generation().config(); let max_header_bytes = config.web.limits.max_header_bytes; let header_timeout = Duration::from_secs(config.web.timeouts.header_secs); let idle_timeout = Duration::from_secs(config.web.timeouts.http_idle_secs); let connection_activity = ConnectionActivity::new(); let service_activity = connection_activity.clone(); let service = service_fn(move |mut request| { let runtime = Arc::clone(&runtime); let trusted_proxy_cidrs = Arc::clone(&trusted_proxy_cidrs); let connection_activity = service_activity.clone(); let client_ip_source = client_ip_source; async move { let Some(activity) = RequestActivity::begin(connection_activity) else { let mut response = service_unavailable(); response .headers_mut() .insert(header::CONNECTION, HeaderValue::from_static("close")); return Ok::<_, Infallible>(response); }; request.extensions_mut().insert(activity.deadline_handle()); let trace = runtime.trace().begin_http(&request, peer.ip()); if let Some(trace) = &trace { request.extensions_mut().insert(Arc::clone(trace)); } let request = request.map(|body| RequestBody::new(body, trace.clone())); let response = if let Some(_handler_permit) = runtime.try_http_handler() { handle_request( request, peer, client_ip_source, &trusted_proxy_cidrs, runtime, ) .await } else { service_unavailable() }; if let Some(trace) = &trace { trace.response_ready(&response); } let response = response.map(|body| ActivityBody::new(body, activity, trace).boxed_unsync()); Ok::<_, Infallible>(response) } }); let connection = http1::Builder::new() .timer(TokioTimer::new()) .header_read_timeout(header_timeout) .max_buf_size(max_header_bytes) .keep_alive(true) .serve_connection( TokioIo::new(websocket::ConnectionIo::new(stream, connection_permit)), service, ) .with_upgrades(); tokio::pin!(connection); let mut idle_check = tokio::time::interval((idle_timeout / 2).max(Duration::from_secs(1))); idle_check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); loop { tokio::select! { biased; _ = cancellation.cancelled() => break, _ = &mut connection => break, _ = idle_check.tick() => { if connection_activity.should_close(tokio::time::Instant::now(), idle_timeout) { break; } } } } } async fn handle_request( mut request: Request, peer: SocketAddr, client_ip_source: WebClientIpSource, trusted_proxy_cidrs: &[IpNetwork], runtime: Arc, ) -> HttpResponse { set_trace_route(&request, TraceRoute::Decoy); if let Some(trace) = request_trace(&request) && let Some(client_ip) = client_ip(&request, peer, client_ip_source, trusted_proxy_cidrs) { trace.set_effective_ip(client_ip); } let generation = runtime.active_generation(); let config = generation.config(); let Some(web_runtime) = config.web.runtime.as_ref() else { return generic_not_found(); }; let Some(host) = canonical_request_host(&request) else { return generic_not_found(); }; let Some(vhost) = web_runtime.vhosts.get(host).cloned() else { return generic_not_found(); }; secrets::mark_internal_credential(&mut request, web_runtime, &runtime); let suffix = request.uri().path().strip_prefix(&vhost.base); if suffix == Some(WEBSOCKET_SUFFIX) { return websocket::handle( request, peer, client_ip_source, trusted_proxy_cidrs, runtime, vhost, ) .await; } if suffix.is_some_and(|suffix| TRANSPORT_SUFFIXES.contains(&suffix)) { return handle_api( request, peer, client_ip_source, trusted_proxy_cidrs, runtime, vhost, ) .await; } if suffix == Some("") && matches!(*request.method(), Method::GET | Method::HEAD) { return handle_root( request, peer, client_ip_source, trusted_proxy_cidrs, runtime, generation, vhost, ) .await; } let sanitize_recovery = recovery::has_media_type(&request); if sanitize_recovery { strip_query(&mut request); } serve_decoy(request, vhost, sanitize_recovery, &runtime).await } async fn handle_root( mut request: Request, peer: SocketAddr, client_ip_source: WebClientIpSource, trusted_proxy_cidrs: &[IpNetwork], runtime: Arc, generation: Arc, vhost: Arc, ) -> HttpResponse { let representation = recovery::classify(&request); if matches!(representation, recovery::RootRepresentation::Invalid) { strip_query(&mut request); return serve_decoy(request, vhost, true, &runtime).await; } let candidate = bridge_candidate(request.uri().query()); let canonical = candidate.is_canonical(); let plausible_candidate = canonical && request.method() == Method::GET; let fasttrack_mode = vhost.decoy_fasttrack_mode; match fasttrack_mode { WebDecoyFastTrackMode::Off => {} WebDecoyFastTrackMode::Shadow => { runtime .telemetry() .record_decoy_fasttrack(if plausible_candidate { WebDecoyFastTrackDisposition::ShadowCandidateFullScan } else { WebDecoyFastTrackDisposition::ShadowWouldFastTrack }); } WebDecoyFastTrackMode::Enforce if !plausible_candidate => { runtime .telemetry() .record_decoy_fasttrack(WebDecoyFastTrackDisposition::EnforceFastTrack); let recovery_requested = matches!(representation, recovery::RootRepresentation::Recovery(_)); if recovery_requested { strip_query(&mut request); } return serve_decoy(request, vhost, recovery_requested, &runtime).await; } WebDecoyFastTrackMode::Enforce => { runtime .telemetry() .record_decoy_fasttrack(WebDecoyFastTrackDisposition::EnforceCandidateFullScan); } } let matched_profile = match_profile(&vhost, candidate.scan_bytes()); let recovery_requested = matches!(representation, recovery::RootRepresentation::Recovery(_)); let profile = matched_profile.filter(|_| canonical && request.method() == Method::GET); let Some(profile) = profile else { if recovery_requested { strip_query(&mut request); } return serve_decoy(request, vhost, recovery_requested, &runtime).await; }; let Some(client_ip) = client_ip(&request, peer, client_ip_source, trusted_proxy_cidrs) else { strip_query(&mut request); return serve_decoy(request, vhost, true, &runtime).await; }; if let Some(trace) = request_trace(&request) { trace.set_route(TraceRoute::Bridge); trace.set_effective_ip(client_ip); } let user_agent = request .headers() .get(header::USER_AGENT) .and_then(|value| value.to_str().ok()); let recovery_session = match representation { recovery::RootRepresentation::Recovery(Some(hash)) => { runtime.bridge_recovery_session(hash, &vhost.host, &profile) } _ => None, }; let bootstrap = match match representation { recovery::RootRepresentation::Bridge => runtime.issue_bootstrap_for_request( &generation, Arc::clone(&profile), client_ip, user_agent, ), recovery::RootRepresentation::Recovery(_) => runtime.issue_recovery_bootstrap_for_request( &generation, Arc::clone(&profile), client_ip, user_agent, recovery_session .as_ref() .map(|session| session.trace_session_id()), ), recovery::RootRepresentation::Invalid => { strip_query(&mut request); return serve_decoy(request, vhost, true, &runtime).await; } } { Ok(bootstrap) => bootstrap, Err(error) => { runtime.trace().record_profile_lifecycle( client_ip, None, &profile, TraceLifecycleEvent::BootstrapRejected, None, Some(error.as_str()), ); strip_query(&mut request); return serve_decoy(request, vhost, true, &runtime).await; } }; if let Some(trace) = request_trace(&request) { trace.bind_profile(&profile, bootstrap.trace_session_id); trace.register_redaction(bootstrap.token.as_bytes()); } let config = generation.config(); if let recovery::RootRepresentation::Recovery(_) = representation { if let Some(session) = recovery_session { let outcome = session.close(crate::web::session::SessionCloseReason::BridgeRecovery); if outcome != crate::web::session::SessionCloseOutcome::Closed && tokio::time::timeout( Duration::from_secs(config.web.timeouts.bridge_request_secs), session.wait_close_complete(), ) .await .is_err() { strip_query(&mut request); return serve_decoy(request, vhost, true, &runtime).await; } } let Some(response) = recovery::response( &bootstrap, &vhost, &profile, &config.web.limits, &config.web.timeouts, ) else { strip_query(&mut request); return serve_decoy(request, vhost, true, &runtime).await; }; return response; } let page = bridge::render( &vhost.host, &vhost.base, &bootstrap.token, config.web.limits.carrier_batch_bytes, config.web.limits.pending_bytes_per_session, config.web.limits.pending_items_per_session, profile.max_streams_per_session, profile.carrier_negotiation_enabled, profile.carriers.len(), profile.carrier_negotiation_deadlines_secs, config.web.timeouts.long_poll_secs, config.web.timeouts.bridge_request_secs, config.web.timeouts.bridge_retry_secs, config.web.timeouts.bridge_recovery_secs, config.web.timeouts.websocket_open_secs, config.web.timeouts.reconnect_grace_secs, config.web.timeouts.carrier_probe_coalesce_ms, config.web.debug.bridge_diagnostics_enabled(), config.web.carrier_method, &generation.rng, ); let mut response = full_response(StatusCode::OK, Bytes::from(page.body)); response.headers_mut().insert( header::CONTENT_TYPE, HeaderValue::from_static("text/html; charset=utf-8"), ); insert_header( &mut response, header::CONTENT_SECURITY_POLICY, &page.content_security_policy, ); response .headers_mut() .insert(header::CACHE_CONTROL, HeaderValue::from_static("no-store")); response.headers_mut().insert( header::REFERRER_POLICY, HeaderValue::from_static("no-referrer"), ); response.headers_mut().insert( header::X_CONTENT_TYPE_OPTIONS, HeaderValue::from_static("nosniff"), ); response.headers_mut().insert( HeaderName::from_static("x-dns-prefetch-control"), HeaderValue::from_static("off"), ); insert_header( &mut response, HeaderName::from_static("permissions-policy"), bridge::PERMISSIONS_POLICY, ); response } async fn handle_api( request: Request, peer: SocketAddr, client_ip_source: WebClientIpSource, trusted_proxy_cidrs: &[IpNetwork], runtime: Arc, vhost: Arc, ) -> HttpResponse { if request.uri().query().is_some() || !compatible_cookie_header(&request) { return serve_decoy(request, vhost, true, &runtime).await; } let Some(client_ip) = client_ip(&request, peer, client_ip_source, trusted_proxy_cidrs) else { return serve_decoy(request, vhost, true, &runtime).await; }; if let Some(trace) = request_trace(&request) { trace.set_effective_ip(client_ip); } let Some(token_hash) = bearer_token_hash(&request) else { return serve_decoy(request, vhost, true, &runtime).await; }; match request.uri().path().strip_prefix(&vhost.base) { Some("api/v1/session") => { handle_session(request, runtime, vhost, token_hash, client_ip).await } Some("api/v1/up") => handle_up(request, runtime, vhost, token_hash).await, Some("api/v1/down") => handle_down(request, runtime, vhost, token_hash).await, Some("api/v1/diagnostic") => { diagnostic::handle(request, runtime, vhost, token_hash, client_ip).await } _ => serve_decoy(request, vhost, true, &runtime).await, } } async fn handle_up( request: Request, runtime: Arc, vhost: Arc, token_hash: crate::web::manager::TokenHash, ) -> HttpResponse { if !matches!(*request.method(), Method::POST | Method::PUT) || !binary_content_type(&request) { return serve_decoy(request, vhost, true, &runtime).await; } let Some(sequence) = canonical_u64_header(&request, "x-up-seq").filter(|value| *value != 0) else { return serve_decoy(request, vhost, true, &runtime).await; }; let Ok(session) = runtime.get_session(token_hash, &vhost.host) else { return serve_decoy(request, vhost, true, &runtime).await; }; if session.carrier().uses_websocket() { return serve_decoy(request, vhost, true, &runtime).await; } if let Some(trace) = request_trace(&request) { trace.set_route(TraceRoute::Uplink); trace.bind_identity(session.trace_identity()); } let Some(lane_id) = carrier_lane(&request, session.carrier()) else { return serve_decoy(request, vhost, true, &runtime).await; }; let limit = session.limits().max_body_bytes; let CollectedBody { request, body, _body_budget, } = match collect_body( request, &runtime, Duration::from_secs(session.timeouts().body_secs), limit, false, ) .await { Ok(result) => result, Err(CollectBodyError::Limit) => return service_unavailable(), Err(CollectBodyError::Invalid(request)) => { return serve_decoy(request, vhost, true, &runtime).await; } }; if let Some(trace) = request_trace(&request) { trace.record_frames(TraceDirection::Request, &body, session.limits()); } let result = match lane_id { Some(lane_id) => session.process_up_lane(lane_id, sequence, &body), None => session.process_up(sequence, &body), }; match result { Ok(ack) => { let mut response = carrier_empty(StatusCode::NO_CONTENT); insert_header( &mut response, HeaderName::from_static("x-up-ack"), &ack.to_string(), ); response } Err(ManagerError::Backpressure | ManagerError::Concurrent | ManagerError::Limit) => { service_unavailable() } Err(_) => serve_decoy(request, vhost, true, &runtime).await, } } fn strip_query(request: &mut Request) { if request.uri().query().is_some() && let Ok(uri) = request.uri().path().parse() { *request.uri_mut() = uri; } } fn request_trace(request: &Request) -> Option<&Arc> { request.extensions().get::>() } fn request_deadline(request: &Request) -> Option { request.extensions().get::().cloned() } fn set_trace_route(request: &Request, route: TraceRoute) { if let Some(trace) = request_trace(request) { trace.set_route(route); } }