This commit is contained in:
Mateusz Gruszczyński committed 2026-10-20 22:09:50 +02:00
1 parent 1fd81eaf5f
commit ee174bc283
36 files changed
+908 -183

No files matched your search

+46 -16
View File
@@ -71,7 +71,10 @@ 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)?;
@@ -93,9 +96,11 @@ impl Vhost {
let _ = self.www_redirect_pair()?;
let _ = self.routing_hosts()?;
}
if let Some(id) = &self.acl_profile_id {
ensure!(crate::config::valid_id(id), "invalid ACL profile id");
}
crate::acl::validate_profile_ids(self.acl_profile_id.as_deref(), &self.acl_profile_ids)?;
ensure!(
!self.allow_certificate_mismatch || self.certificate_id.is_some(),
"certificate mismatch override requires an explicitly selected certificate"
);
self.method_policy.validate()?;
ensure!(
!self.frontend.listener_ids.is_empty(),
@@ -159,7 +164,10 @@ 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()?;
@@ -197,13 +205,21 @@ 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
@@ -221,7 +237,9 @@ 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()?;
@@ -242,7 +260,8 @@ 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()?;
@@ -317,8 +336,10 @@ 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}"))
@@ -326,14 +347,23 @@ 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)
}