This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 09:30:21 +02:00
1 parent 1dea7f55c8
commit c819f0a335
151 files changed
+7610 -4264

No files matched your search

+76 -44
View File
@@ -92,57 +92,89 @@ fn canonical_url(scheme: &str, authority: &str, target: &str) -> String {
fn normalized_matcher(request: &InvalidationRequest) -> Result<SelectorMatcher> {
ensure!(!request.value.is_empty(), "invalidation value is required");
ensure!(request.value.len() <= 8192, "invalidation value is too long");
ensure!(
request.value.len() <= 8192,
"invalidation value is too long"
);
if let Some(vhost) = &request.vhost {
ensure!(!vhost.is_empty() && vhost.len() <= 64, "invalid vhost selector");
ensure!(
!vhost.is_empty() && vhost.len() <= 64,
"invalid vhost selector"
);
}
if let Some(host) = &request.host {
ensure!(!host.is_empty() && host.len() <= 253, "invalid host selector");
ensure!(
!host.is_empty() && host.len() <= 253,
"invalid host selector"
);
}
let mut value = request.value.trim().to_owned();
let mut scheme = None;
let mut host = request.host.as_ref().map(|value| value.to_ascii_lowercase());
let regex = match request.selector {
InvalidationSelector::Exact | InvalidationSelector::Prefix => {
if value.starts_with("http://") || value.starts_with("https://") {
let url = url::Url::parse(&value).context("invalid purge URL")?;
ensure!(url.username().is_empty() && url.password().is_none(), "purge URL must not contain credentials");
ensure!(url.fragment().is_none(), "purge URL must not contain a fragment");
scheme = Some(url.scheme().to_ascii_lowercase());
let authority = match url.port() {
Some(port) => format!("{}:{port}", url.host_str().context("purge URL host missing")?),
None => url.host_str().context("purge URL host missing")?.to_owned(),
};
host = Some(authority.to_ascii_lowercase());
value = url.path().to_owned();
if let Some(query) = url.query() {
value.push('?');
value.push_str(query);
let mut host = request
.host
.as_ref()
.map(|value| value.to_ascii_lowercase());
let regex =
match request.selector {
InvalidationSelector::Exact | InvalidationSelector::Prefix => {
if value.starts_with("http://") || value.starts_with("https://") {
let url = url::Url::parse(&value).context("invalid purge URL")?;
ensure!(
url.username().is_empty() && url.password().is_none(),
"purge URL must not contain credentials"
);
ensure!(
url.fragment().is_none(),
"purge URL must not contain a fragment"
);
scheme = Some(url.scheme().to_ascii_lowercase());
let authority = match url.port() {
Some(port) => format!(
"{}:{port}",
url.host_str().context("purge URL host missing")?
),
None => url.host_str().context("purge URL host missing")?.to_owned(),
};
host = Some(authority.to_ascii_lowercase());
value = url.path().to_owned();
if let Some(query) = url.query() {
value.push('?');
value.push_str(query);
}
}
ensure!(
value.starts_with('/'),
"exact/prefix invalidation must use an absolute URL or /path target"
);
None
}
ensure!(value.starts_with('/'), "exact/prefix invalidation must use an absolute URL or /path target");
None
}
InvalidationSelector::Tag => {
ensure!(value.len() <= 128, "cache tag is too long");
ensure!(value.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.' | b':' | b'/')), "invalid cache tag");
None
}
InvalidationSelector::Regex => {
ensure!(value.len() <= 512, "regex is too long");
Some(
regex::RegexBuilder::new(&value)
.size_limit(1 << 20)
.build()
.context("invalid or too expensive regex")?,
)
}
InvalidationSelector::CacheKey => {
ensure!(parse_key_hex(&value).is_some(), "cache_key must be 64 hexadecimal characters");
value.make_ascii_lowercase();
None
}
};
InvalidationSelector::Tag => {
ensure!(value.len() <= 128, "cache tag is too long");
ensure!(
value.bytes().all(|b| b.is_ascii_alphanumeric()
|| matches!(b, b'_' | b'-' | b'.' | b':' | b'/')),
"invalid cache tag"
);
None
}
InvalidationSelector::Regex => {
ensure!(value.len() <= 512, "regex is too long");
Some(
regex::RegexBuilder::new(&value)
.size_limit(1 << 20)
.build()
.context("invalid or too expensive regex")?,
)
}
InvalidationSelector::CacheKey => {
ensure!(
parse_key_hex(&value).is_some(),
"cache_key must be 64 hexadecimal characters"
);
value.make_ascii_lowercase();
None
}
};
Ok(SelectorMatcher {
selector: request.selector,
value,
@@ -198,7 +230,7 @@ fn matcher_matches(
InvalidationSelector::Tag => {
host_matches(matcher, scheme, authority, domain)
&& tags.iter().any(|tag| tag == &matcher.value)
},
}
InvalidationSelector::Regex => {
if !host_matches(matcher, scheme, authority, domain) {
return false;
+13 -6
View File
@@ -56,24 +56,31 @@ impl Cache {
let started = Instant::now();
let matcher = normalized_matcher(&request)?;
if requires_wide_confirmation(&matcher) {
ensure!(request.confirm_wide, "wide invalidation requires confirm_wide=true");
ensure!(
request.confirm_wide,
"wide invalidation requires confirm_wide=true"
);
}
match request.operation {
InvalidationOperation::Ban => {
let ttl = request.ttl_seconds.unwrap_or(DEFAULT_BAN_TTL_SECONDS);
ensure!((1..=MAX_BAN_TTL_SECONDS).contains(&ttl), "BAN ttl_seconds must be 1..604800");
ensure!(
(1..=MAX_BAN_TTL_SECONDS).contains(&ttl),
"BAN ttl_seconds must be 1..604800"
);
let now = SystemTime::now();
let mut bans = self.bans.lock().unwrap_or_else(|e| e.into_inner());
bans.retain(|rule| !rule.expired(now));
ensure!(bans.len() < MAX_BAN_RULES, "active BAN rule limit reached");
let id = self.ban_sequence.fetch_add(1, Ordering::Relaxed).wrapping_add(1);
let id = self
.ban_sequence
.fetch_add(1, Ordering::Relaxed)
.wrapping_add(1);
bans.push(BanRule {
id,
matcher: matcher.clone(),
created_at: now,
expires_at: now
.checked_add(Duration::from_secs(ttl))
.unwrap_or(now),
expires_at: now.checked_add(Duration::from_secs(ttl)).unwrap_or(now),
});
self.counters.bans_created.fetch_add(1, Ordering::Relaxed);
Ok(InvalidationResult {