mirror of
https://github.com/telemt/telemt.git
synced 2026-09-19 08:52:48 +03:00
Bounded Debugging + Websocket Carriers + Carriers Negotiation
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
//! | `general` | `telemetry` / `me_*_policy` | Applied immediately |
|
||||
//! | `network` | `dns_overrides` | Applied immediately |
|
||||
//! | `access` | All user/quota fields | Effective immediately |
|
||||
//! | `web` | Carrier, timing, and debug policy | Applied to newly issued sessions |
|
||||
//! Fields that require re-binding sockets (`server.listeners`, legacy
|
||||
//! `server.port`, `censorship.*`, `network.*`, `use_middle_proxy`) are **not**
|
||||
//! applied; a warning is emitted. SYN limiter rules are process-owned and are
|
||||
|
||||
@@ -30,6 +30,23 @@ fn write_reload_config(path: &Path, ad_tag: Option<&str>, server_port: Option<u1
|
||||
std::fs::write(path, config).unwrap();
|
||||
}
|
||||
|
||||
fn write_web_reload_config(path: &Path, carriers: &str, carrier_learning: bool) {
|
||||
let config = format!(
|
||||
r#"
|
||||
[censorship]
|
||||
tls_domain = "example.com"
|
||||
|
||||
[access.users]
|
||||
user = "00000000000000000000000000000000"
|
||||
|
||||
[web]
|
||||
carriers = {carriers}
|
||||
carrier_learning = {carrier_learning}
|
||||
"#,
|
||||
);
|
||||
std::fs::write(path, config).unwrap();
|
||||
}
|
||||
|
||||
fn temp_config_path(prefix: &str) -> PathBuf {
|
||||
let nonce = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
@@ -264,6 +281,29 @@ fn reload_keeps_hot_apply_when_non_hot_fields_change() {
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reload_publishes_web_negotiation_policy_outside_hot_field_reporting() {
|
||||
let path = temp_config_path("telemt_web_negotiation_reload");
|
||||
|
||||
write_web_reload_config(&path, "false", true);
|
||||
let initial_cfg = Arc::new(ProxyConfig::load(&path).unwrap());
|
||||
let initial_hash = ProxyConfig::load_with_metadata(&path)
|
||||
.unwrap()
|
||||
.rendered_hash;
|
||||
let (config_tx, _config_rx) = watch::channel(initial_cfg.clone());
|
||||
let (log_tx, _log_rx) = watch::channel(initial_cfg.general.log_level.clone());
|
||||
let mut reload_state = ReloadState::new(Some(initial_hash));
|
||||
|
||||
write_web_reload_config(&path, "[\"websocket\", \"https\"]", false);
|
||||
reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state).unwrap();
|
||||
|
||||
let applied = config_tx.borrow().clone();
|
||||
assert!(applied.web.carrier_negotiation_enabled());
|
||||
assert!(!applied.web.carrier_learning);
|
||||
|
||||
let _ = std::fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_sni_change_requires_restart() {
|
||||
// censorship.* is not in overlay_hot_fields -> restart.
|
||||
|
||||
@@ -164,7 +164,7 @@ pub(super) fn reload_config(
|
||||
let old_hot = HotFields::from_config(&old_cfg);
|
||||
let applied_hot = HotFields::from_config(&applied_cfg);
|
||||
let non_hot_changed = !config_equal(&applied_cfg, &new_cfg);
|
||||
let hot_changed = old_hot != applied_hot;
|
||||
let hot_changed = !config_equal(&old_cfg, &applied_cfg);
|
||||
|
||||
if non_hot_changed {
|
||||
warn_non_hot_changes(&old_cfg, &new_cfg, non_hot_changed);
|
||||
|
||||
@@ -27,6 +27,7 @@ pub(super) fn rebuild(config: &mut ProxyConfig) -> Result<()> {
|
||||
let mut static_files = 0usize;
|
||||
let mut static_bytes = 0usize;
|
||||
|
||||
let carrier_candidates: Arc<[WebCarrier]> = config.web.carrier_candidates().into();
|
||||
for vhost in &config.web.vhosts {
|
||||
let decoy = build_decoy(
|
||||
vhost,
|
||||
@@ -63,6 +64,13 @@ pub(super) fn rebuild(config: &mut ProxyConfig) -> Result<()> {
|
||||
user: profile.user.clone(),
|
||||
secret_mode: profile.secret_mode,
|
||||
carrier: config.web.carrier,
|
||||
carrier_negotiation_enabled: config.web.carrier_negotiation_enabled(),
|
||||
carrier_learning: config.web.carrier_learning,
|
||||
carriers: Arc::clone(&carrier_candidates),
|
||||
carrier_negotiation_deadlines_secs: config
|
||||
.web
|
||||
.timeouts
|
||||
.carrier_negotiation_deadlines_secs,
|
||||
capability,
|
||||
key_fingerprint,
|
||||
max_sessions: profile
|
||||
|
||||
@@ -260,7 +260,14 @@ const LISTENER_CONFIG_KEYS: &[&str] = &[
|
||||
];
|
||||
|
||||
const WEB_CONFIG_KEYS: &[&str] = &[
|
||||
"enabled", "carrier", "debug", "limits", "timeouts", "vhosts",
|
||||
"enabled",
|
||||
"carrier",
|
||||
"carriers",
|
||||
"carrier_learning",
|
||||
"debug",
|
||||
"limits",
|
||||
"timeouts",
|
||||
"vhosts",
|
||||
];
|
||||
|
||||
const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[
|
||||
@@ -275,6 +282,7 @@ const WEB_LIMITS_CONFIG_KEYS: &[&str] = &[
|
||||
"websocket_admission_watermark_pct",
|
||||
"websocket_eviction_watermark_pct",
|
||||
"websocket_http_connection_reserve",
|
||||
"max_carrier_learning_entries",
|
||||
"max_body_readers",
|
||||
"max_body_bytes_global",
|
||||
"max_sessions_global",
|
||||
@@ -328,6 +336,8 @@ const WEB_TIMEOUTS_CONFIG_KEYS: &[&str] = &[
|
||||
"websocket_write_secs",
|
||||
"websocket_backpressure_secs",
|
||||
"websocket_eviction_secs",
|
||||
"carrier_negotiation_deadlines_secs",
|
||||
"carrier_learning_secs",
|
||||
"bootstrap_lifetime_secs",
|
||||
"reconnect_grace_secs",
|
||||
"http_idle_secs",
|
||||
|
||||
@@ -6,6 +6,10 @@ use super::*;
|
||||
mod debug;
|
||||
// Memory-envelope arithmetic remains isolated from protocol validation.
|
||||
mod memory;
|
||||
// Carrier ordering, cumulative deadlines, and fallback identity are validated together.
|
||||
mod negotiation;
|
||||
// Request and lifecycle timeout relationships are validated together.
|
||||
mod timeouts;
|
||||
// WebSocket transport policy is validated independently from HTTP body policy.
|
||||
mod websocket;
|
||||
|
||||
@@ -67,11 +71,14 @@ pub(super) fn validate(config: &mut ProxyConfig) -> Result<()> {
|
||||
|
||||
validate_limits(&config.web.limits)?;
|
||||
debug::validate(&config.web.debug, &config.web.limits)?;
|
||||
if config.web.carrier == WebCarrier::HttpsLanes && config.web.limits.max_http_handlers < 2 {
|
||||
return config_error("web.carrier=https-lanes requires web.limits.max_http_handlers >= 2");
|
||||
let carriers = negotiation::validate(&config.web)?;
|
||||
if carriers.contains(&WebCarrier::HttpsLanes) && config.web.limits.max_http_handlers < 2 {
|
||||
return config_error(
|
||||
"WEB https-lanes candidates require web.limits.max_http_handlers >= 2",
|
||||
);
|
||||
}
|
||||
validate_timeouts(&config.web.timeouts)?;
|
||||
websocket::validate(config.web.carrier, &config.web.limits, &config.web.timeouts)?;
|
||||
timeouts::validate(&config.web.timeouts)?;
|
||||
websocket::validate(&carriers, &config.web.limits, &config.web.timeouts)?;
|
||||
validate_vhosts(config)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -155,6 +162,10 @@ fn validate_limits(limits: &WebLimitsConfig) -> Result<()> {
|
||||
let positive = [
|
||||
("max_http_connections", limits.max_http_connections),
|
||||
("max_http_handlers", limits.max_http_handlers),
|
||||
(
|
||||
"max_carrier_learning_entries",
|
||||
limits.max_carrier_learning_entries,
|
||||
),
|
||||
("max_body_readers", limits.max_body_readers),
|
||||
("max_body_bytes_global", limits.max_body_bytes_global),
|
||||
("max_sessions_global", limits.max_sessions_global),
|
||||
@@ -324,41 +335,6 @@ fn validate_limits(limits: &WebLimitsConfig) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_timeouts(timeouts: &WebTimeoutsConfig) -> Result<()> {
|
||||
let values = [
|
||||
("header_secs", timeouts.header_secs),
|
||||
("body_secs", timeouts.body_secs),
|
||||
("stream_handshake_secs", timeouts.stream_handshake_secs),
|
||||
("long_poll_secs", timeouts.long_poll_secs),
|
||||
("websocket_write_secs", timeouts.websocket_write_secs),
|
||||
(
|
||||
"websocket_backpressure_secs",
|
||||
timeouts.websocket_backpressure_secs,
|
||||
),
|
||||
("websocket_eviction_secs", timeouts.websocket_eviction_secs),
|
||||
("bootstrap_lifetime_secs", timeouts.bootstrap_lifetime_secs),
|
||||
("reconnect_grace_secs", timeouts.reconnect_grace_secs),
|
||||
("http_idle_secs", timeouts.http_idle_secs),
|
||||
("shutdown_secs", timeouts.shutdown_secs),
|
||||
("decoy_header_secs", timeouts.decoy_header_secs),
|
||||
];
|
||||
if let Some((field, _)) = values
|
||||
.into_iter()
|
||||
.find(|(_, value)| !(1..=3600).contains(value))
|
||||
{
|
||||
return config_error(&format!("web.timeouts.{field} must be within [1, 3600]"));
|
||||
}
|
||||
let request_deadline = timeouts
|
||||
.header_secs
|
||||
.max(timeouts.body_secs)
|
||||
.max(timeouts.long_poll_secs)
|
||||
.max(timeouts.decoy_header_secs);
|
||||
if request_deadline >= timeouts.http_idle_secs {
|
||||
return config_error("web.timeouts request deadlines must be lower than http_idle_secs");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_vhosts(config: &mut ProxyConfig) -> Result<()> {
|
||||
let limits = &config.web.limits;
|
||||
if config.web.vhosts.len() > limits.max_vhosts {
|
||||
|
||||
@@ -3,9 +3,13 @@ use super::*;
|
||||
const WEB_DEBUG_RENDERERS: usize = 2;
|
||||
const WEB_DEBUG_STATUS_PAGE_BYTES: usize = 8 * 1024 * 1024;
|
||||
const WEB_DEBUG_GROUP_SCRATCH_BYTES: usize = 4 * 1024 * 1024;
|
||||
const WEB_CARRIER_LEARNING_ENTRY_BYTES: usize = 256;
|
||||
|
||||
/// Validates process-wide body, header, queue, static, and debug reservations.
|
||||
pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
|
||||
if limits.max_carrier_learning_entries == 0 {
|
||||
return config_error("web.limits.max_carrier_learning_entries must be > 0");
|
||||
}
|
||||
let body_reservation = limits
|
||||
.max_body_readers
|
||||
.checked_mul(limits.max_body_bytes)
|
||||
@@ -47,6 +51,12 @@ pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
|
||||
.and_then(|scratch| value.checked_add(scratch))
|
||||
})
|
||||
.ok_or_else(|| ProxyError::Config("web.debug reservations overflowed usize".to_string()))?;
|
||||
let carrier_learning_reservation = limits
|
||||
.max_carrier_learning_entries
|
||||
.checked_mul(WEB_CARRIER_LEARNING_ENTRY_BYTES)
|
||||
.ok_or_else(|| {
|
||||
ProxyError::Config("web.carrier learning reservation overflowed usize".to_string())
|
||||
})?;
|
||||
let reserved = limits
|
||||
.pending_bytes_global
|
||||
.checked_add(limits.max_body_bytes_global)
|
||||
@@ -54,6 +64,7 @@ pub(super) fn validate(limits: &WebLimitsConfig) -> Result<()> {
|
||||
.and_then(|value| value.checked_add(debug_ring_index))
|
||||
.and_then(|value| value.checked_add(status_pages))
|
||||
.and_then(|value| value.checked_add(debug_reservation))
|
||||
.and_then(|value| value.checked_add(carrier_learning_reservation))
|
||||
.and_then(|value| value.checked_add(http_header_reservation))
|
||||
.ok_or_else(|| ProxyError::Config("web.limits byte ceilings overflow usize".to_string()))?;
|
||||
if reserved > limits.memory_envelope_bytes
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::*;
|
||||
|
||||
/// Validates bounded carrier selection and learning policy.
|
||||
pub(super) fn validate(config: &WebConfig) -> Result<Vec<WebCarrier>> {
|
||||
if let Some(carriers) = config.carriers.enabled() {
|
||||
if carriers.is_empty() {
|
||||
return config_error("web.carriers must contain at least one carrier");
|
||||
}
|
||||
let mut unique = HashSet::with_capacity(carriers.len());
|
||||
if carriers.iter().any(|carrier| !unique.insert(*carrier)) {
|
||||
return config_error("web.carriers must not contain duplicate carriers");
|
||||
}
|
||||
}
|
||||
let candidates = config.carrier_candidates();
|
||||
if candidates.len() > WebCarrier::ALL.len() {
|
||||
return config_error(
|
||||
"web.carriers and the web.carrier fallback must contain at most four carriers",
|
||||
);
|
||||
}
|
||||
let deadlines = config.timeouts.carrier_negotiation_deadlines_secs;
|
||||
if deadlines[0] == 0 || deadlines.windows(2).any(|pair| pair[0] >= pair[1]) {
|
||||
return config_error(
|
||||
"web.timeouts.carrier_negotiation_deadlines_secs must be non-zero and strictly increasing",
|
||||
);
|
||||
}
|
||||
if deadlines[3] > config.timeouts.bootstrap_lifetime_secs {
|
||||
return config_error(
|
||||
"web.timeouts carrier negotiation deadline must not exceed bootstrap_lifetime_secs",
|
||||
);
|
||||
}
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fallback_is_appended_once() {
|
||||
let config = WebConfig {
|
||||
carrier: WebCarrier::Https,
|
||||
carriers: WebCarriers::Enabled(vec![WebCarrier::Websocket, WebCarrier::Https]),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
validate(&config).unwrap(),
|
||||
vec![WebCarrier::Websocket, WebCarrier::Https]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_carriers_are_rejected() {
|
||||
let config = WebConfig {
|
||||
carriers: WebCarriers::Enabled(vec![WebCarrier::Websocket, WebCarrier::Websocket]),
|
||||
..Default::default()
|
||||
};
|
||||
assert!(validate(&config).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_or_false_carriers_disable_negotiation() {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct Wrapper {
|
||||
value: WebCarriers,
|
||||
}
|
||||
|
||||
let config = WebConfig::default();
|
||||
assert!(!config.carrier_negotiation_enabled());
|
||||
assert_eq!(validate(&config).unwrap(), [WebCarrier::Https]);
|
||||
|
||||
let disabled: Wrapper = toml::from_str("value = false").unwrap();
|
||||
assert_eq!(disabled.value, WebCarriers::Disabled);
|
||||
assert!(toml::from_str::<Wrapper>("value = true").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_cannot_expand_the_candidate_set_beyond_four() {
|
||||
let mut config = WebConfig {
|
||||
carrier: WebCarrier::Https,
|
||||
carriers: WebCarriers::Enabled(vec![
|
||||
WebCarrier::HttpsLanes,
|
||||
WebCarrier::Websocket,
|
||||
WebCarrier::WebsocketLanes,
|
||||
WebCarrier::Https,
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(validate(&config).unwrap().len(), 4);
|
||||
|
||||
config.carriers = WebCarriers::Enabled(vec![
|
||||
WebCarrier::HttpsLanes,
|
||||
WebCarrier::Websocket,
|
||||
WebCarrier::WebsocketLanes,
|
||||
]);
|
||||
assert_eq!(validate(&config).unwrap().len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deadlines_are_cumulative_and_bounded_by_bootstrap_lifetime() {
|
||||
let mut config = WebConfig::default();
|
||||
config.timeouts.carrier_negotiation_deadlines_secs = [3, 3, 8, 12];
|
||||
assert!(validate(&config).is_err());
|
||||
config.timeouts.carrier_negotiation_deadlines_secs = [3, 5, 8, 121];
|
||||
assert!(validate(&config).is_err());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use super::*;
|
||||
|
||||
/// Validates WEB request, learning, and lifecycle timeouts.
|
||||
pub(super) fn validate(timeouts: &WebTimeoutsConfig) -> Result<()> {
|
||||
let values = [
|
||||
("header_secs", timeouts.header_secs),
|
||||
("body_secs", timeouts.body_secs),
|
||||
("stream_handshake_secs", timeouts.stream_handshake_secs),
|
||||
("long_poll_secs", timeouts.long_poll_secs),
|
||||
("websocket_write_secs", timeouts.websocket_write_secs),
|
||||
(
|
||||
"websocket_backpressure_secs",
|
||||
timeouts.websocket_backpressure_secs,
|
||||
),
|
||||
("websocket_eviction_secs", timeouts.websocket_eviction_secs),
|
||||
("bootstrap_lifetime_secs", timeouts.bootstrap_lifetime_secs),
|
||||
("reconnect_grace_secs", timeouts.reconnect_grace_secs),
|
||||
("http_idle_secs", timeouts.http_idle_secs),
|
||||
("shutdown_secs", timeouts.shutdown_secs),
|
||||
("decoy_header_secs", timeouts.decoy_header_secs),
|
||||
];
|
||||
if let Some((field, _)) = values
|
||||
.into_iter()
|
||||
.find(|(_, value)| !(1..=3600).contains(value))
|
||||
{
|
||||
return config_error(&format!("web.timeouts.{field} must be within [1, 3600]"));
|
||||
}
|
||||
if timeouts.carrier_learning_secs == 0 {
|
||||
return config_error("web.timeouts.carrier_learning_secs must be > 0");
|
||||
}
|
||||
let request_deadline = timeouts
|
||||
.header_secs
|
||||
.max(timeouts.body_secs)
|
||||
.max(timeouts.long_poll_secs)
|
||||
.max(timeouts.decoy_header_secs);
|
||||
if request_deadline >= timeouts.http_idle_secs {
|
||||
return config_error("web.timeouts request deadlines must be lower than http_idle_secs");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -7,7 +7,7 @@ const WEBSOCKET_FRAME_OVERHEAD_BYTES: usize = 14;
|
||||
|
||||
/// Validates WebSocket admission, memory, and deadline invariants.
|
||||
pub(super) fn validate(
|
||||
carrier: WebCarrier,
|
||||
carriers: &[WebCarrier],
|
||||
limits: &WebLimitsConfig,
|
||||
timeouts: &WebTimeoutsConfig,
|
||||
) -> Result<()> {
|
||||
@@ -27,7 +27,7 @@ pub(super) fn validate(
|
||||
"web.timeouts.websocket_eviction_secs must not exceed websocket_write_secs",
|
||||
);
|
||||
}
|
||||
if !carrier.uses_websocket() {
|
||||
if !carriers.iter().any(|carrier| carrier.uses_websocket()) {
|
||||
return Ok(());
|
||||
}
|
||||
if limits.carrier_batch_bytes > MAX_WEBSOCKET_BATCH_BYTES {
|
||||
|
||||
@@ -49,6 +49,87 @@ fn web_config_builds_canonical_runtime_snapshot() {
|
||||
assert_eq!(vhost.profiles[0].max_streams_per_session, 16);
|
||||
assert_eq!(vhost.profiles[0].key_fingerprint.len(), 16);
|
||||
assert_ne!(vhost.profiles[0].key_fingerprint, "0001020304050607");
|
||||
assert!(!vhost.profiles[0].carrier_negotiation_enabled);
|
||||
assert_eq!(vhost.profiles[0].carriers.as_ref(), [WebCarrier::HttpsLanes]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_carriers_missing_or_false_disable_negotiation() {
|
||||
let missing = load_config_from_temp_toml(WEB_CONFIG);
|
||||
assert!(!missing.web.carrier_negotiation_enabled());
|
||||
|
||||
let disabled = WEB_CONFIG.replace(
|
||||
"carrier = \"https-lanes\"",
|
||||
"carrier = \"https-lanes\"\ncarriers = false",
|
||||
);
|
||||
let disabled = load_config_from_temp_toml(&disabled);
|
||||
assert!(!disabled.web.carrier_negotiation_enabled());
|
||||
assert_eq!(
|
||||
disabled.web.runtime.unwrap().profiles[0].carriers.as_ref(),
|
||||
[WebCarrier::HttpsLanes]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_carrier_array_enables_ordered_negotiation_and_appends_fallback() {
|
||||
let configured = WEB_CONFIG.replace(
|
||||
"carrier = \"https-lanes\"",
|
||||
"carrier = \"https-lanes\"\ncarriers = [\"websocket\", \"https\"]\ncarrier_learning = false",
|
||||
);
|
||||
let config = load_config_from_temp_toml(&configured);
|
||||
assert!(config.web.carrier_negotiation_enabled());
|
||||
assert!(!config.web.carrier_learning);
|
||||
let profile = &config.web.runtime.unwrap().profiles[0];
|
||||
assert_eq!(
|
||||
profile.carriers.as_ref(),
|
||||
[
|
||||
WebCarrier::Websocket,
|
||||
WebCarrier::Https,
|
||||
WebCarrier::HttpsLanes
|
||||
]
|
||||
);
|
||||
assert!(!profile.carrier_learning);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_carriers_reject_true_empty_and_duplicates() {
|
||||
for value in [
|
||||
"true",
|
||||
"[]",
|
||||
"[\"https\", \"https\"]",
|
||||
] {
|
||||
let invalid = WEB_CONFIG.replace(
|
||||
"carrier = \"https-lanes\"",
|
||||
&format!("carrier = \"https-lanes\"\ncarriers = {value}"),
|
||||
);
|
||||
assert!(load_config_error_from_temp_toml(&invalid).contains("web.carriers"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_carrier_deadlines_and_learning_window_are_configurable() {
|
||||
let configured = WEB_CONFIG.replace(
|
||||
"[[web.vhosts]]",
|
||||
"[web.timeouts]\ncarrier_negotiation_deadlines_secs = [1, 2, 4, 9]\ncarrier_learning_secs = 30\n\n[[web.vhosts]]",
|
||||
);
|
||||
let config = load_config_from_temp_toml(&configured);
|
||||
assert_eq!(
|
||||
config.web.timeouts.carrier_negotiation_deadlines_secs,
|
||||
[1, 2, 4, 9]
|
||||
);
|
||||
assert_eq!(config.web.timeouts.carrier_learning_secs, 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_carrier_learning_capacity_must_remain_nonzero() {
|
||||
let invalid = WEB_CONFIG.replace(
|
||||
"[[web.vhosts]]",
|
||||
"[web.limits]\nmax_carrier_learning_entries = 0\n\n[[web.vhosts]]",
|
||||
);
|
||||
assert!(
|
||||
load_config_error_from_temp_toml(&invalid)
|
||||
.contains("web.limits.max_carrier_learning_entries")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -106,7 +187,7 @@ fn https_lanes_requires_separate_poll_and_control_handler_capacity() {
|
||||
"carrier = \"https-lanes\"\n\n[web.limits]\nmax_http_handlers = 1\nmax_body_readers = 1",
|
||||
);
|
||||
let error = load_config_error_from_temp_toml(&invalid);
|
||||
assert!(error.contains("web.carrier=https-lanes requires"));
|
||||
assert!(error.contains("WEB https-lanes candidates require"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+6
-2
@@ -24,6 +24,8 @@ mod network;
|
||||
mod policies;
|
||||
mod server;
|
||||
mod web;
|
||||
// WEB carrier tokens and fixed-slot policy helpers remain independent from bulky config types.
|
||||
mod web_carrier;
|
||||
// WEB debug capture policy is reusable by config reload and process storage.
|
||||
mod web_debug;
|
||||
|
||||
@@ -51,9 +53,11 @@ pub use server::{
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
pub use web::{
|
||||
WebCarrier, WebConfig, WebDecoyConfig, WebLimitsConfig, WebProfileConfig, WebSecretMode,
|
||||
WebTimeoutsConfig, WebVhostConfig,
|
||||
WebConfig, WebDecoyConfig, WebLimitsConfig, WebProfileConfig, WebSecretMode, WebTimeoutsConfig,
|
||||
WebVhostConfig,
|
||||
};
|
||||
#[allow(unused_imports)]
|
||||
pub use web_carrier::{WebCarrier, WebCarriers};
|
||||
pub(crate) use web::{
|
||||
WebRuntimeConfig, WebRuntimeDecoy, WebRuntimeProfile, WebRuntimeVhost, WebStaticAsset,
|
||||
WebStaticSite,
|
||||
|
||||
+70
-135
@@ -6,8 +6,13 @@ use std::sync::Arc;
|
||||
use bytes::Bytes;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::web_carrier::{WebCarrier, WebCarriers};
|
||||
use super::web_debug::WebDebugConfig;
|
||||
|
||||
// Serialized WEB defaults remain separate from the runtime data model.
|
||||
mod defaults;
|
||||
use defaults::*;
|
||||
|
||||
/// Client-facing secret representation used to derive a WEB capability.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -18,48 +23,6 @@ pub enum WebSecretMode {
|
||||
Dd,
|
||||
}
|
||||
|
||||
/// Carrier selected for newly issued WEB bridge sessions.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum WebCarrier {
|
||||
/// Serialize all logical streams through one uplink and one downlink sequence.
|
||||
#[default]
|
||||
Https,
|
||||
/// Give every logical stream independent HTTPS sequencing and polling state.
|
||||
HttpsLanes,
|
||||
/// Multiplex all logical streams over one ordered WebSocket.
|
||||
Websocket,
|
||||
/// Give every logical stream an independently owned WebSocket lane.
|
||||
WebsocketLanes,
|
||||
}
|
||||
|
||||
impl WebCarrier {
|
||||
/// Returns the exact carrier token advertised to the browser bridge.
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Https => "https",
|
||||
Self::HttpsLanes => "https-lanes",
|
||||
Self::Websocket => "websocket",
|
||||
Self::WebsocketLanes => "websocket-lanes",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether one carrier owns independent state per logical stream.
|
||||
pub(crate) const fn uses_lanes(self) -> bool {
|
||||
matches!(self, Self::HttpsLanes | Self::WebsocketLanes)
|
||||
}
|
||||
|
||||
/// Returns whether carrier messages use RFC 6455 instead of HTTP bodies.
|
||||
pub(crate) const fn uses_websocket(self) -> bool {
|
||||
matches!(self, Self::Websocket | Self::WebsocketLanes)
|
||||
}
|
||||
|
||||
/// Returns whether all logical streams share one carrier state machine.
|
||||
pub(crate) const fn is_multiplexed(self) -> bool {
|
||||
matches!(self, Self::Https | Self::Websocket)
|
||||
}
|
||||
}
|
||||
|
||||
/// One access user explicitly exposed through a WEB virtual host.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct WebProfileConfig {
|
||||
@@ -147,6 +110,9 @@ pub struct WebLimitsConfig {
|
||||
/// Accepted HTTP connections that WebSocket upgrades must leave available.
|
||||
#[serde(default = "default_web_websocket_http_connection_reserve")]
|
||||
pub websocket_http_connection_reserve: usize,
|
||||
/// Process-wide bounded carrier-learning evidence entry ceiling.
|
||||
#[serde(default = "default_web_max_carrier_learning_entries")]
|
||||
pub max_carrier_learning_entries: usize,
|
||||
/// Process-wide concurrently collected request body ceiling.
|
||||
#[serde(default = "default_web_max_body_readers")]
|
||||
pub max_body_readers: usize,
|
||||
@@ -253,6 +219,7 @@ impl Default for WebLimitsConfig {
|
||||
websocket_admission_watermark_pct: default_web_websocket_admission_watermark_pct(),
|
||||
websocket_eviction_watermark_pct: default_web_websocket_eviction_watermark_pct(),
|
||||
websocket_http_connection_reserve: default_web_websocket_http_connection_reserve(),
|
||||
max_carrier_learning_entries: default_web_max_carrier_learning_entries(),
|
||||
max_body_readers: default_web_max_body_readers(),
|
||||
max_body_bytes_global: default_web_max_body_bytes_global(),
|
||||
max_sessions_global: default_web_max_sessions_global(),
|
||||
@@ -311,6 +278,12 @@ pub struct WebTimeoutsConfig {
|
||||
/// Maximum graceful close wait for an evicted WebSocket.
|
||||
#[serde(default = "default_web_websocket_eviction_secs")]
|
||||
pub websocket_eviction_secs: u64,
|
||||
/// Cumulative carrier-attempt deadlines for up to four unique candidates.
|
||||
#[serde(default = "default_web_carrier_negotiation_deadlines_secs")]
|
||||
pub carrier_negotiation_deadlines_secs: [u64; 4],
|
||||
/// Fixed process-local carrier-learning evidence lifetime.
|
||||
#[serde(default = "default_web_carrier_learning_secs")]
|
||||
pub carrier_learning_secs: u64,
|
||||
/// Lifetime of an unused bootstrap credential and closed-token replay marker.
|
||||
#[serde(default = "default_web_bootstrap_lifetime_secs")]
|
||||
pub bootstrap_lifetime_secs: u64,
|
||||
@@ -338,6 +311,9 @@ impl Default for WebTimeoutsConfig {
|
||||
websocket_write_secs: default_web_websocket_write_secs(),
|
||||
websocket_backpressure_secs: default_web_websocket_backpressure_secs(),
|
||||
websocket_eviction_secs: default_web_websocket_eviction_secs(),
|
||||
carrier_negotiation_deadlines_secs:
|
||||
default_web_carrier_negotiation_deadlines_secs(),
|
||||
carrier_learning_secs: default_web_carrier_learning_secs(),
|
||||
bootstrap_lifetime_secs: default_web_bootstrap_lifetime_secs(),
|
||||
reconnect_grace_secs: default_web_reconnect_grace_secs(),
|
||||
http_idle_secs: default_web_http_idle_secs(),
|
||||
@@ -348,14 +324,20 @@ impl Default for WebTimeoutsConfig {
|
||||
}
|
||||
|
||||
/// WEB ingress, carrier, fallback, and lifecycle configuration.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WebConfig {
|
||||
/// Enables issuance of new WEB bridge and session credentials.
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
/// Carrier selected for newly issued WEB bridge sessions.
|
||||
/// Sole carrier when negotiation is disabled and final fallback when enabled.
|
||||
#[serde(default)]
|
||||
pub carrier: WebCarrier,
|
||||
/// Ordered carriers considered by server-side negotiation before the fallback carrier.
|
||||
#[serde(default)]
|
||||
pub carriers: WebCarriers,
|
||||
/// Enables bounded process-local carrier learning for automatic sessions.
|
||||
#[serde(default = "default_web_carrier_learning")]
|
||||
pub carrier_learning: bool,
|
||||
/// Hard process and protocol limits.
|
||||
#[serde(default)]
|
||||
pub limits: WebLimitsConfig,
|
||||
@@ -373,6 +355,41 @@ pub struct WebConfig {
|
||||
pub(crate) runtime: Option<Arc<WebRuntimeConfig>>,
|
||||
}
|
||||
|
||||
impl WebConfig {
|
||||
/// Returns the configured negotiation order with the fallback appended once.
|
||||
pub(crate) fn carrier_candidates(&self) -> Vec<WebCarrier> {
|
||||
let Some(configured) = self.carriers.enabled() else {
|
||||
return vec![self.carrier];
|
||||
};
|
||||
let mut candidates = configured.to_vec();
|
||||
if !candidates.contains(&self.carrier) {
|
||||
candidates.push(self.carrier);
|
||||
}
|
||||
candidates
|
||||
}
|
||||
|
||||
/// Returns whether the explicit candidate list enables auto-negotiation.
|
||||
pub(crate) fn carrier_negotiation_enabled(&self) -> bool {
|
||||
self.carriers.enabled().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WebConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
carrier: WebCarrier::default(),
|
||||
carriers: WebCarriers::default(),
|
||||
carrier_learning: default_web_carrier_learning(),
|
||||
limits: WebLimitsConfig::default(),
|
||||
debug: WebDebugConfig::default(),
|
||||
timeouts: WebTimeoutsConfig::default(),
|
||||
vhosts: Vec::new(),
|
||||
runtime: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Precomputed WEB configuration consumed by listener hot paths.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct WebRuntimeConfig {
|
||||
@@ -406,8 +423,16 @@ pub(crate) struct WebRuntimeProfile {
|
||||
pub(crate) user: String,
|
||||
/// Client secret representation and inner protocol policy.
|
||||
pub(crate) secret_mode: WebSecretMode,
|
||||
/// Carrier frozen into bridge and session state at issuance time.
|
||||
/// Sole carrier or final fallback frozen into the issued bridge policy.
|
||||
pub(crate) carrier: WebCarrier,
|
||||
/// Whether an explicit carrier list enabled automatic negotiation.
|
||||
pub(crate) carrier_negotiation_enabled: bool,
|
||||
/// Whether automatic outcomes consult and update process-local evidence.
|
||||
pub(crate) carrier_learning: bool,
|
||||
/// Ordered negotiation candidates including the fallback carrier exactly once.
|
||||
pub(crate) carriers: Arc<[WebCarrier]>,
|
||||
/// Cumulative carrier-attempt deadlines frozen when the bridge is issued.
|
||||
pub(crate) carrier_negotiation_deadlines_secs: [u64; 4],
|
||||
/// HMAC-derived bridge capability.
|
||||
pub(crate) capability: [u8; 32],
|
||||
/// Non-secret domain-separated client-secret fingerprint for debugging.
|
||||
@@ -446,93 +471,3 @@ pub(crate) struct WebStaticAsset {
|
||||
/// Strong SHA-256 entity tag.
|
||||
pub(crate) etag: String,
|
||||
}
|
||||
|
||||
fn default_web_static_index() -> String {
|
||||
"index.html".to_string()
|
||||
}
|
||||
|
||||
macro_rules! usize_default {
|
||||
($name:ident, $value:expr) => {
|
||||
fn $name() -> usize {
|
||||
$value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! u32_default {
|
||||
($name:ident, $value:expr) => {
|
||||
fn $name() -> u32 {
|
||||
$value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! u8_default {
|
||||
($name:ident, $value:expr) => {
|
||||
fn $name() -> u8 {
|
||||
$value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! u64_default {
|
||||
($name:ident, $value:expr) => {
|
||||
fn $name() -> u64 {
|
||||
$value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
usize_default!(default_web_max_header_bytes, 16 * 1024);
|
||||
usize_default!(default_web_max_body_bytes, 2 * 1024 * 1024);
|
||||
usize_default!(default_web_max_frame_payload_bytes, 1024 * 1024);
|
||||
usize_default!(default_web_carrier_batch_bytes, 2 * 1024 * 1024);
|
||||
usize_default!(default_web_max_frames_per_body, 4096);
|
||||
usize_default!(default_web_max_http_connections, 1024);
|
||||
usize_default!(default_web_max_http_handlers, 512);
|
||||
usize_default!(default_web_websocket_bytes_global, 256 * 1024 * 1024);
|
||||
u8_default!(default_web_websocket_admission_watermark_pct, 75);
|
||||
u8_default!(default_web_websocket_eviction_watermark_pct, 90);
|
||||
usize_default!(default_web_websocket_http_connection_reserve, 64);
|
||||
usize_default!(default_web_max_body_readers, 32);
|
||||
usize_default!(default_web_max_body_bytes_global, 64 * 1024 * 1024);
|
||||
usize_default!(default_web_max_sessions_global, 128);
|
||||
usize_default!(default_web_max_sessions_per_ip, 16);
|
||||
usize_default!(default_web_max_streams_per_session, 128);
|
||||
usize_default!(default_web_max_streams_global, 4096);
|
||||
usize_default!(default_web_max_stream_handshakes, 256);
|
||||
usize_default!(default_web_max_tombstones, 4096);
|
||||
usize_default!(default_web_pending_bytes_per_session, 32 * 1024 * 1024);
|
||||
usize_default!(default_web_pending_bytes_global, 512 * 1024 * 1024);
|
||||
usize_default!(default_web_pending_items_per_session, 16 * 1024);
|
||||
usize_default!(default_web_pending_items_global, 256 * 1024);
|
||||
usize_default!(default_web_control_bytes_per_session, 256 * 1024);
|
||||
usize_default!(default_web_control_bytes_global, 16 * 1024 * 1024);
|
||||
usize_default!(default_web_max_bootstraps_global, 512);
|
||||
usize_default!(default_web_max_bootstraps_per_ip, 64);
|
||||
usize_default!(default_web_max_vhosts, 8);
|
||||
usize_default!(default_web_max_profiles, 32);
|
||||
usize_default!(default_web_max_static_files, 4096);
|
||||
usize_default!(default_web_max_static_file_bytes, 8 * 1024 * 1024);
|
||||
usize_default!(default_web_max_static_bytes, 64 * 1024 * 1024);
|
||||
usize_default!(default_web_debug_records_capacity, 65_536);
|
||||
usize_default!(default_web_debug_bytes_global, 64 * 1024 * 1024);
|
||||
usize_default!(default_web_memory_envelope_bytes, 768 * 1024 * 1024);
|
||||
u32_default!(default_web_new_bootstraps_per_minute, 1200);
|
||||
u32_default!(default_web_new_bootstraps_burst, 256);
|
||||
u32_default!(default_web_new_sessions_per_minute, 600);
|
||||
u32_default!(default_web_new_sessions_burst, 128);
|
||||
u32_default!(default_web_new_streams_per_minute, 6000);
|
||||
u32_default!(default_web_new_streams_burst, 512);
|
||||
u64_default!(default_web_header_timeout_secs, 10);
|
||||
u64_default!(default_web_body_timeout_secs, 30);
|
||||
u64_default!(default_web_stream_handshake_timeout_secs, 10);
|
||||
u64_default!(default_web_long_poll_timeout_secs, 25);
|
||||
u64_default!(default_web_websocket_write_secs, 30);
|
||||
u64_default!(default_web_websocket_backpressure_secs, 30);
|
||||
u64_default!(default_web_websocket_eviction_secs, 1);
|
||||
u64_default!(default_web_bootstrap_lifetime_secs, 120);
|
||||
u64_default!(default_web_reconnect_grace_secs, 120);
|
||||
u64_default!(default_web_http_idle_secs, 75);
|
||||
u64_default!(default_web_shutdown_secs, 15);
|
||||
u64_default!(default_web_decoy_header_timeout_secs, 30);
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
pub(super) fn default_web_static_index() -> String {
|
||||
"index.html".to_string()
|
||||
}
|
||||
|
||||
macro_rules! usize_default {
|
||||
($name:ident, $value:expr) => {
|
||||
pub(super) fn $name() -> usize {
|
||||
$value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! u32_default {
|
||||
($name:ident, $value:expr) => {
|
||||
pub(super) fn $name() -> u32 {
|
||||
$value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! u8_default {
|
||||
($name:ident, $value:expr) => {
|
||||
pub(super) fn $name() -> u8 {
|
||||
$value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! u64_default {
|
||||
($name:ident, $value:expr) => {
|
||||
pub(super) fn $name() -> u64 {
|
||||
$value
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
usize_default!(default_web_max_header_bytes, 16 * 1024);
|
||||
usize_default!(default_web_max_body_bytes, 2 * 1024 * 1024);
|
||||
usize_default!(default_web_max_frame_payload_bytes, 1024 * 1024);
|
||||
usize_default!(default_web_carrier_batch_bytes, 2 * 1024 * 1024);
|
||||
usize_default!(default_web_max_frames_per_body, 4096);
|
||||
usize_default!(default_web_max_http_connections, 1024);
|
||||
usize_default!(default_web_max_http_handlers, 512);
|
||||
usize_default!(default_web_websocket_bytes_global, 256 * 1024 * 1024);
|
||||
u8_default!(default_web_websocket_admission_watermark_pct, 75);
|
||||
u8_default!(default_web_websocket_eviction_watermark_pct, 90);
|
||||
usize_default!(default_web_websocket_http_connection_reserve, 64);
|
||||
usize_default!(default_web_max_carrier_learning_entries, 4096);
|
||||
usize_default!(default_web_max_body_readers, 32);
|
||||
usize_default!(default_web_max_body_bytes_global, 64 * 1024 * 1024);
|
||||
usize_default!(default_web_max_sessions_global, 128);
|
||||
usize_default!(default_web_max_sessions_per_ip, 16);
|
||||
usize_default!(default_web_max_streams_per_session, 128);
|
||||
usize_default!(default_web_max_streams_global, 4096);
|
||||
usize_default!(default_web_max_stream_handshakes, 256);
|
||||
usize_default!(default_web_max_tombstones, 4096);
|
||||
usize_default!(default_web_pending_bytes_per_session, 32 * 1024 * 1024);
|
||||
usize_default!(default_web_pending_bytes_global, 512 * 1024 * 1024);
|
||||
usize_default!(default_web_pending_items_per_session, 16 * 1024);
|
||||
usize_default!(default_web_pending_items_global, 256 * 1024);
|
||||
usize_default!(default_web_control_bytes_per_session, 256 * 1024);
|
||||
usize_default!(default_web_control_bytes_global, 16 * 1024 * 1024);
|
||||
usize_default!(default_web_max_bootstraps_global, 512);
|
||||
usize_default!(default_web_max_bootstraps_per_ip, 64);
|
||||
usize_default!(default_web_max_vhosts, 8);
|
||||
usize_default!(default_web_max_profiles, 32);
|
||||
usize_default!(default_web_max_static_files, 4096);
|
||||
usize_default!(default_web_max_static_file_bytes, 8 * 1024 * 1024);
|
||||
usize_default!(default_web_max_static_bytes, 64 * 1024 * 1024);
|
||||
usize_default!(default_web_debug_records_capacity, 65_536);
|
||||
usize_default!(default_web_debug_bytes_global, 64 * 1024 * 1024);
|
||||
usize_default!(default_web_memory_envelope_bytes, 768 * 1024 * 1024);
|
||||
u32_default!(default_web_new_bootstraps_per_minute, 1200);
|
||||
u32_default!(default_web_new_bootstraps_burst, 256);
|
||||
u32_default!(default_web_new_sessions_per_minute, 600);
|
||||
u32_default!(default_web_new_sessions_burst, 128);
|
||||
u32_default!(default_web_new_streams_per_minute, 6000);
|
||||
u32_default!(default_web_new_streams_burst, 512);
|
||||
u64_default!(default_web_header_timeout_secs, 10);
|
||||
u64_default!(default_web_body_timeout_secs, 30);
|
||||
u64_default!(default_web_stream_handshake_timeout_secs, 10);
|
||||
u64_default!(default_web_long_poll_timeout_secs, 25);
|
||||
u64_default!(default_web_websocket_write_secs, 30);
|
||||
u64_default!(default_web_websocket_backpressure_secs, 30);
|
||||
u64_default!(default_web_websocket_eviction_secs, 1);
|
||||
pub(super) fn default_web_carrier_negotiation_deadlines_secs() -> [u64; 4] {
|
||||
[3, 5, 8, 12]
|
||||
}
|
||||
u64_default!(default_web_carrier_learning_secs, 600);
|
||||
pub(super) fn default_web_carrier_learning() -> bool {
|
||||
true
|
||||
}
|
||||
u64_default!(default_web_bootstrap_lifetime_secs, 120);
|
||||
u64_default!(default_web_reconnect_grace_secs, 120);
|
||||
u64_default!(default_web_http_idle_secs, 75);
|
||||
u64_default!(default_web_shutdown_secs, 15);
|
||||
u64_default!(default_web_decoy_header_timeout_secs, 30);
|
||||
@@ -0,0 +1,115 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Carrier selected for one newly issued WEB relay session.
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum WebCarrier {
|
||||
/// Serialize all logical streams through one uplink and one downlink sequence.
|
||||
#[default]
|
||||
Https,
|
||||
/// Give every logical stream independent HTTPS sequencing and polling state.
|
||||
HttpsLanes,
|
||||
/// Multiplex all logical streams over one ordered WebSocket.
|
||||
Websocket,
|
||||
/// Give every logical stream an independently owned WebSocket lane.
|
||||
WebsocketLanes,
|
||||
}
|
||||
|
||||
impl WebCarrier {
|
||||
/// Every carrier supported by the WEB v1 bridge.
|
||||
pub(crate) const ALL: [Self; 4] = [
|
||||
Self::Https,
|
||||
Self::HttpsLanes,
|
||||
Self::Websocket,
|
||||
Self::WebsocketLanes,
|
||||
];
|
||||
|
||||
/// Returns the exact carrier token advertised to the browser bridge.
|
||||
pub(crate) const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Https => "https",
|
||||
Self::HttpsLanes => "https-lanes",
|
||||
Self::Websocket => "websocket",
|
||||
Self::WebsocketLanes => "websocket-lanes",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the stable fixed-slot index used by bounded learning state.
|
||||
pub(crate) const fn index(self) -> usize {
|
||||
match self {
|
||||
Self::Https => 0,
|
||||
Self::HttpsLanes => 1,
|
||||
Self::Websocket => 2,
|
||||
Self::WebsocketLanes => 3,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether one carrier owns independent state per logical stream.
|
||||
pub(crate) const fn uses_lanes(self) -> bool {
|
||||
matches!(self, Self::HttpsLanes | Self::WebsocketLanes)
|
||||
}
|
||||
|
||||
/// Returns whether carrier messages use RFC 6455 instead of HTTP bodies.
|
||||
pub(crate) const fn uses_websocket(self) -> bool {
|
||||
matches!(self, Self::Websocket | Self::WebsocketLanes)
|
||||
}
|
||||
|
||||
/// Returns whether all logical streams share one carrier state machine.
|
||||
pub(crate) const fn is_multiplexed(self) -> bool {
|
||||
matches!(self, Self::Https | Self::Websocket)
|
||||
}
|
||||
}
|
||||
|
||||
/// Optional ordered carrier list that enables server-side auto-negotiation.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub enum WebCarriers {
|
||||
/// Auto-negotiation is disabled and only `web.carrier` is used.
|
||||
#[default]
|
||||
Disabled,
|
||||
/// Auto-negotiation uses this ordered candidate list before the fallback.
|
||||
Enabled(Vec<WebCarrier>),
|
||||
}
|
||||
|
||||
impl WebCarriers {
|
||||
/// Returns the explicit candidate list when negotiation is enabled.
|
||||
pub fn enabled(&self) -> Option<&[WebCarrier]> {
|
||||
match self {
|
||||
Self::Disabled => None,
|
||||
Self::Enabled(carriers) => Some(carriers),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for WebCarriers {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
Self::Disabled => false.serialize(serializer),
|
||||
Self::Enabled(carriers) => carriers.serialize(serializer),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for WebCarriers {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum Repr {
|
||||
Flag(bool),
|
||||
List(Vec<WebCarrier>),
|
||||
}
|
||||
|
||||
match Repr::deserialize(deserializer)? {
|
||||
Repr::Flag(false) => Ok(Self::Disabled),
|
||||
Repr::Flag(true) => Err(serde::de::Error::custom(
|
||||
"web.carriers accepts false or a non-empty carrier array",
|
||||
)),
|
||||
Repr::List(carriers) => Ok(Self::Enabled(carriers)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user