This commit is contained in:
Mateusz Gruszczyński committed 2026-10-22 15:31:47 +02:00
1 parent 3ea00045bf
commit 1cd6b7ac99
22 files changed
+550 -149

No files matched your search

+89 -24
View File
@@ -44,19 +44,89 @@ fn directives(headers: &HeaderMap) -> Option<HashMap<String, Option<String>>> {
}
Some(result)
}
/// Keep all client cache restrictions except the hard-reload directives that
/// Varnish normally disregards for a fresh shared object. Never mutate the
/// actual request (it is still forwarded to the origin and used for Vary).
fn compatible_freshness_headers<'a>(
headers: &'a HeaderMap,
compatible: bool,
) -> std::borrow::Cow<'a, HeaderMap> {
if !compatible || (!headers.contains_key("cache-control") && !headers.contains_key("pragma")) {
return std::borrow::Cow::Borrowed(headers);
}
// An invalid cache-control header cannot be made valid by filtering it.
// Also avoid cloning the full map for ordinary requests with no reload
// directives, including min-fresh, max-stale and only-if-cached.
let Some(control) = directives(headers) else {
return std::borrow::Cow::Borrowed(headers);
};
let remove_reload = control.contains_key("no-cache")
|| control.get("max-age").and_then(|value| value.as_deref())
.and_then(|value| value.parse::<u64>().ok()) == Some(0);
if !remove_reload && !pragma_no_cache(headers) {
return std::borrow::Cow::Borrowed(headers);
}
let mut freshness = headers.clone();
let mut changed = false;
if pragma_no_cache(headers) {
// Pragma only affects freshness through its `no-cache` token in this
// implementation. Preserve other tokens, if any.
freshness.remove("pragma");
for header in headers.get_all("pragma") {
let Ok(text) = header.to_str() else {
return std::borrow::Cow::Borrowed(headers);
};
let retained = text.split(',')
.map(str::trim)
.filter(|token| !token.eq_ignore_ascii_case("no-cache"))
.collect::<Vec<_>>();
if !retained.is_empty() {
let Ok(value) = HeaderValue::from_str(&retained.join(", ")) else {
return std::borrow::Cow::Borrowed(headers);
};
freshness.append("pragma", value);
}
}
changed = true;
}
if headers.contains_key("cache-control") {
let mut retained_values = Vec::new();
for header in headers.get_all("cache-control") {
let Ok(text) = header.to_str() else {
return std::borrow::Cow::Borrowed(headers);
};
let retained = text.split(',').map(str::trim).filter(|token| {
let (name, value) = token.split_once('=').unwrap_or((token, ""));
let name = name.trim();
let skip = name.eq_ignore_ascii_case("no-cache")
|| (name.eq_ignore_ascii_case("max-age")
&& value.trim().trim_matches('"').parse::<u64>() == Ok(0));
if skip { changed = true; }
!skip
}).collect::<Vec<_>>();
if !retained.is_empty() {
let Ok(value) = HeaderValue::from_str(&retained.join(", ")) else {
return std::borrow::Cow::Borrowed(headers);
};
retained_values.push(value);
}
}
if changed {
freshness.remove("cache-control");
for value in retained_values {
freshness.append("cache-control", value);
}
}
}
if changed { std::borrow::Cow::Owned(freshness) }
else { std::borrow::Cow::Borrowed(headers) }
}
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)
compatible_freshness_headers(headers, settings.varnish_compatible)
}
pub fn only_if_cached(headers: &HeaderMap) -> bool {
@@ -173,25 +243,22 @@ 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;
}
// Compatibility only relaxes browser hard-reload freshness requests.
// Retain no-store, only-if-cached, min-fresh and max-stale semantics.
let Some(control) = directives(headers) else {
return RequestPolicy::Bypass("request_cache_control_invalid");
};
if control.contains_key("no-store") {
return RequestPolicy::Bypass("request_no_store");
}
if control.contains_key("no-cache") {
if !settings.varnish_compatible && control.contains_key("no-cache") {
return RequestPolicy::Revalidate("request_no_cache");
}
if let Some(value) = control.get("max-age") {
let Some(value) = value.as_deref().and_then(|value| value.parse::<u64>().ok()) else {
return RequestPolicy::Bypass("request_cache_control_invalid");
};
if value == 0 {
if value == 0 && !settings.varnish_compatible {
return RequestPolicy::Revalidate("request_max_age");
}
}
@@ -209,7 +276,10 @@ pub fn request_policy(
return RequestPolicy::Bypass("request_cache_control_invalid");
}
}
if !headers.contains_key("cache-control") && pragma_no_cache(headers) {
if !settings.varnish_compatible
&& !headers.contains_key("cache-control")
&& pragma_no_cache(headers)
{
return RequestPolicy::Revalidate("request_pragma_no_cache");
}
RequestPolicy::Use
@@ -404,13 +474,8 @@ pub fn response_reason(
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";
}
if settings.varnish_compatible && surrogate_no_store(headers) {
return "response_surrogate_control";
}
for value in headers.get_all("vary") {
if value.to_str().ok().is_none_or(|value| {
+25 -19
View File
@@ -92,15 +92,19 @@ 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))
{
// Surrogate-Control: no-store is a shared-cache veto in compatibility
// mode, regardless of public/fallback TTL or any other directive.
if settings.varnish_compatible && surrogate_no_store(headers) {
return None;
}
let requires_revalidation = control.contains_key("no-cache");
// Explicitly configured fallback TTL is an opt-in origin no-cache
// override *only* for public responses. With fallback disabled, keep
// the pre-0.34.7 validator-based revalidation and never grant a HIT.
let override_public_no_cache = settings.varnish_compatible
&& control.contains_key("no-cache")
&& control.contains_key("public")
&& settings.default_ttl_seconds > 0;
let requires_revalidation = control.contains_key("no-cache") && !override_public_no_cache;
let must_revalidate = control.contains_key("must-revalidate");
let proxy_revalidate = control.contains_key("proxy-revalidate");
let parse_window = |name: &str| -> Option<u64> {
@@ -124,7 +128,9 @@ pub fn plan(
None => now,
};
let declared_ttl =
if let Some(value) = control.get("s-maxage").or_else(|| control.get("max-age")) {
if override_public_no_cache {
Some(settings.default_ttl_seconds)
} else if let Some(value) = control.get("s-maxage").or_else(|| control.get("max-age")) {
Some(value.as_ref()?.parse::<u64>().ok()?)
} else if let Some(expires) = headers.get("expires") {
Some(
@@ -156,17 +162,17 @@ 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;
// Retain zero-TTL and already-expired representations for conditional
// revalidation, client max-stale or stale-if-error. They must not be
// served as fresh HITs. Varnish-style grace is only implicit when the
// origin did not explicitly set stale-while-revalidate (including =0),
// the object arrived fresh and no origin no-cache rule is in effect.
if settings.varnish_compatible
&& !control.contains_key("stale-while-revalidate")
&& !control.contains_key("no-cache")
&& initial_age.as_secs() < ttl
{
stale_while_revalidate = settings.varnish_grace_seconds;
}
if !requires_revalidation
&& initial_age.as_secs()