use super::*; #[path = "web_tests/base_path_tests.rs"] mod base_path_tests; // Method selection must not alter capability identity or accepted config literals. #[path = "web_tests/carrier_method_tests.rs"] mod carrier_method_tests; const WEB_CONFIG: &str = r#" [access.users] alice = "000102030405060708090a0b0c0d0e0f" [[server.listeners]] ip = "127.0.0.1" port = 18080 transport = "web" proxy_protocol = false web_client_ip_source = "x_forwarded_for" web_trusted_proxy_cidrs = ["127.0.0.1/32"] [web] enabled = true carrier = "https-lanes" [[web.vhosts]] host = "Proxy.Example.COM" public_addr = "203.0.113.10:443" [web.vhosts.decoy] mode = "http_upstream" upstream = "http://127.0.0.1:18081" [[web.vhosts.profiles]] user = "alice" secret_mode = "dd" max_sessions = 4 max_streams = 64 max_streams_per_session = 16 "#; #[test] fn web_config_builds_canonical_runtime_snapshot() { let config = load_config_from_temp_toml(WEB_CONFIG); let runtime = config.web.runtime.expect("WEB runtime snapshot"); let vhost = runtime .vhosts .get("proxy.example.com") .expect("canonical WEB vhost"); assert_eq!(vhost.base, "/"); assert_eq!(vhost.profiles.len(), 1); assert_eq!(vhost.capabilities.len(), vhost.profiles.len()); assert_eq!(vhost.capabilities[0], vhost.profiles[0].capability); assert_eq!(runtime.capabilities.as_ref(), vhost.capabilities.as_ref()); assert_eq!(vhost.decoy_fasttrack_mode, WebDecoyFastTrackMode::Off); assert_eq!(vhost.profiles[0].user, "alice"); assert_eq!(vhost.profiles[0].secret_mode, WebSecretMode::Dd); assert_eq!(vhost.profiles[0].carrier, WebCarrier::HttpsLanes); assert_eq!(vhost.profiles[0].max_sessions, 4); assert_eq!(vhost.profiles[0].max_streams, 64); 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_base_path_is_canonical_and_precomputed() { let maximum = "a".repeat(128); for base_path in ["a", "a/b/c9_x-y", "Dobry-Cola/super_app", maximum.as_str()] { let configured = WEB_CONFIG.replace( "host = \"Proxy.Example.COM\"", &format!("host = \"Proxy.Example.COM\"\nbase_path = \"{base_path}\""), ); let config = load_config_from_temp_toml(&configured); assert_eq!(config.web.vhosts[0].base_path, base_path); assert_eq!( config.web.runtime.as_ref().unwrap().vhosts["proxy.example.com"].base, format!("/{base_path}/") ); } let strict = format!( "[general]\nconfig_strict = true\n{}", WEB_CONFIG.replace( "host = \"Proxy.Example.COM\"", "host = \"Proxy.Example.COM\"\nbase_path = \"relay\"", ) ); assert_eq!( load_config_from_temp_toml(&strict).web.vhosts[0].base_path, "relay" ); } #[test] fn web_base_path_rejects_noncanonical_forms() { let oversized = "a".repeat(129); for base_path in [ "/relay", "relay/", "relay//nested", "-relay", "_relay", "a/-lead", "a/_lead", "dot.ted", "..", "/", "relay/.hidden", "relay/%2fhidden", "relay path", "relay/ั‚ะตัั‚", oversized.as_str(), ] { let invalid = WEB_CONFIG.replace( "host = \"Proxy.Example.COM\"", &format!("host = \"Proxy.Example.COM\"\nbase_path = \"{base_path}\""), ); assert!( load_config_error_from_temp_toml(&invalid).contains("web.vhosts[0].base_path"), "base path {base_path:?} was accepted" ); } } #[test] fn web_decoy_fasttrack_mode_is_typed_and_defaults_off() { let defaults = ProxyConfig::default(); assert_eq!( defaults.web.decoy_fasttrack_mode, WebDecoyFastTrackMode::Off ); for (token, expected) in [ ("shadow", WebDecoyFastTrackMode::Shadow), ("enforce", WebDecoyFastTrackMode::Enforce), ] { let configured = WEB_CONFIG.replace( "carrier = \"https-lanes\"", &format!("carrier = \"https-lanes\"\ndecoy_fasttrack_mode = \"{token}\""), ); let config = load_config_from_temp_toml(&configured); assert_eq!(config.web.decoy_fasttrack_mode, expected); assert_eq!( config.web.runtime.as_ref().unwrap().vhosts["proxy.example.com"].decoy_fasttrack_mode, expected ); } let invalid = WEB_CONFIG.replace( "carrier = \"https-lanes\"", "carrier = \"https-lanes\"\ndecoy_fasttrack_mode = \"automatic\"", ); assert!(load_config_error_from_temp_toml(&invalid).contains("decoy_fasttrack_mode")); } #[test] fn web_http_connection_capacity_policy_is_bounded_and_configurable() { let configured = WEB_CONFIG .replace( "carrier = \"https-lanes\"", "carrier = \"https-lanes\"\nhttp_connection_capacity_action = \"wait\"", ) .replace( "[[web.vhosts]]", "[web.limits]\nmax_http_overload_connections = 23\n\n[web.timeouts]\nhttp_overload_timeout_ms = 731\n\n[[web.vhosts]]", ); let config = load_config_from_temp_toml(&configured); assert_eq!( config.web.http_connection_capacity_action, WebHttpConnectionCapacityAction::Wait ); assert_eq!(config.web.limits.max_http_overload_connections, 23); assert_eq!(config.web.timeouts.http_overload_timeout_ms, 731); let defaults = ProxyConfig::default(); assert_eq!( defaults.web.http_connection_capacity_action, WebHttpConnectionCapacityAction::Drop ); assert_eq!(defaults.web.limits.max_http_overload_connections, 64); assert_eq!(defaults.web.timeouts.http_overload_timeout_ms, 250); } #[test] fn web_http_connection_capacity_policy_rejects_unknown_or_unbounded_values() { let unknown = WEB_CONFIG.replace( "carrier = \"https-lanes\"", "carrier = \"https-lanes\"\nhttp_connection_capacity_action = \"queue\"", ); assert!(load_config_error_from_temp_toml(&unknown).contains("http_connection_capacity_action")); for timeout in [0, 60_001] { let invalid = WEB_CONFIG.replace( "[[web.vhosts]]", &format!("[web.timeouts]\nhttp_overload_timeout_ms = {timeout}\n\n[[web.vhosts]]"), ); assert!( load_config_error_from_temp_toml(&invalid) .contains("web.timeouts.http_overload_timeout_ms") ); } let no_overload_slots = WEB_CONFIG.replace( "[[web.vhosts]]", "[web.limits]\nmax_http_overload_connections = 0\n\n[[web.vhosts]]", ); assert!( load_config_error_from_temp_toml(&no_overload_slots) .contains("web.limits.max_http_overload_connections") ); } #[test] fn web_decoy_rejects_direct_and_wildcard_listener_loops() { let direct = WEB_CONFIG.replace("http://127.0.0.1:18081", "http://127.0.0.1:18080"); assert!( load_config_error_from_temp_toml(&direct).contains("decoy upstream overlaps WEB listener") ); let wildcard = direct.replace("ip = \"127.0.0.1\"", "ip = \"0.0.0.0\""); assert!( load_config_error_from_temp_toml(&wildcard) .contains("decoy upstream overlaps WEB listener") ); } #[test] fn web_profile_user_labels_are_bounded_for_runtime_status() { let user = "a".repeat(65); let invalid = WEB_CONFIG.replace("alice", &user); assert!( load_config_error_from_temp_toml(&invalid) .contains("web.vhosts[0].profiles[0].user must contain 1..64 bytes") ); } #[test] fn web_carriers_missing_or_false_disable_negotiation() { let missing = load_config_from_temp_toml(WEB_CONFIG); assert!(!missing.web.carrier_negotiation_enabled()); assert!(!missing.web.runtime.unwrap().profiles[0].carrier_learning); 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!(!disabled.web.runtime.as_ref().unwrap().profiles[0].carrier_learning); 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_and_bridge_deadlines_are_configurable() { let configured = WEB_CONFIG.replace( "[[web.vhosts]]", "[web.timeouts]\ncarrier_negotiation_deadlines_secs = [1, 2, 4, 9]\ncarrier_learning_secs = 30\nbridge_request_secs = 7\nbridge_retry_secs = 41\nbridge_recovery_secs = 13\ncarrier_probe_coalesce_ms = 4\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); assert_eq!(config.web.timeouts.bridge_request_secs, 7); assert_eq!(config.web.timeouts.bridge_retry_secs, 41); assert_eq!(config.web.timeouts.bridge_recovery_secs, 13); assert_eq!(config.web.timeouts.carrier_probe_coalesce_ms, 4); } #[test] fn web_bridge_deadlines_are_known_in_strict_mode() { let configured = WEB_CONFIG.replace( "[[web.vhosts]]", "[web.timeouts]\nbridge_request_secs = 7\nbridge_retry_secs = 41\nbridge_recovery_secs = 13\ncarrier_probe_coalesce_ms = 4\n\n[[web.vhosts]]", ); let configured = format!("[general]\nconfig_strict = true\n{configured}"); let config = load_config_from_temp_toml(&configured); assert_eq!(config.web.timeouts.bridge_request_secs, 7); assert_eq!(config.web.timeouts.bridge_retry_secs, 41); assert_eq!(config.web.timeouts.bridge_recovery_secs, 13); assert_eq!(config.web.timeouts.carrier_probe_coalesce_ms, 4); } #[test] fn web_bridge_deadlines_are_bounded_and_ordered() { for (field, value) in [ ("bridge_request_secs", "0"), ("bridge_request_secs", "61"), ("bridge_retry_secs", "0"), ("bridge_retry_secs", "301"), ("bridge_recovery_secs", "0"), ("bridge_recovery_secs", "61"), ("carrier_probe_coalesce_ms", "11"), ] { let invalid = WEB_CONFIG.replace( "[[web.vhosts]]", &format!("[web.timeouts]\n{field} = {value}\n\n[[web.vhosts]]"), ); assert!( load_config_error_from_temp_toml(&invalid).contains(&format!("web.timeouts.{field}")) ); } let reversed = WEB_CONFIG.replace( "[[web.vhosts]]", "[web.timeouts]\nbridge_request_secs = 20\nbridge_retry_secs = 10\n\n[[web.vhosts]]", ); assert!( load_config_error_from_temp_toml(&reversed) .contains("bridge_request_secs must not exceed bridge_retry_secs") ); } #[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] fn web_debug_table_uses_debug_name_and_bounded_defaults() { assert!(!crate::config::WebDebugConfig::default().sideband); let mut ineffective = crate::config::WebDebugConfig { enabled: true, sideband: true, ..Default::default() }; ineffective.capture_lifecycle = false; assert!(!ineffective.bridge_diagnostics_enabled()); let configured = WEB_CONFIG.replace( "[[web.vhosts]]", "[web.debug]\nenabled = true\nsideband = true\nbody_capture = \"prefix\"\nbody_prefix_bytes = 2048\ndefault_window_secs = 180\nmax_window_secs = 900\n\n[[web.vhosts]]", ); let config = load_config_from_temp_toml(&configured); assert!(config.web.debug.enabled); assert!(config.web.debug.sideband); assert!(config.web.debug.bridge_diagnostics_enabled()); assert_eq!(config.web.debug.body_capture, WebDebugBodyCapture::Prefix); assert_eq!(config.web.debug.body_prefix_bytes, 2048); assert_eq!(config.web.debug.default_window_secs, 180); assert_eq!(config.web.debug.max_window_secs, 900); let strict = format!("[general]\nconfig_strict = true\n{configured}"); assert!(load_config_from_temp_toml(&strict).web.debug.sideband); let old_name = format!( "[general]\nconfig_strict = true\n{}", WEB_CONFIG.replace( "[[web.vhosts]]", "[web.trace]\nenabled = true\n\n[[web.vhosts]]", ) ); let error = load_config_error_from_temp_toml(&old_name); assert!(error.contains("web.trace")); let old_parameter = format!( "[general]\nconfig_strict = true\n{}", WEB_CONFIG.replace( "[[web.vhosts]]", "[web.debug]\nbridge_diagnostics = true\n\n[[web.vhosts]]", ) ); let error = load_config_error_from_temp_toml(&old_parameter); assert!(error.contains("web.debug.bridge_diagnostics")); } #[test] fn web_debug_prefix_and_window_validation_fail_closed() { let oversized_prefix = WEB_CONFIG.replace( "[[web.vhosts]]", "[web.debug]\nenabled = true\nbody_prefix_bytes = 2097153\n\n[[web.vhosts]]", ); let error = load_config_error_from_temp_toml(&oversized_prefix); assert!(error.contains("web.debug.body_prefix_bytes")); let reversed_window = WEB_CONFIG.replace( "[[web.vhosts]]", "[web.debug]\nenabled = true\ndefault_window_secs = 181\nmax_window_secs = 180\n\n[[web.vhosts]]", ); let error = load_config_error_from_temp_toml(&reversed_window); assert!(error.contains("web.debug windows")); let undersized_store = WEB_CONFIG.replace( "carrier = \"https-lanes\"", "carrier = \"https-lanes\"\n\n[web.limits]\ndebug_bytes_global = 4095", ); let error = load_config_error_from_temp_toml(&undersized_store); assert!(error.contains("debug_bytes_global must be at least 4096")); } #[test] fn https_lanes_requires_separate_poll_and_control_handler_capacity() { let invalid = WEB_CONFIG.replace( "carrier = \"https-lanes\"", "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 https-lanes candidates require")); } #[test] fn web_listener_requires_an_explicit_trusted_proxy() { let invalid = WEB_CONFIG.replace( "web_trusted_proxy_cidrs = [\"127.0.0.1/32\"]", "web_trusted_proxy_cidrs = []", ); let error = load_config_error_from_temp_toml(&invalid); assert!(error.contains("web_trusted_proxy_cidrs must be non-empty")); } #[test] fn web_queue_limits_preserve_control_and_uplink_progress() { let invalid = WEB_CONFIG.replace( "carrier = \"https-lanes\"", "carrier = \"https-lanes\"\n\n[web.limits]\ncontrol_bytes_per_session = 1", ); let error = load_config_error_from_temp_toml(&invalid); assert!(error.contains("control reserves must cover bounded control frames")); } #[test] fn web_semaphore_limits_are_rejected_before_runtime_construction() { let invalid = WEB_CONFIG.replace( "carrier = \"https-lanes\"", &format!( "carrier = \"https-lanes\"\n\n[web.limits]\nmax_http_connections = {}", tokio::sync::Semaphore::MAX_PERMITS + 1, ), ); let error = load_config_error_from_temp_toml(&invalid); assert!(error.contains("exceeds Tokio semaphore capacity")); } #[test] fn web_ipv6_decoy_uses_a_valid_http_authority() { let ipv6 = WEB_CONFIG.replace("http://127.0.0.1:18081", "http://[::1]:18081"); let config = load_config_from_temp_toml(&ipv6); let runtime = config.web.runtime.expect("WEB runtime snapshot"); let vhost = runtime.vhosts.get("proxy.example.com").unwrap(); let WebRuntimeDecoy::HttpUpstream { authority, .. } = &vhost.decoy else { panic!("expected HTTP decoy"); }; assert_eq!(authority, "[::1]:18081"); } #[test] fn websocket_carriers_build_runtime_profiles_with_bounded_defaults() { for (name, carrier) in [ ("websocket", WebCarrier::Websocket), ("websocket-lanes", WebCarrier::WebsocketLanes), ] { let configured = WEB_CONFIG.replace("https-lanes", name); let config = load_config_from_temp_toml(&configured); let profile = &config.web.runtime.unwrap().profiles[0]; assert_eq!(profile.carrier, carrier); assert_eq!(config.web.limits.websocket_bytes_global, 256 * 1024 * 1024); assert_eq!(config.web.limits.websocket_admission_watermark_pct, 75); assert_eq!(config.web.limits.websocket_eviction_watermark_pct, 90); assert_eq!(config.web.limits.websocket_http_connection_reserve, 64); assert_eq!(config.web.timeouts.websocket_write_secs, 30); assert_eq!(config.web.timeouts.websocket_backpressure_secs, 30); assert_eq!(config.web.timeouts.websocket_eviction_secs, 1); } } #[test] fn websocket_limits_reject_ambiguous_or_nonprogressing_policy() { let reversed_watermarks = WEB_CONFIG.replace( "carrier = \"https-lanes\"", "carrier = \"websocket\"\n\n[web.limits]\nwebsocket_admission_watermark_pct = 90\nwebsocket_eviction_watermark_pct = 75", ); assert!( load_config_error_from_temp_toml(&reversed_watermarks).contains("WebSocket watermarks") ); let no_http_reserve = WEB_CONFIG.replace( "carrier = \"https-lanes\"", "carrier = \"websocket\"\n\n[web.limits]\nwebsocket_http_connection_reserve = 0", ); assert!( load_config_error_from_temp_toml(&no_http_reserve) .contains("websocket_http_connection_reserve") ); let oversized_batch = WEB_CONFIG.replace( "carrier = \"https-lanes\"", "carrier = \"websocket\"\n\n[web.limits]\nmax_body_bytes = 4194304\ncarrier_batch_bytes = 4194304\nmax_body_readers = 16", ); assert!( load_config_error_from_temp_toml(&oversized_batch) .contains("carrier_batch_bytes <= 2097152") ); }