WEB Knobs in API

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-08-27 21:42:14 +03:00
parent d41a8c3220
commit cf0cd08387
43 changed files with 3399 additions and 196 deletions
+23 -7
View File
@@ -63,18 +63,20 @@ impl ConnectionActivity {
if state.failed {
return true;
}
let request_protected = state
let request_deadline = state
.request
.as_ref()
.and_then(|request| request.deadline)
.is_some_and(|deadline| now <= deadline.deadline);
let upgrade_protected = state
.map(|deadline| deadline.deadline);
let upgrade_deadline = state
.upgrade
.as_ref()
.is_some_and(|upgrade| now <= upgrade.deadline.deadline);
!request_protected
&& !upgrade_protected
&& now.saturating_duration_since(state.last_progress) >= idle
.map(|upgrade| upgrade.deadline.deadline);
let protected_until = request_deadline.into_iter().chain(upgrade_deadline).max();
let idle_since = protected_until
.filter(|deadline| *deadline > state.last_progress)
.unwrap_or(state.last_progress);
now.saturating_duration_since(idle_since) >= idle
}
fn fail(&self) {
@@ -390,6 +392,20 @@ mod tests {
assert!(!activity.should_close(Instant::now(), Duration::from_secs(1)));
}
#[test]
fn expired_operation_lease_gets_one_idle_interval_to_publish_its_result() {
let activity = ConnectionActivity::new();
let request = RequestActivity::begin(activity.clone()).unwrap();
let now = Instant::now();
let _lease = request
.deadline_handle()
.lease_until(now + Duration::from_secs(1))
.unwrap();
assert!(!activity.should_close(now + Duration::from_millis(1500), Duration::from_secs(1)));
assert!(activity.should_close(now + Duration::from_secs(2), Duration::from_secs(1)));
}
#[test]
fn stale_request_lease_cannot_clear_a_new_request_deadline() {
let activity = ConnectionActivity::new();
+103
View File
@@ -0,0 +1,103 @@
use super::*;
#[tokio::test]
async fn runtime_index_lists_and_asynchronously_closes_by_opaque_reference() {
let capability = [19u8; 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 root = format!(
"GET /?bridge={encoded} HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.19\r\nUser-Agent: Telemt-Control-Test/1\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)
.unwrap();
let hello = frame::encode(FrameType::Hello, 0, &[1]);
let mut create = format!(
"POST /api/v1/session HTTP/1.1\r\nHost: proxy.example.com\r\nX-Forwarded-For: 192.0.2.19\r\nAuthorization: Bearer {bootstrap}\r\nContent-Type: application/octet-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
hello.len()
)
.into_bytes();
create.extend_from_slice(&hello);
let create_response = request(&listener, &runtime, create).await;
assert!(create_response.starts_with(b"HTTP/1.1 200"));
let page = runtime.list_sessions(SessionListRequest {
limit: 50,
cursor: None,
filter: SessionFilter::default(),
});
assert_eq!(page.sessions.len(), 1);
assert_eq!(
page.sessions[0].user_agent.as_deref(),
Some("Telemt-Control-Test/1")
);
let session_ref = page.sessions[0].session_ref.clone();
let trace_session_id = runtime.parse_session_ref(&session_ref).unwrap();
let noncanonical_session_ref = format!("ws1.{}.000000000000000A", runtime.runtime_instance());
assert_eq!(
runtime.parse_session_ref(&noncanonical_session_ref),
Err(SessionRefError::Invalid)
);
let noncanonical_operation_id = format!("wo1.{}.000000000000000A", runtime.runtime_instance());
assert!(matches!(
runtime.control_operation(&noncanonical_operation_id),
Err(ControlError::InvalidOperation)
));
let nonmatching = runtime
.start_close_operation(
runtime.runtime_instance(),
CloseOperationSelector::Filter(SessionFilter {
state: Some("healthy".to_string()),
..SessionFilter::default()
}),
)
.unwrap();
let mut nonmatching_status = None;
for _ in 0..32 {
let status = runtime
.control_operation(&nonmatching.operation_id)
.unwrap();
if serde_json::to_value(&status).unwrap()["state"] == "completed" {
nonmatching_status = Some(status);
break;
}
tokio::task::yield_now().await;
}
let nonmatching_status = nonmatching_status.expect("filtered close operation completed");
assert_eq!(nonmatching_status.matched, 0);
assert_eq!(nonmatching_status.close_signalled, 0);
assert!(matches!(
runtime.session_detail(trace_session_id),
SessionDetail::Active(_)
));
let operation = runtime
.start_close_operation(
runtime.runtime_instance(),
CloseOperationSelector::Refs(vec![trace_session_id]),
)
.unwrap();
for _ in 0..32 {
let status = runtime.control_operation(&operation.operation_id).unwrap();
if serde_json::to_value(&status).unwrap()["state"] == "completed" {
break;
}
tokio::task::yield_now().await;
}
assert!(matches!(
runtime.session_detail(trace_session_id),
SessionDetail::Gone { .. }
));
runtime.shutdown().await;
generation.stop_sessions().await;
generation.stop_background_tasks().await;
}
+1 -4
View File
@@ -68,7 +68,7 @@ pub(super) async fn handle_session(
return serve_decoy(request, vhost, true, &runtime).await;
};
let ip_learning_eligible = carrier_ip_learning_eligible(&request, client_ip);
let Some((trace_session_id, profile, frozen_body_timeout)) =
let Some((trace_session_id, profile, body_timeout)) =
runtime.bootstrap_trace_identity(token_hash, &vhost.host)
else {
return serve_decoy(request, vhost, true, &runtime).await;
@@ -77,9 +77,6 @@ pub(super) async fn handle_session(
trace.set_route(TraceRoute::Session);
trace.bind_profile(&profile, trace_session_id);
}
let body_timeout = frozen_body_timeout.unwrap_or_else(|| {
Duration::from_secs(runtime.active_generation().config().web.timeouts.body_secs)
});
let CollectedBody {
request,
body,
+55
View File
@@ -75,6 +75,61 @@ async fn request_with_body_delay(
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];
+27 -1
View File
@@ -15,7 +15,10 @@ use crate::config::{
};
use crate::maestro::generation::test_runtime_generation;
use crate::web::frame::{self, FrameType};
use crate::web::manager::WebProcessRuntime;
use crate::web::manager::{
CloseOperationSelector, ControlError, SessionDetail, SessionFilter, SessionListRequest,
SessionRefError, WebProcessRuntime,
};
#[path = "legacy_tests.rs"]
mod legacy_tests;
@@ -24,6 +27,9 @@ mod negotiation_tests;
// Reload-stability tests for session-owned timeout policy.
#[path = "session_policy_tests.rs"]
mod session_policy_tests;
// Runtime control integration stays separate from carrier protocol scenarios.
#[path = "control_tests.rs"]
mod control_tests;
const TEST_CARRIER_DEADLINES_SECS: [u64; 4] = [3, 5, 8, 12];
@@ -394,6 +400,26 @@ async fn unused_bootstrap_survives_equivalent_runtime_generation_swap() {
replacement.stop_background_tasks().await;
}
#[tokio::test]
async fn bridge_bootstrap_uses_the_generation_that_selected_its_profile() {
let initial = test_runtime_generation(1, runtime_config([21; 32], WebCarrier::Https));
let active_runtime = Arc::new(ArcSwap::from(Arc::clone(&initial)));
let runtime = WebProcessRuntime::start(Arc::clone(&active_runtime));
let profile = initial.config().web.runtime.as_ref().unwrap().profiles[0].clone();
let replacement = test_runtime_generation(2, runtime_config([22; 32], WebCarrier::HttpsLanes));
active_runtime.store(Arc::clone(&replacement));
let result =
runtime.issue_bootstrap_for_generation(&initial, profile, "192.0.2.10".parse().unwrap());
assert!(result.is_ok());
runtime.shutdown().await;
initial.stop_sessions().await;
initial.stop_background_tasks().await;
replacement.stop_sessions().await;
replacement.stop_background_tasks().await;
}
#[tokio::test]
async fn unused_bootstrap_is_rejected_after_profile_identity_change() {
let capability = [11u8; 32];