Files
RustProxy/src/cache/policy/response.rs
T
2026-10-22 14:20:03 +02:00

202 lines
6.8 KiB
Rust

impl Plan {
pub fn remaining_ttl(&self) -> u64 {
self.ttl.saturating_sub(self.initial_age.as_secs())
}
pub fn with_vary(mut self, name: &str) -> Self {
if !self
.vary
.iter()
.any(|value| value.eq_ignore_ascii_case(name))
{
self.vary.push(name.to_ascii_lowercase());
self.vary.sort();
}
self
}
pub fn without_vary(mut self, name: &str) -> Self {
self.vary.retain(|value| !value.eq_ignore_ascii_case(name));
self
}
pub fn restrict(mut self, transformed: Self) -> Self {
self.ttl = self.ttl.min(transformed.ttl);
self.stale_while_revalidate = self
.stale_while_revalidate
.min(transformed.stale_while_revalidate);
self.stale_if_error = self.stale_if_error.min(transformed.stale_if_error);
self.must_revalidate |= transformed.must_revalidate;
self.proxy_revalidate |= transformed.proxy_revalidate;
self.requires_revalidation |= transformed.requires_revalidation;
self.vary.extend(transformed.vary);
self.vary.sort();
self.vary.dedup();
self.expected_length = transformed.expected_length;
if transformed.received >= self.received {
let carried = self
.initial_age
.saturating_add(transformed.received.duration_since(self.received));
self.received = transformed.received;
self.initial_age = carried.max(transformed.initial_age);
} else {
let carried = transformed
.initial_age
.saturating_add(self.received.duration_since(transformed.received));
self.initial_age = self.initial_age.max(carried);
}
self
}
}
pub fn plan(
status: StatusCode,
headers: &HeaderMap,
settings: &Settings,
response_delay: Duration,
) -> Option<Plan> {
if ["age", "date", "expires", "content-length"]
.iter()
.any(|name| headers.get_all(*name).iter().count() > 1)
{
return None;
}
if status != StatusCode::OK
|| headers.contains_key("set-cookie")
|| headers.contains_key("content-range")
|| headers.contains_key("trailer")
{
return None;
}
let bytes: usize = headers
.iter()
.map(|(k, v)| k.as_str().len() + v.as_bytes().len() + 32)
.sum();
if bytes > MAX_HEADER_BYTES {
return None;
}
if headers
.get("content-type")
.and_then(|v| v.to_str().ok())
.is_some_and(|v| {
let v = v.trim_start().to_ascii_lowercase();
v.starts_with("text/event-stream") || v.starts_with("application/grpc")
})
{
return None;
}
let vary = vary_names(headers)?;
let control = directives(headers)?;
if ["no-store", "private", "must-understand"]
.iter()
.any(|name| control.contains_key(*name))
{
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");
let parse_window = |name: &str| -> Option<u64> {
match control.get(name) {
None => Some(0),
Some(Some(value)) => value.parse::<u64>().ok().map(|v| v.min(604_800)),
Some(None) => None,
}
};
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")
&& !headers.contains_key("last-modified")
{
return None;
}
let now = SystemTime::now();
let date = match headers.get("date") {
Some(v) => httpdate::parse_http_date(v.to_str().ok()?).ok()?,
None => now,
};
let declared_ttl =
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(
httpdate::parse_http_date(expires.to_str().ok()?)
.ok()?
.duration_since(date)
.ok()?
.as_secs(),
)
} else if control.contains_key("public") {
Some(settings.default_ttl_seconds)
} else {
None
};
let age = match headers.get("age") {
Some(v) => v.to_str().ok()?.parse::<u64>().ok()?,
None => 0,
};
let apparent_age = now.duration_since(date).unwrap_or_default();
let corrected_age_value = Duration::from_secs(age).saturating_add(response_delay);
let initial_age = apparent_age.max(corrected_age_value);
let ttl = match declared_ttl {
Some(value) => value.min(
initial_age
.as_secs()
.saturating_add(settings.max_ttl_seconds),
),
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(
ttl,
stale_while_revalidate,
stale_if_error,
has_validator(headers),
)
{
return None;
}
let expected_length = match headers.get("content-length") {
Some(v) => Some(v.to_str().ok()?.parse::<usize>().ok()?),
None => None,
};
if expected_length.is_some_and(|n| n > settings.max_object_bytes) {
return None;
}
Some(Plan {
received: Instant::now(),
initial_age,
ttl,
stale_while_revalidate,
stale_if_error,
must_revalidate,
proxy_revalidate,
expected_length,
requires_revalidation,
vary,
})
}