This commit is contained in:
Mateusz Gruszczyński committed 2026-10-10 20:56:36 +02:00
1 parent f213d853e2
commit 76b8bb7ee2
78 files changed
+7750 -5204

No files matched your search

+1
View File
@@ -1,4 +1,5 @@
include!("model/settings.rs");
include!("model/http_invalidation.rs");
include!("model/entry.rs");
include!("model/cache.rs");
include!("model/maintenance.rs");
+1
View File
@@ -78,6 +78,7 @@ impl Settings {
"cache bypass prefixes must be normalized absolute paths"
);
}
self.validate_http_invalidation_allowlist()?;
ensure!(
self.static_extensions.len() <= 64,
"at most 64 static cache extensions"
+31
View File
@@ -0,0 +1,31 @@
// A strict, human-editable IP/CIDR policy. The caller supplies only a client IP
// produced by the existing forwarding trust boundary, never an arbitrary header.
fn parse_invalidation_cidr(value: &str) -> anyhow::Result<ipnet::IpNet> {
let value = value.trim();
if let Ok(ip) = value.parse::<std::net::IpAddr>() {
return Ok(ipnet::IpNet::from(ip));
}
value.parse::<ipnet::IpNet>().map_err(|_| anyhow::anyhow!("invalid HTTP invalidation IP/CIDR: {value}"))
}
impl Settings {
fn validate_http_invalidation_allowlist(&self) -> anyhow::Result<()> {
ensure!(
self.http_invalidation_allowed_cidrs.len() <= 32,
"cache.http_invalidation_allowed_cidrs allows at most 32 entries"
);
let mut networks = std::collections::HashSet::new();
for input in &self.http_invalidation_allowed_cidrs {
ensure!(input.len() <= 64, "HTTP invalidation address too long");
let net = parse_invalidation_cidr(input)?;
ensure!(net.prefix_len() > 0, "HTTP invalidation cannot authorize the entire Internet");
ensure!(networks.insert(net), "duplicate HTTP invalidation IP/CIDR");
}
Ok(())
}
pub fn permits_http_invalidation(&self, client: std::net::IpAddr) -> bool {
let client = crate::vhost::canonical_ip(client);
self.http_invalidation_allowed_cidrs.iter().any(|value| {
parse_invalidation_cidr(value).ok().is_some_and(|network| network.contains(&client))
})
}
}
+4
View File
@@ -149,6 +149,9 @@ pub struct Settings {
pub max_ttl_seconds: u64,
pub bypass_query: bool,
pub bypass_prefixes: Vec<String>,
/// Actual client IP/CIDR allowlist for HTTP BAN/PURGE. Empty disables these
/// public-listener methods (independent of authenticated admin API).
pub http_invalidation_allowed_cidrs: Vec<String>,
/// For explicitly selected static extensions, ignore request Cookie when
/// deciding cache eligibility and when building the shared cache key.
pub static_ignore_cookies: bool,
@@ -210,6 +213,7 @@ impl Default for Settings {
max_ttl_seconds: 86_400,
bypass_query: false,
bypass_prefixes: Vec::new(),
http_invalidation_allowed_cidrs: Vec::new(),
// Static assets are normally representation-identical for logged-in and
// anonymous visitors. Keep this enabled by default once the shared cache
// itself is enabled; response-side privacy guards still reject Set-Cookie,
+3
View File
@@ -16,3 +16,6 @@ include!("tests/v022_cache_tests.rs");
#[cfg(test)]
include!("tests/cold_tier_tests.rs");
#[cfg(test)]
include!("tests/http_invalidation_tests.rs");
+41
View File
@@ -0,0 +1,41 @@
mod http_invalidation_tests {
use super::*;
#[test]
fn default_deny_and_strict_ipv4_ipv6_cidr_validation() {
let mut cache = Settings::default();
assert!(!cache.permits_http_invalidation("127.0.0.1".parse().unwrap()));
cache.http_invalidation_allowed_cidrs = vec!["192.0.2.42".into(), "2001:db8:10::/64".into()];
cache.validate().unwrap();
assert!(cache.permits_http_invalidation("192.0.2.42".parse().unwrap()));
assert!(!cache.permits_http_invalidation("192.0.2.43".parse().unwrap()));
assert!(cache.permits_http_invalidation("2001:db8:10::1".parse().unwrap()));
assert!(!cache.permits_http_invalidation("2001:db8:11::1".parse().unwrap()));
for invalid in ["0.0.0.0/0", "::/0", "127.0.0.1; 8.8.8.8", "not-an-ip", "10.0.0.0/33"] {
cache.http_invalidation_allowed_cidrs = vec![invalid.into()];
assert!(cache.validate().is_err(), "must reject {invalid}");
}
cache.http_invalidation_allowed_cidrs = vec!["192.0.2.42".into(), "192.0.2.42/32".into()];
assert!(cache.validate().is_err(), "duplicate ranges");
let old: Settings = serde_json::from_value(serde_json::json!({})).unwrap();
assert!(old.http_invalidation_allowed_cidrs.is_empty());
}
#[test]
fn url_qualified_invalidation_matches_only_the_requested_scheme_and_host() {
let requested = InvalidationRequest {
operation: InvalidationOperation::Purge,
selector: InvalidationSelector::Exact,
value: "https://a.example/a?v=1".into(),
host: Some("a.example".into()),
vhost: Some("frontend-1".into()),
ttl_seconds: None,
confirm_wide: false,
};
let matcher = normalized_matcher(&requested).unwrap();
assert_eq!(matcher.value, "/a?v=1");
assert_eq!(matcher.scheme.as_deref(), Some("https"));
assert!(host_matches(&matcher, "https", "a.example", "a.example"));
assert!(!host_matches(&matcher, "http", "a.example", "a.example"));
assert!(!host_matches(&matcher, "https", "b.example", "b.example"));
}
}