mirror of
https://github.com/telemt/telemt.git
synced 2026-09-12 21:44:08 +03:00
WEB Knobs in API
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user