This commit is contained in:
Mateusz Gruszczyński committed 2026-10-20 22:47:13 +02:00
1 parent ee174bc283
commit 52e7ce7b46
24 files changed
+672 -418

No files matched your search

+3 -10
View File
@@ -58,10 +58,7 @@ pub struct MethodPolicy {
}
impl MethodPolicy {
pub fn validate(&mut self) -> Result<()> {
ensure!(
self.methods.len() <= 32,
"method policy allows at most 32 methods"
);
ensure!(self.methods.len() <= 32, "method policy allows at most 32 methods");
let mut seen = std::collections::HashSet::new();
for method in &mut self.methods {
*method = method.trim().to_ascii_uppercase();
@@ -69,10 +66,7 @@ impl MethodPolicy {
!method.is_empty() && method.len() <= 32 && method.parse::<http::Method>().is_ok(),
"invalid HTTP method in method policy"
);
ensure!(
seen.insert(method.clone()),
"duplicate HTTP method in method policy"
);
ensure!(seen.insert(method.clone()), "duplicate HTTP method in method policy");
}
ensure!(
self.mode == MethodPolicyMode::Off || !self.methods.is_empty(),
@@ -322,8 +316,7 @@ pub struct Vhost {
pub headers: HeaderRules,
#[serde(default)]
pub security_headers: SecurityHeaders,
#[serde(default)]
pub user_agent_blocking: crate::user_agent::Policy,
#[serde(default)] pub user_agent_blocking: crate::user_agent::Policy,
/// Nginx-style upstream timeout controls. Empty values inherit process defaults.
#[serde(default)]
pub proxy_timeouts: ProxyTimeouts,
+13 -41
View File
@@ -71,10 +71,7 @@ impl Vhost {
!self.websocket_rfc9220 || self.http3_enabled,
"RFC 9220 requires HTTP/3 to be enabled for the vhost"
);
ensure!(
!self.frontend.hosts.is_empty(),
"vhost requires at least one hostname"
);
ensure!(!self.frontend.hosts.is_empty(), "vhost requires at least one hostname");
let mut hosts = std::collections::HashSet::new();
for host in &mut self.frontend.hosts {
*host = crate::config::normalize_host_pattern(host)?;
@@ -164,10 +161,7 @@ impl Vhost {
"location cannot define both a local response and a redirect"
);
if let Some(id) = &route.acl_profile_id {
ensure!(
crate::config::valid_id(id),
"invalid location ACL profile id"
);
ensure!(crate::config::valid_id(id), "invalid location ACL profile id");
}
route.country_access.validate()?;
route.method_policy.validate()?;
@@ -205,21 +199,13 @@ impl Vhost {
// Reject cycles at configuration time; a runtime depth guard remains as a
// second line of defense for hot-reload/state inconsistencies.
let mut starts = vec![None];
starts.extend(
self.routes
.iter()
.filter(|route| route.enabled)
.map(|route| Some(route.id.as_str())),
);
starts.extend(self.routes.iter().filter(|route| route.enabled).map(|route| Some(route.id.as_str())));
for start in starts {
let mut seen = std::collections::HashSet::new();
let mut current = start;
loop {
let key = current.unwrap_or("<main>");
ensure!(
seen.insert(key.to_owned()),
"recursive error fallback loop detected"
);
ensure!(seen.insert(key.to_owned()), "recursive error fallback loop detected");
let fallback = match current {
None => self.error_fallback.as_ref(),
Some(id) => self
@@ -237,9 +223,7 @@ impl Vhost {
if let Some(custom) = &member.health_check {
let mut settings = self.load_balancing.clone();
settings.health_check = custom.clone();
settings
.validate()
.with_context(|| format!("backend {} health check", member.id))?;
settings.validate().with_context(|| format!("backend {} health check", member.id))?;
}
}
self.proxy_timeouts.validate()?;
@@ -260,8 +244,7 @@ impl Vhost {
"choose either Tinyauth or Basic authentication"
);
self.pages.validate()?;
self.transform
.resolve(&crate::transform::Settings::default())?;
self.transform.resolve(&crate::transform::Settings::default())?;
self.security_headers.compile()?;
self.user_agent_blocking.compile()?;
self.headers.compile()?;
@@ -336,10 +319,8 @@ pub fn validate_listener(listener: Listener) -> Result<()> {
/// counterpart must both be routable, even if only one was configured.
fn www_partner(host: &str) -> Result<String> {
if let Some(bare) = host.strip_prefix("www.") {
ensure!(
!bare.is_empty() && bare.parse::<std::net::IpAddr>().is_err(),
"www redirect requires a DNS hostname"
);
ensure!(!bare.is_empty() && bare.parse::<std::net::IpAddr>().is_err(),
"www redirect requires a DNS hostname");
crate::config::normalize_host(bare)
} else {
crate::config::normalize_host(&format!("www.{host}"))
@@ -347,23 +328,14 @@ fn www_partner(host: &str) -> Result<String> {
}
impl Vhost {
fn www_canonical_host(&self) -> Result<&str> {
let canonical = self
.www_redirect_target
.as_deref()
let canonical = self.www_redirect_target.as_deref()
.or_else(|| self.frontend.hosts.first().map(String::as_str))
.ok_or_else(|| anyhow::anyhow!("vhost requires at least one hostname"))?;
ensure!(
!canonical.starts_with("*.") && canonical.parse::<std::net::IpAddr>().is_err(),
"www redirect requires an exact DNS hostname"
);
ensure!(!canonical.starts_with("*.") && canonical.parse::<std::net::IpAddr>().is_err(),
"www redirect requires an exact DNS hostname");
let partner = www_partner(canonical)?;
ensure!(
self.frontend
.hosts
.iter()
.any(|host| host == canonical || host == &partner),
"www redirect target or its www/non-www partner must belong to this vhost"
);
ensure!(self.frontend.hosts.iter().any(|host| host == canonical || host == &partner),
"www redirect target or its www/non-www partner must belong to this vhost");
Ok(canonical)
}