use super::*; async fn open_keepalive( listener: &TcpListener, runtime: &Arc, ) -> (TcpStream, CancellationToken, tokio::task::JoinHandle<()>) { let addr = listener.local_addr().unwrap(); let (accepted, client) = tokio::join!(listener.accept(), TcpStream::connect(addr)); let (server, peer) = accepted.unwrap(); let cancellation = CancellationToken::new(); let permit = runtime.try_http_connection().unwrap(); let task = tokio::spawn(serve_connection( server, peer, WebClientIpSource::XForwardedFor, Arc::from(["127.0.0.1/32".parse().unwrap()]), Arc::clone(runtime), cancellation.clone(), permit, )); (client.unwrap(), cancellation, task) } async fn read_http_response(client: &mut TcpStream) -> Vec { let mut response = Vec::new(); while !response.ends_with(b"\r\n\r\n") { assert!(response.len() < 16 * 1024); response.push(client.read_u8().await.unwrap()); } let content_length = std::str::from_utf8(&response) .unwrap() .lines() .filter_map(|line| line.split_once(':')) .find_map(|(name, value)| { name.eq_ignore_ascii_case("content-length") .then(|| value.trim().parse::().unwrap()) }) .unwrap_or(0); let body_start = response.len(); response.resize(body_start + content_length, 0); client .read_exact(&mut response[body_start..]) .await .unwrap(); response } async fn request_with_body_delay( listener: &TcpListener, runtime: &Arc, head: Vec, body: &[u8], delay: std::time::Duration, ) -> Vec { let addr = listener.local_addr().unwrap(); let (accepted, client) = tokio::join!(listener.accept(), TcpStream::connect(addr)); let (server, peer) = accepted.unwrap(); let mut client = client.unwrap(); let permit = runtime.try_http_connection().unwrap(); let task = tokio::spawn(serve_connection( server, peer, WebClientIpSource::XForwardedFor, Arc::from(["127.0.0.1/32".parse().unwrap()]), Arc::clone(runtime), CancellationToken::new(), permit, )); client.write_all(&head).await.unwrap(); tokio::time::sleep(delay).await; let _ = client.write_all(body).await; let mut response = Vec::new(); client.read_to_end(&mut response).await.unwrap(); task.await.unwrap(); response } #[tokio::test] async fn issued_bootstrap_timeouts_survive_reload_before_session_creation() { let capability = [20u8; 32]; let mut initial_config = runtime_config(capability, WebCarrier::Https); initial_config.web.timeouts.body_secs = 3; initial_config.web.timeouts.long_poll_secs = 3; initial_config.web.timeouts.bootstrap_lifetime_secs = 5; let generation = test_runtime_generation(1, initial_config); let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation))); let runtime = WebProcessRuntime::start(Arc::clone(&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 root = 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 root_response = request(&listener, &runtime, root).await; let (_, root_body) = split_response(&root_response); let bootstrap = std::str::from_utf8(root_body) .unwrap() .split_once("bootstrap=\"") .and_then(|(_, suffix)| suffix.split_once('"')) .map(|(token, _)| token.to_string()) .unwrap(); let mut replacement_config = runtime_config(capability, WebCarrier::Https); replacement_config.web.timeouts.body_secs = 1; replacement_config.web.timeouts.long_poll_secs = 1; replacement_config.web.timeouts.bootstrap_lifetime_secs = 1; let replacement = test_runtime_generation(2, replacement_config); active_runtime.store(Arc::clone(&replacement)); let hello = frame::encode(FrameType::Hello, 0, &[1]); let create_head = format!( "POST /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", hello.len() ) .into_bytes(); let create_response = request_with_body_delay( &listener, &runtime, create_head, &hello, std::time::Duration::from_millis(1200), ) .await; assert!(create_response.starts_with(b"HTTP/1.1 200")); runtime.shutdown().await; generation.stop_sessions().await; generation.stop_background_tasks().await; replacement.stop_sessions().await; replacement.stop_background_tasks().await; } #[tokio::test] async fn live_session_body_and_closed_token_timeouts_survive_reload() { let capability = [21u8; 32]; let mut initial_config = runtime_config(capability, WebCarrier::Https); initial_config.web.timeouts.body_secs = 3; initial_config.web.timeouts.header_secs = 1; initial_config.web.timeouts.http_idle_secs = 4; initial_config.web.timeouts.long_poll_secs = 3; initial_config.web.timeouts.bootstrap_lifetime_secs = 5; let generation = test_runtime_generation(1, initial_config); let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation))); let runtime = WebProcessRuntime::start(Arc::clone(&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 root = 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 root_response = request(&listener, &runtime, root).await; let (_, root_body) = split_response(&root_response); let bootstrap = std::str::from_utf8(root_body) .unwrap() .split_once("bootstrap=\"") .and_then(|(_, suffix)| suffix.split_once('"')) .map(|(token, _)| token.to_string()) .unwrap(); let hello = frame::encode(FrameType::Hello, 0, &[1]); let create_head = format!( "POST /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", hello.len() ) .into_bytes(); let mut create = create_head.clone(); create.extend_from_slice(&hello); let create_response = request(&listener, &runtime, create).await; let (create_headers, _) = split_response(&create_response); assert!(create_headers.starts_with(b"HTTP/1.1 200")); let session = response_header(create_headers, "x-session-token").to_string(); let mut replacement_config = runtime_config(capability, WebCarrier::Https); replacement_config.web.timeouts.body_secs = 1; replacement_config.web.timeouts.header_secs = 1; replacement_config.web.timeouts.http_idle_secs = 2; replacement_config.web.timeouts.long_poll_secs = 1; replacement_config.web.timeouts.bootstrap_lifetime_secs = 1; let replacement = test_runtime_generation(2, replacement_config); active_runtime.store(Arc::clone(&replacement)); let retry_response = request_with_body_delay( &listener, &runtime, create_head, &hello, std::time::Duration::from_millis(1200), ) .await; let (retry_headers, _) = split_response(&retry_response); assert!(retry_headers.starts_with(b"HTTP/1.1 200")); assert_eq!(response_header(retry_headers, "x-session-token"), session); let down = format!( "POST /api/v1/down HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nX-Down-Cursor: 0\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" ) .into_bytes(); let down_response = request(&listener, &runtime, down).await; assert!(down_response.starts_with(b"HTTP/1.1 204")); let close = format!( "DELETE /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {session}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" ) .into_bytes(); let close_response = request(&listener, &runtime, close.clone()).await; assert!(close_response.starts_with(b"HTTP/1.1 204")); tokio::time::sleep(std::time::Duration::from_millis(1500)).await; let close_retry_response = request(&listener, &runtime, close).await; assert!(close_retry_response.starts_with(b"HTTP/1.1 204")); runtime.shutdown().await; generation.stop_sessions().await; generation.stop_background_tasks().await; replacement.stop_sessions().await; replacement.stop_background_tasks().await; } #[tokio::test] async fn active_body_deadline_survives_reload_on_old_keepalive_connection() { let capability = [22u8; 32]; let mut initial_config = runtime_config(capability, WebCarrier::Https); initial_config.web.timeouts.header_secs = 1; initial_config.web.timeouts.body_secs = 1; initial_config.web.timeouts.long_poll_secs = 1; initial_config.web.timeouts.http_idle_secs = 2; let generation = test_runtime_generation(1, initial_config); let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&generation))); let runtime = WebProcessRuntime::start(Arc::clone(&active_runtime)); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let (mut client, cancellation, task) = open_keepalive(&listener, &runtime).await; client .write_all( b"GET / HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\n\r\n", ) .await .unwrap(); assert!( read_http_response(&mut client) .await .starts_with(b"HTTP/1.1 200") ); let mut replacement_config = runtime_config(capability, WebCarrier::Https); replacement_config.web.timeouts.header_secs = 1; replacement_config.web.timeouts.body_secs = 3; replacement_config.web.timeouts.long_poll_secs = 1; replacement_config.web.timeouts.http_idle_secs = 4; let replacement = test_runtime_generation(2, replacement_config); active_runtime.store(Arc::clone(&replacement)); let encoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(capability); client .write_all( format!( "GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\n\r\n" ) .as_bytes(), ) .await .unwrap(); let bridge = read_http_response(&mut client).await; let (_, bridge_body) = split_response(&bridge); let bootstrap = std::str::from_utf8(bridge_body) .unwrap() .split_once("bootstrap=\"") .and_then(|(_, suffix)| suffix.split_once('"')) .map(|(token, _)| token.to_string()) .unwrap(); let hello = frame::encode(FrameType::Hello, 0, &[1]); client .write_all( format!( "POST /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.10\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", hello.len() ) .as_bytes(), ) .await .unwrap(); tokio::time::sleep(std::time::Duration::from_millis(2200)).await; client.write_all(&hello).await.unwrap(); assert!( read_http_response(&mut client) .await .starts_with(b"HTTP/1.1 200") ); cancellation.cancel(); drop(client); task.await.unwrap(); runtime.shutdown().await; generation.stop_sessions().await; generation.stop_background_tasks().await; replacement.stop_sessions().await; replacement.stop_background_tasks().await; }