mirror of
https://github.com/telemt/telemt.git
synced 2026-11-01 05:12:02 +03:00
WEB: Base Path: security + reload coverage fixes&tests
This commit is contained in:
@@ -62,5 +62,8 @@ use reporting::log_changes;
|
||||
#[cfg(test)]
|
||||
use watcher::{ReloadState, reload_config};
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "hot_reload/base_path_tests.rs"]
|
||||
mod base_path_tests;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
use base64::Engine as _;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn write_base_path_config(path: &Path, base_path: &str) {
|
||||
let base_path = if base_path.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!("base_path = \"{base_path}\"\n")
|
||||
};
|
||||
let config = format!(
|
||||
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
|
||||
|
||||
[[web.vhosts]]
|
||||
host = "proxy.example.com"
|
||||
{base_path}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 = "plain"
|
||||
"#,
|
||||
);
|
||||
std::fs::write(path, config).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reload_rejects_invalid_base_then_publishes_route_identity_together() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let path = directory.path().join("config.toml");
|
||||
write_base_path_config(&path, "");
|
||||
let initial = Arc::new(ProxyConfig::load(&path).unwrap());
|
||||
let initial_hash = ProxyConfig::load_with_metadata(&path)
|
||||
.unwrap()
|
||||
.rendered_hash;
|
||||
let initial_capability = initial.web.runtime.as_ref().unwrap().capabilities[0];
|
||||
let (config_tx, _config_rx) = watch::channel(Arc::clone(&initial));
|
||||
let (log_tx, _log_rx) = watch::channel(initial.general.log_level.clone());
|
||||
let mut reload_state = ReloadState::new(Some(initial_hash));
|
||||
|
||||
write_base_path_config(&path, "/invalid");
|
||||
reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state);
|
||||
let unchanged = config_tx.borrow().clone();
|
||||
assert!(Arc::ptr_eq(&unchanged, &initial));
|
||||
assert_eq!(unchanged.web.vhosts[0].base_path, "");
|
||||
assert_eq!(
|
||||
unchanged.web.runtime.as_ref().unwrap().capabilities[0],
|
||||
initial_capability
|
||||
);
|
||||
|
||||
write_base_path_config(&path, "dobry-cola-super-app");
|
||||
reload_config(&path, &config_tx, &log_tx, None, None, &mut reload_state);
|
||||
let applied = config_tx.borrow().clone();
|
||||
let runtime = applied.web.runtime.as_ref().unwrap();
|
||||
let vhost = &runtime.vhosts["proxy.example.com"];
|
||||
assert_eq!(applied.web.vhosts[0].base_path, "dobry-cola-super-app");
|
||||
assert_eq!(vhost.base, "/dobry-cola-super-app/");
|
||||
assert_eq!(vhost.capabilities[0], vhost.profiles[0].capability);
|
||||
assert_eq!(runtime.capabilities.as_ref(), vhost.capabilities.as_ref());
|
||||
assert!(!runtime.capabilities.contains(&initial_capability));
|
||||
assert_eq!(
|
||||
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(vhost.capabilities[0]),
|
||||
"hHz99Xs93EN1j91G9gpNepXwGNNt5YdAFkEVk_LlqdQ"
|
||||
);
|
||||
}
|
||||
@@ -38,6 +38,23 @@ fn capability_matches_reference_vectors() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capability_binds_the_exact_host_and_base_path_identity() {
|
||||
let secret = hex::decode("000102030405060708090a0b0c0d0e0f").unwrap();
|
||||
let root = derive_web_capability(&secret, b"proxy.example.com", b"").unwrap();
|
||||
let mixed = derive_web_capability(&secret, b"proxy.example.com", b"MixedCase/path").unwrap();
|
||||
let lower = derive_web_capability(&secret, b"proxy.example.com", b"mixedcase/path").unwrap();
|
||||
let other_path =
|
||||
derive_web_capability(&secret, b"proxy.example.com", b"MixedCase/other").unwrap();
|
||||
let other_host =
|
||||
derive_web_capability(&secret, b"other.example.com", b"MixedCase/path").unwrap();
|
||||
|
||||
let identities = [root, mixed, lower, other_path, other_host]
|
||||
.into_iter()
|
||||
.collect::<std::collections::HashSet<_>>();
|
||||
assert_eq!(identities.len(), 5);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn static_snapshot_remains_anchored_after_root_path_replacement() {
|
||||
|
||||
@@ -365,13 +365,7 @@ const WEB_TIMEOUTS_CONFIG_KEYS: &[&str] = &[
|
||||
"decoy_header_secs",
|
||||
];
|
||||
|
||||
const WEB_VHOST_CONFIG_KEYS: &[&str] = &[
|
||||
"host",
|
||||
"base_path",
|
||||
"public_addr",
|
||||
"decoy",
|
||||
"profiles",
|
||||
];
|
||||
const WEB_VHOST_CONFIG_KEYS: &[&str] = &["host", "base_path", "public_addr", "decoy", "profiles"];
|
||||
const WEB_DECOY_CONFIG_KEYS: &[&str] = &["mode", "upstream", "directory", "index"];
|
||||
const WEB_PROFILE_CONFIG_KEYS: &[&str] = &[
|
||||
"user",
|
||||
|
||||
@@ -85,7 +85,9 @@ fn validate_web_base_path(value: &str, field: &str) -> Result<()> {
|
||||
&& !value.ends_with('/')
|
||||
&& value.split('/').all(|segment| {
|
||||
let mut bytes = segment.bytes();
|
||||
bytes.next().is_some_and(|byte| byte.is_ascii_alphanumeric())
|
||||
bytes
|
||||
.next()
|
||||
.is_some_and(|byte| byte.is_ascii_alphanumeric())
|
||||
&& bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
|
||||
});
|
||||
if value.is_empty() || valid {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use super::*;
|
||||
|
||||
#[path = "web_tests/base_path_tests.rs"]
|
||||
mod base_path_tests;
|
||||
|
||||
const WEB_CONFIG: &str = r#"
|
||||
[access.users]
|
||||
alice = "000102030405060708090a0b0c0d0e0f"
|
||||
@@ -64,7 +67,7 @@ fn web_config_builds_canonical_runtime_snapshot() {
|
||||
#[test]
|
||||
fn web_base_path_is_canonical_and_precomputed() {
|
||||
let maximum = "a".repeat(128);
|
||||
for base_path in ["Dobry-Cola/super_app", maximum.as_str()] {
|
||||
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}\""),
|
||||
@@ -99,6 +102,11 @@ fn web_base_path_rejects_noncanonical_forms() {
|
||||
"relay//nested",
|
||||
"-relay",
|
||||
"_relay",
|
||||
"a/-lead",
|
||||
"a/_lead",
|
||||
"dot.ted",
|
||||
"..",
|
||||
"/",
|
||||
"relay/.hidden",
|
||||
"relay/%2fhidden",
|
||||
"relay path",
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn web_runtime_collects_every_vhost_capability() {
|
||||
let configured = format!(
|
||||
"{WEB_CONFIG}\n{}",
|
||||
r#"
|
||||
[[web.vhosts]]
|
||||
host = "Other.Example.COM"
|
||||
base_path = "other/path"
|
||||
public_addr = "203.0.113.11:443"
|
||||
|
||||
[web.vhosts.decoy]
|
||||
mode = "http_upstream"
|
||||
upstream = "http://127.0.0.1:18082"
|
||||
|
||||
[[web.vhosts.profiles]]
|
||||
user = "alice"
|
||||
secret_mode = "dd"
|
||||
"#
|
||||
);
|
||||
let config = load_config_from_temp_toml(&configured);
|
||||
let runtime = config.web.runtime.as_ref().unwrap();
|
||||
let first = &runtime.vhosts["proxy.example.com"];
|
||||
let second = &runtime.vhosts["other.example.com"];
|
||||
|
||||
assert_eq!(runtime.capabilities.len(), 2);
|
||||
assert_eq!(first.capabilities[0], first.profiles[0].capability);
|
||||
assert_eq!(second.capabilities[0], second.profiles[0].capability);
|
||||
assert_ne!(first.capabilities[0], second.capabilities[0]);
|
||||
assert!(runtime.capabilities.contains(&first.capabilities[0]));
|
||||
assert!(runtime.capabilities.contains(&second.capabilities[0]));
|
||||
}
|
||||
Reference in New Issue
Block a user