This commit is contained in:
Mateusz Gruszczyński committed 2026-10-22 14:20:03 +02:00
1 parent 133d2f0f91
commit 3ea00045bf
27 files changed
+446 -136

No files matched your search

+46 -1
View File
@@ -44,6 +44,21 @@ fn directives(headers: &HeaderMap) -> Option<HashMap<String, Option<String>>> {
}
Some(result)
}
pub fn request_freshness_headers<'a>(
headers: &'a HeaderMap,
settings: &Settings,
) -> std::borrow::Cow<'a, HeaderMap> {
if !settings.varnish_compatible
|| (!headers.contains_key("cache-control") && !headers.contains_key("pragma"))
{
return std::borrow::Cow::Borrowed(headers);
}
let mut freshness = headers.clone();
freshness.remove("cache-control");
freshness.remove("pragma");
std::borrow::Cow::Owned(freshness)
}
pub fn only_if_cached(headers: &HeaderMap) -> bool {
headers
.get_all("cache-control")
@@ -158,6 +173,11 @@ pub fn request_policy(
return RequestPolicy::Bypass(reason);
}
}
// Varnish builtin.vcl performs a cache lookup without interpreting
// client Cache-Control or Pragma. Security bypasses above still apply.
if settings.varnish_compatible {
return RequestPolicy::Use;
}
let Some(control) = directives(headers) else {
return RequestPolicy::Bypass("request_cache_control_invalid");
};
@@ -346,7 +366,24 @@ pub fn varies_on(headers: &HeaderMap, wanted: &str) -> bool {
})
}
pub fn response_reason(status: StatusCode, headers: &HeaderMap) -> &'static str {
pub fn surrogate_no_store(headers: &HeaderMap) -> bool {
headers.get_all("surrogate-control").iter().any(|value| {
value.to_str().ok().is_some_and(|text| {
text.split([',', ';']).any(|part| {
part.trim()
.split_once('=')
.map_or(part.trim(), |(key, _)| key.trim())
.eq_ignore_ascii_case("no-store")
})
})
})
}
pub fn response_reason(
status: StatusCode,
headers: &HeaderMap,
settings: &Settings,
) -> &'static str {
if status != StatusCode::OK {
return "response_status";
}
@@ -367,6 +404,14 @@ pub fn response_reason(status: StatusCode, headers: &HeaderMap) -> &'static str
return "response_cache_control";
}
}
if settings.varnish_compatible {
if control.contains_key("no-cache") {
return "response_cache_control";
}
if surrogate_no_store(headers) {
return "response_surrogate_control";
}
}
for value in headers.get_all("vary") {
if value.to_str().ok().is_none_or(|value| {
value.split(',').any(|name| {
+21 -1
View File
@@ -92,6 +92,14 @@ pub fn plan(
{
return None;
}
// Varnish builtin.vcl treats these as uncacheable origin responses
// (hit-for-miss is not implemented by RustProxy). Never weaken private
// or no-store handling even if Surrogate-Control is also present.
if settings.varnish_compatible
&& (control.contains_key("no-cache") || surrogate_no_store(headers))
{
return None;
}
let requires_revalidation = control.contains_key("no-cache");
let must_revalidate = control.contains_key("must-revalidate");
let proxy_revalidate = control.contains_key("proxy-revalidate");
@@ -102,7 +110,7 @@ pub fn plan(
Some(None) => None,
}
};
let stale_while_revalidate = parse_window("stale-while-revalidate")?;
let mut stale_while_revalidate = parse_window("stale-while-revalidate")?;
let stale_if_error = parse_window("stale-if-error")?;
if requires_revalidation
&& !headers.contains_key("etag")
@@ -148,6 +156,18 @@ pub fn plan(
None if requires_revalidation => 0,
None => return None,
};
// Like Varnish's built-in VCL, a backend response with no remaining
// freshness is not a reusable shared object (Varnish stores a hit-for-miss
// marker instead; RustProxy does not implement those markers).
if settings.varnish_compatible && initial_age.as_secs() >= ttl {
return None;
}
// Varnish defaults to ten seconds of grace after a *positive* TTL.
// Keep RustProxy's existing must-revalidate/proxy-revalidate safety checks;
// never turn a no-cache or zero-TTL response into a grace HIT.
if settings.varnish_compatible && stale_while_revalidate == 0 {
stale_while_revalidate = 10;
}
if !requires_revalidation
&& initial_age.as_secs()
>= retention_deadline(