215 lines
8.4 KiB
Rust
215 lines
8.4 KiB
Rust
mod default_listener_tests {
|
|
use super::*;
|
|
use serde_json::json;
|
|
#[test]
|
|
fn tls_defaults_to_h3_and_explicit_null_survives_a_roundtrip() {
|
|
let auto: Listener =
|
|
serde_json::from_value(json!({"address":"127.0.0.1:8443","tls":true})).unwrap();
|
|
assert_eq!(auto.http3, Some(crate::http3::Settings::default()));
|
|
assert!(auto.http2);
|
|
let disabled: Listener =
|
|
serde_json::from_value(json!({"address":"127.0.0.1:8443","tls":true,"http3":null}))
|
|
.unwrap();
|
|
let wire = serde_json::to_value(disabled).unwrap();
|
|
assert!(wire["http3"].is_null());
|
|
assert!(serde_json::from_value::<Listener>(wire)
|
|
.unwrap()
|
|
.http3
|
|
.is_none());
|
|
let plain: Listener = serde_json::from_value(json!({"address":"127.0.0.1:8080"})).unwrap();
|
|
assert!(plain.http3.is_none());
|
|
}
|
|
#[test]
|
|
fn listener_plan_inherits_legacy_h2_limits_and_prefers_listener_override() {
|
|
let mut config = Config::default();
|
|
config.http2 = crate::settings::Http2Connections {
|
|
idle_timeout_seconds: 111,
|
|
max_connection_lifetime_seconds: 222,
|
|
drain_timeout_seconds: 33,
|
|
};
|
|
config.listeners.endpoints = serde_json::from_value(json!([
|
|
{
|
|
"id": "tls",
|
|
"name": "TLS",
|
|
"scope": "public",
|
|
"enabled": true,
|
|
"binding": {
|
|
"address": "127.0.0.1:8443",
|
|
"tls": true,
|
|
"http2": true,
|
|
"http3": null
|
|
}
|
|
},
|
|
{
|
|
"id": "h2c",
|
|
"name": "H2C",
|
|
"scope": "internal",
|
|
"enabled": true,
|
|
"binding": {
|
|
"address": "127.0.0.1:8081",
|
|
"tls": false,
|
|
"http2": false,
|
|
"h2c": true,
|
|
"http2_settings": {
|
|
"idle_timeout_seconds": 444,
|
|
"max_connection_lifetime_seconds": 555,
|
|
"drain_timeout_seconds": 66
|
|
},
|
|
"http3": null
|
|
}
|
|
}
|
|
]))
|
|
.unwrap();
|
|
assert!(config.listeners.endpoints[0].binding.http2_settings.is_none());
|
|
let policy = ValidationPolicy {
|
|
proxy_addr: "127.0.0.1:18080".parse().unwrap(),
|
|
admin_addr: "127.0.0.1:19090".parse().unwrap(),
|
|
tls_addr: None,
|
|
cert_dir: None,
|
|
data_dir: None,
|
|
};
|
|
let plan = listener_plan(&config, &policy).unwrap();
|
|
let tls = plan.iter().find(|item| item.address.port() == 8443).unwrap();
|
|
assert_eq!(tls.http2_settings, Some(config.http2));
|
|
let h2c = plan.iter().find(|item| item.address.port() == 8081).unwrap();
|
|
assert_eq!(
|
|
h2c.http2_settings,
|
|
Some(crate::settings::Http2Connections {
|
|
idle_timeout_seconds: 444,
|
|
max_connection_lifetime_seconds: 555,
|
|
drain_timeout_seconds: 66,
|
|
})
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn method_policy_normalizes_and_enforces_allow_and_block_modes() {
|
|
let mut allow: MethodPolicy = serde_json::from_value(json!({
|
|
"mode": "allow",
|
|
"methods": ["get", "HEAD", "post"]
|
|
}))
|
|
.unwrap();
|
|
allow.validate().unwrap();
|
|
assert_eq!(allow.methods, vec!["GET", "HEAD", "POST"]);
|
|
assert!(allow.allows(&http::Method::GET));
|
|
assert!(!allow.allows(&http::Method::DELETE));
|
|
assert_eq!(allow.allow_header().unwrap().to_str().unwrap(), "GET, HEAD, POST");
|
|
|
|
let mut block: MethodPolicy = serde_json::from_value(json!({
|
|
"mode": "block",
|
|
"methods": ["delete"]
|
|
}))
|
|
.unwrap();
|
|
block.validate().unwrap();
|
|
assert!(!block.allows(&http::Method::DELETE));
|
|
assert!(block.allows(&http::Method::GET));
|
|
assert!(block.allow_header().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn h2c_is_cleartext_only_and_defaults_off() {
|
|
let plain: Listener =
|
|
serde_json::from_value(json!({"address":"127.0.0.1:8080","tls":false})).unwrap();
|
|
assert!(!plain.h2c);
|
|
let h2c: Listener = serde_json::from_value(
|
|
json!({"address":"127.0.0.1:8080","tls":false,"h2c":true}),
|
|
)
|
|
.unwrap();
|
|
assert!(validate_listener(h2c).is_ok());
|
|
let invalid: Listener = serde_json::from_value(
|
|
json!({"address":"127.0.0.1:8443","tls":true,"h2c":true}),
|
|
)
|
|
.unwrap();
|
|
assert!(validate_listener(invalid).is_err());
|
|
}
|
|
#[test]
|
|
fn proxy_timeouts_have_explicit_safe_defaults_but_keep_null_compatibility() {
|
|
let defaults: ProxyTimeouts = serde_json::from_value(json!({})).unwrap();
|
|
assert_eq!(defaults.connect_timeout_seconds, Some(10));
|
|
assert_eq!(defaults.response_header_timeout_seconds, Some(120));
|
|
assert_eq!(defaults.read_timeout_seconds, Some(60));
|
|
assert_eq!(defaults.send_timeout_seconds, Some(60));
|
|
let inherited: ProxyTimeouts =
|
|
serde_json::from_value(json!({"read_timeout_seconds":null})).unwrap();
|
|
assert_eq!(inherited.read_timeout_seconds, None);
|
|
assert_eq!(inherited.connect_timeout_seconds, Some(10));
|
|
}
|
|
#[test]
|
|
fn vhost_hostname_count_is_not_artificially_capped() {
|
|
let hosts = (0..40)
|
|
.map(|index| format!("host-{index}.example.test"))
|
|
.collect::<Vec<_>>();
|
|
let mut vhost: Vhost = serde_json::from_value(json!({
|
|
"id": "many-hosts",
|
|
"frontend": {"hosts": hosts, "listener_ids": ["https"]},
|
|
"backends": [{"id": "origin", "url": "http://127.0.0.1:9001", "allow_private": true}]
|
|
}))
|
|
.unwrap();
|
|
assert!(vhost.validate().is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn error_fallback_requires_internal_target_and_rejects_cycles() {
|
|
let base = json!({
|
|
"id": "fallback-test",
|
|
"frontend": {"hosts": ["example.test"], "listener_ids": ["http"]},
|
|
"backends": [{"id": "origin", "url": "http://127.0.0.1:9001", "allow_private": true}],
|
|
"error_fallback": {"target": "redirect-check", "statuses": [503, 404, 404]},
|
|
"routes": [{
|
|
"id": "redirect-check",
|
|
"internal": true,
|
|
"path_prefix": "/",
|
|
"source": {"type": "reverse_proxy"}
|
|
}]
|
|
});
|
|
let mut valid: Vhost = serde_json::from_value(base.clone()).unwrap();
|
|
valid.validate().unwrap();
|
|
assert_eq!(valid.error_fallback.as_ref().unwrap().statuses, vec![404, 503]);
|
|
|
|
let mut public_target: Vhost = serde_json::from_value(base.clone()).unwrap();
|
|
public_target.routes[0].internal = false;
|
|
assert!(public_target
|
|
.validate()
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("internal location"));
|
|
|
|
let mut cyclic: Vhost = serde_json::from_value(base).unwrap();
|
|
cyclic.routes[0].error_fallback = Some(ErrorFallback {
|
|
target: "redirect-check".into(),
|
|
statuses: vec![404],
|
|
});
|
|
assert!(cyclic
|
|
.validate()
|
|
.unwrap_err()
|
|
.to_string()
|
|
.contains("recursive error fallback"));
|
|
}
|
|
#[test]
|
|
fn collision_explains_addresses_owners_and_protocol_limits() {
|
|
let mut config = Config::default();
|
|
config.listeners.endpoints = serde_json::from_value(json!([
|
|
{"id":"public","name":"Public TLS","binding":{"address":"0.0.0.0:8443","tls":true}},
|
|
{"id":"private","name":"Private TLS","binding":{"address":"127.0.0.1:8443","tls":true,"http3":{"max_concurrent_connections":12}}}
|
|
])).unwrap();
|
|
let policy = ValidationPolicy {
|
|
proxy_addr: "127.0.0.1:8080".parse().unwrap(),
|
|
admin_addr: "127.0.0.1:9090".parse().unwrap(),
|
|
tls_addr: None,
|
|
cert_dir: None,
|
|
data_dir: None,
|
|
};
|
|
let message = listener_plan(&config, &policy).unwrap_err().to_string();
|
|
for expected in [
|
|
"0.0.0.0:8443",
|
|
"127.0.0.1:8443",
|
|
"Public TLS",
|
|
"Private TLS",
|
|
"H3 limits",
|
|
"max_concurrent_connections",
|
|
] {
|
|
assert!(message.contains(expected), "{message}");
|
|
}
|
|
}
|
|
}
|