WEB Session Lifecycle Observability + Bridge Recovery Drafts

Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
Alexey
2026-09-01 19:19:52 +03:00
parent aec7d1619a
commit 01ffca5d34
47 changed files with 1678 additions and 441 deletions
+1
View File
@@ -297,6 +297,7 @@ fn lease_deadline(
fn sanitize_transport_request<B>(request: &mut Request<B>) {
for name in [
header::AUTHORIZATION,
header::ACCEPT,
header::CONTENT_LENGTH,
header::CONTENT_TYPE,
header::UPGRADE,
+141
View File
@@ -0,0 +1,141 @@
use bytes::Bytes;
use hyper::header::{self, HeaderValue};
use hyper::{Request, StatusCode};
use serde::Serialize;
use super::request::{bearer_token_hash, compatible_cookie_header};
use super::response::{full_response, insert_header};
use super::{HttpResponse, RequestBody};
use crate::config::{WebRuntimeProfile, WebRuntimeVhost};
use crate::web::manager::{BootstrapResult, TokenHash};
pub(super) const MEDIA_TYPE: &str = "application/vnd.telemt.web-recovery+json";
const MAX_RESPONSE_BYTES: usize = 1024;
/// Positive representation selected by exact recovery headers.
#[derive(Clone, Copy)]
pub(super) enum RootRepresentation {
/// Render the ordinary transient bridge document.
Bridge,
/// Return fresh recovery policy and optionally retire one current bearer.
Recovery(Option<TokenHash>),
/// Hide malformed recovery material behind the configured decoy.
Invalid,
}
/// Classifies recovery headers without changing capability authentication.
pub(super) fn classify(request: &Request<RequestBody>) -> RootRepresentation {
let accepts = request.headers().get_all(header::ACCEPT);
let mut values = accepts.iter();
let first = values.next();
let exact = first.is_some_and(|value| value.as_bytes() == MEDIA_TYPE.as_bytes())
&& values.next().is_none();
let recovery_present = accepts
.iter()
.any(|value| value.as_bytes() == MEDIA_TYPE.as_bytes());
let authorization_present = request.headers().contains_key(header::AUTHORIZATION);
if !exact {
return if authorization_present || recovery_present {
RootRepresentation::Invalid
} else {
RootRepresentation::Bridge
};
}
if !compatible_cookie_header(request) {
return RootRepresentation::Invalid;
}
if !authorization_present {
return RootRepresentation::Recovery(None);
}
bearer_token_hash(request)
.map(|hash| RootRepresentation::Recovery(Some(hash)))
.unwrap_or(RootRepresentation::Invalid)
}
/// Builds the bounded no-store recovery representation.
pub(super) fn response(
bootstrap: &BootstrapResult,
vhost: &WebRuntimeVhost,
profile: &WebRuntimeProfile,
limits: &crate::config::WebLimitsConfig,
timeouts: &crate::config::WebTimeoutsConfig,
) -> Option<HttpResponse> {
let document = RecoveryDocument {
version: 1,
bootstrap: &bootstrap.token,
limits: RecoveryLimits {
carrier_batch_bytes: limits.carrier_batch_bytes,
pending_bytes_per_session: limits.pending_bytes_per_session,
pending_items_per_session: limits.pending_items_per_session,
max_streams_per_session: profile.max_streams_per_session,
},
timeouts: RecoveryTimeouts {
long_poll_secs: timeouts.long_poll_secs,
bridge_request_secs: timeouts.bridge_request_secs,
bridge_retry_secs: timeouts.bridge_retry_secs,
bridge_recovery_secs: timeouts.bridge_recovery_secs,
websocket_open_secs: timeouts.websocket_open_secs,
reconnect_grace_secs: timeouts.reconnect_grace_secs,
},
negotiation: RecoveryNegotiation {
enabled: profile.carrier_negotiation_enabled,
candidate_count: profile.carriers.len(),
deadlines_secs: profile.carrier_negotiation_deadlines_secs,
carrier_probe_coalesce_ms: timeouts.carrier_probe_coalesce_ms,
},
};
let body = serde_json::to_vec(&document).ok()?;
if body.len() > MAX_RESPONSE_BYTES || profile.host != vhost.host {
return None;
}
let mut response = full_response(StatusCode::OK, Bytes::from(body));
insert_header(&mut response, header::CONTENT_TYPE, MEDIA_TYPE);
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"),
);
Some(response)
}
#[derive(Serialize)]
struct RecoveryDocument<'a> {
#[serde(rename = "v")]
version: u8,
bootstrap: &'a str,
limits: RecoveryLimits,
timeouts: RecoveryTimeouts,
negotiation: RecoveryNegotiation,
}
#[derive(Serialize)]
struct RecoveryLimits {
carrier_batch_bytes: usize,
pending_bytes_per_session: usize,
pending_items_per_session: usize,
max_streams_per_session: usize,
}
#[derive(Serialize)]
struct RecoveryTimeouts {
long_poll_secs: u64,
bridge_request_secs: u64,
bridge_retry_secs: u64,
bridge_recovery_secs: u64,
websocket_open_secs: u64,
reconnect_grace_secs: u64,
}
#[derive(Serialize)]
struct RecoveryNegotiation {
enabled: bool,
candidate_count: usize,
deadlines_secs: [u64; 4],
carrier_probe_coalesce_ms: u64,
}
+5 -3
View File
@@ -10,7 +10,9 @@ use tokio_util::sync::CancellationToken;
use super::ConnectionIo;
use crate::web::http::activity::UpgradeDeadlineLease;
use crate::web::manager::{WebProcessRuntime, WebSocketBudgetLease, WebSocketConnection};
use crate::web::session::{WebSession, WebSocketLaneReservation, WebSocketProbeReservation};
use crate::web::session::{
SessionCloseReason, WebSession, WebSocketLaneReservation, WebSocketProbeReservation,
};
use crate::web::trace::{TraceDirection, TraceWebSocketContext};
const READ_BUFFER_BYTES: usize = 64 * 1024;
@@ -103,7 +105,7 @@ pub(super) async fn run_upgraded(
if let Some(reservation) = lane_reservation {
session.close_websocket_lane(reservation);
} else if !acknowledge_commit || session.is_carrier_committed() {
session.close();
session.close(SessionCloseReason::WebSocketEnded);
}
}
@@ -194,7 +196,7 @@ async fn run_multiplex(
started,
);
if !session.websocket_commit_ack_written(connection.id()) {
session.close();
session.close(SessionCloseReason::Protocol);
return Err(());
}
} else if acknowledge_commit
+3 -3
View File
@@ -8,7 +8,7 @@ use tokio_util::sync::CancellationToken;
use super::CarrierSocket;
use super::io::{flush, process_lane, read_message, record_message, reserve_data, send};
use crate::web::manager::{WebProcessRuntime, WebSocketBudgetLease, WebSocketConnection};
use crate::web::session::{WebSession, WebSocketLaneReservation};
use crate::web::session::{SessionCloseReason, WebSession, WebSocketLaneReservation};
use crate::web::trace::{TraceDirection, TraceWebSocketContext};
#[allow(clippy::too_many_arguments)]
@@ -92,7 +92,7 @@ pub(super) async fn run_lane(
.await
.is_err()
{
session.close();
session.close(SessionCloseReason::Protocol);
return Err(());
}
record_message(
@@ -104,7 +104,7 @@ pub(super) async fn run_lane(
started,
);
if !session.websocket_commit_ack_written(connection.id()) {
session.close();
session.close(SessionCloseReason::Protocol);
return Err(());
}
} else if acknowledge_commit