v0.34.4
This commit is contained in:
1 parent
1fd81eaf5f
commit
ee174bc283
36 files changed
+908
-183
No files matched your search
+18
-3
@@ -58,7 +58,10 @@ 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();
|
||||
@@ -66,7 +69,10 @@ 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(),
|
||||
@@ -276,6 +282,10 @@ pub struct Vhost {
|
||||
pub tinyauth: crate::tinyauth::Settings,
|
||||
#[serde(default)]
|
||||
pub certificate_id: Option<String>,
|
||||
/// Explicitly accept serving an SNI name with a certificate that does not
|
||||
/// contain it in DNS SAN. Clients will still report a TLS name mismatch.
|
||||
#[serde(default)]
|
||||
pub allow_certificate_mismatch: bool,
|
||||
/// Optional protocol/cipher override. None inherits the global TLS policy.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tls_policy: Option<crate::tls::ProtocolPolicy>,
|
||||
@@ -302,13 +312,18 @@ pub struct Vhost {
|
||||
/// Optional reusable ACL profile. The profile and the inline ACL are both enforced.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub acl_profile_id: Option<String>,
|
||||
/// Additional reusable profiles. Explicit denies outrank all allows across
|
||||
/// selected profiles. Inline ACL remains an independent restriction.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub acl_profile_ids: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub acl: Option<Acl>,
|
||||
#[serde(default)]
|
||||
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,
|
||||
|
||||
+46
-16
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user