0.34.6_repo_cleanup
This commit is contained in:
1 parent
ce1a90397c
commit
133d2f0f91
273 files changed
+5969
-6926
No files matched your search
Vendored
+5
-24
@@ -1,4 +1,3 @@
|
||||
/// Reject malformed/duplicate directives instead of guessing their meaning.
|
||||
fn directives(headers: &HeaderMap) -> Option<HashMap<String, Option<String>>> {
|
||||
let mut result = HashMap::new();
|
||||
for header in headers.get_all("cache-control") {
|
||||
@@ -11,8 +10,6 @@ fn directives(headers: &HeaderMap) -> Option<HashMap<String, Option<String>>> {
|
||||
Some((k, v)) => {
|
||||
let v = v.trim();
|
||||
let value = if v.starts_with('"') {
|
||||
// Fail closed on truncated/escaped/compound values. Quoted
|
||||
// commas need not be supported by this conservative cache.
|
||||
let inner = v.strip_prefix('"')?.strip_suffix('"')?;
|
||||
if inner.contains(['"', '\\']) {
|
||||
return None;
|
||||
@@ -39,10 +36,6 @@ fn directives(headers: &HeaderMap) -> Option<HashMap<String, Option<String>>> {
|
||||
return None;
|
||||
}
|
||||
if key == "immutable" {
|
||||
// RFC 8246: immutable has no meaningful argument and duplicate
|
||||
// occurrences are equivalent to one occurrence. Preserve only
|
||||
// the presence of the directive instead of invalidating the
|
||||
// complete Cache-Control field.
|
||||
result.entry(key).or_insert(None);
|
||||
} else if result.insert(key, value).is_some() {
|
||||
return None;
|
||||
@@ -111,7 +104,11 @@ pub fn forced_request_policy(
|
||||
}
|
||||
for key in ["if-match", "if-unmodified-since", "upgrade"] {
|
||||
if headers.contains_key(key) {
|
||||
return RequestPolicy::Bypass(if key == "upgrade" { "request_upgrade" } else { "request_precondition" });
|
||||
return RequestPolicy::Bypass(if key == "upgrade" {
|
||||
"request_upgrade"
|
||||
} else {
|
||||
"request_precondition"
|
||||
});
|
||||
}
|
||||
}
|
||||
RequestPolicy::Use
|
||||
@@ -150,8 +147,6 @@ pub fn request_policy(
|
||||
for (key, reason) in [
|
||||
("authorization", "request_authorization"),
|
||||
("cookie", "request_cookie"),
|
||||
// Preconditions other than cache validators can produce 412 and must be
|
||||
// evaluated by the origin until RustProxy implements the full precondition set.
|
||||
("if-match", "request_precondition"),
|
||||
("if-unmodified-since", "request_precondition"),
|
||||
("upgrade", "request_upgrade"),
|
||||
@@ -166,15 +161,9 @@ pub fn request_policy(
|
||||
let Some(control) = directives(headers) else {
|
||||
return RequestPolicy::Bypass("request_cache_control_invalid");
|
||||
};
|
||||
// RFC 9111: request no-store forbids using a stored response and storing this
|
||||
// exchange. It is the hard client-side cache bypass.
|
||||
if control.contains_key("no-store") {
|
||||
return RequestPolicy::Bypass("request_no_store");
|
||||
}
|
||||
// no-cache does not mean no-store. It requires successful validation before a
|
||||
// stored response can be reused. max-age/min-fresh are evaluated against the
|
||||
// cached representation during lookup; only max-age=0 is an unconditional
|
||||
// request-side validation signal.
|
||||
if control.contains_key("no-cache") {
|
||||
return RequestPolicy::Revalidate("request_no_cache");
|
||||
}
|
||||
@@ -200,8 +189,6 @@ pub fn request_policy(
|
||||
return RequestPolicy::Bypass("request_cache_control_invalid");
|
||||
}
|
||||
}
|
||||
// HTTP/1.0 Pragma: no-cache is a revalidation request when Cache-Control did
|
||||
// not already define the policy. Other Pragma values do not disable caching.
|
||||
if !headers.contains_key("cache-control") && pragma_no_cache(headers) {
|
||||
return RequestPolicy::Revalidate("request_pragma_no_cache");
|
||||
}
|
||||
@@ -295,8 +282,6 @@ fn single_if_none_match_matches(headers: &HeaderMap, etag: &str) -> bool {
|
||||
&& token != "*"
|
||||
&& weak_etag(token).is_some_and(|candidate| candidate == target)
|
||||
}
|
||||
/// Varnish-like client-side conditional delivery for a selected cached response.
|
||||
/// If-None-Match takes precedence over If-Modified-Since.
|
||||
pub fn conditional_not_modified(request: &HeaderMap, response: &HeaderMap) -> bool {
|
||||
if request.contains_key("if-none-match") {
|
||||
return response
|
||||
@@ -326,11 +311,8 @@ pub fn make_not_modified(mut response: Response) -> Response {
|
||||
}
|
||||
response
|
||||
}
|
||||
/// Merge a successful backend 304 into the stored representation metadata.
|
||||
pub fn merge_revalidated_headers(cached: &HeaderMap, validation: &HeaderMap) -> HeaderMap {
|
||||
let mut merged = cached.clone();
|
||||
// A new validation starts a new age calculation. Preserve origin Age only if
|
||||
// it is explicitly supplied by the 304 response.
|
||||
if !validation.contains_key("age") {
|
||||
merged.remove("age");
|
||||
}
|
||||
@@ -364,7 +346,6 @@ pub fn varies_on(headers: &HeaderMap, wanted: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// Explains rejected storage; it never relaxes origin or transformed policies.
|
||||
pub fn response_reason(status: StatusCode, headers: &HeaderMap) -> &'static str {
|
||||
if status != StatusCode::OK {
|
||||
return "response_status";
|
||||
|
||||
Vendored
+10
-22
@@ -4,7 +4,11 @@ impl Plan {
|
||||
}
|
||||
|
||||
pub fn with_vary(mut self, name: &str) -> Self {
|
||||
if !self.vary.iter().any(|value| value.eq_ignore_ascii_case(name)) {
|
||||
if !self
|
||||
.vary
|
||||
.iter()
|
||||
.any(|value| value.eq_ignore_ascii_case(name))
|
||||
{
|
||||
self.vary.push(name.to_ascii_lowercase());
|
||||
self.vary.sort();
|
||||
}
|
||||
@@ -28,14 +32,7 @@ impl Plan {
|
||||
self.vary.extend(transformed.vary);
|
||||
self.vary.sort();
|
||||
self.vary.dedup();
|
||||
// The transformed response is the representation that is actually
|
||||
// written to cache. Its Content-Length therefore replaces the origin
|
||||
// length; comparing transformed bytes with the origin length rejects a
|
||||
// valid fill and causes a permanent MISS -> MISS loop.
|
||||
self.expected_length = transformed.expected_length;
|
||||
// Express both age clocks at the later observation point before taking
|
||||
// the stricter value. This avoids resetting or double-counting Age when
|
||||
// origin headers are transformed inside the proxy.
|
||||
if transformed.received >= self.received {
|
||||
let carried = self
|
||||
.initial_age
|
||||
@@ -87,9 +84,6 @@ pub fn plan(
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// Cache-policy and validator fields govern reuse rather than representation
|
||||
// identity. Response Vary drives a secondary key, matching Varnish-style
|
||||
// variants without fragmenting on unrelated request headers.
|
||||
let vary = vary_names(headers)?;
|
||||
let control = directives(headers)?;
|
||||
if ["no-store", "private", "must-understand"]
|
||||
@@ -141,22 +135,16 @@ pub fn plan(
|
||||
Some(v) => v.to_str().ok()?.parse::<u64>().ok()?,
|
||||
None => 0,
|
||||
};
|
||||
// RFC 9111 Section 4.2.3: the corrected initial age is the greater of
|
||||
// the Date-derived apparent age and the received Age plus response delay.
|
||||
// An upstream/CDN Age must be preserved on both transformed and normal HITs.
|
||||
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);
|
||||
|
||||
// `ttl` is an absolute freshness lifetime (measured from the origin's
|
||||
// Age clock), not the time since insertion into our HOT/COLD cache.
|
||||
// Capping it directly to max_ttl_seconds makes an otherwise fresh 30-day
|
||||
// upstream response with Age=18 days look expired when our own cap is 1 day.
|
||||
// Limit the REMAINING freshness from admission, but preserve the Age clock.
|
||||
// Origin freshness still wins: an origin response that has already expired
|
||||
// must never become fresh here. `no-cache` continues to require validation.
|
||||
let ttl = match declared_ttl {
|
||||
Some(value) => value.min(initial_age.as_secs().saturating_add(settings.max_ttl_seconds)),
|
||||
Some(value) => value.min(
|
||||
initial_age
|
||||
.as_secs()
|
||||
.saturating_add(settings.max_ttl_seconds),
|
||||
),
|
||||
None if requires_revalidation => 0,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
Vendored
+150
@@ -0,0 +1,150 @@
|
||||
mod scope_tests {
|
||||
use super::*;
|
||||
use http_body_util::BodyExt;
|
||||
async fn fill(cache: &Arc<Cache>, ticket: Ticket) {
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
..Settings::default()
|
||||
};
|
||||
let mut response = Response::new(Body::from("scope-payload"));
|
||||
response.headers_mut().insert(
|
||||
"cache-control",
|
||||
HeaderValue::from_static("public, max-age=30"),
|
||||
);
|
||||
let plan = plan(
|
||||
StatusCode::OK,
|
||||
response.headers(),
|
||||
&settings,
|
||||
Duration::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
cache
|
||||
.wrap(response, ticket, plan, &settings)
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn selective_purge_preserves_other_hosts_and_cancels_old_fills() {
|
||||
let cache = Cache::new(1);
|
||||
let ticket = |scope| {
|
||||
cache
|
||||
.ticket_scoped(
|
||||
1,
|
||||
scope,
|
||||
"http",
|
||||
"same.test",
|
||||
"root",
|
||||
"/",
|
||||
&HeaderMap::new(),
|
||||
)
|
||||
.unwrap()
|
||||
};
|
||||
let a = ticket("a");
|
||||
let b = ticket("b");
|
||||
fill(&cache, a.clone()).await;
|
||||
fill(&cache, b.clone()).await;
|
||||
assert_eq!(cache.scope_view("a").entries, 1);
|
||||
assert_eq!(cache.purge_scope("a"), 1);
|
||||
assert!(cache.get(&a).is_none());
|
||||
assert!(cache.get(&b).is_some());
|
||||
fill(&cache, a).await;
|
||||
assert_eq!(cache.scope_view("a").entries, 0);
|
||||
fill(&cache, ticket("a")).await;
|
||||
assert_eq!(cache.scope_view("a").entries, 1);
|
||||
}
|
||||
#[test]
|
||||
fn stale_snapshot_cannot_issue_cache_ticket_after_config_commit() {
|
||||
let cache = Cache::new(1);
|
||||
cache.set_control_revision(7);
|
||||
let ticket = |revision| cache.ticket_scoped_for_config(
|
||||
revision, 1, "vhost-a", "http", "same.test", "main", "/resource", &HeaderMap::new()
|
||||
);
|
||||
assert!(ticket(7).is_some(), "the current snapshot can cache");
|
||||
cache.set_control_revision(8);
|
||||
assert_eq!(cache.revision(), 1);
|
||||
assert!(ticket(7).is_none(), "late request from the previous snapshot must bypass");
|
||||
assert!(ticket(8).is_some(), "the new snapshot can reuse the same cache identity");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_vhost_settings_inherit_and_cannot_expand_shared_capacity() {
|
||||
let global = Settings::default();
|
||||
let effective = Overrides {
|
||||
enabled: Some(true),
|
||||
max_ttl_seconds: Some(30),
|
||||
expose_status: Some(true),
|
||||
..Overrides::default()
|
||||
}
|
||||
.resolve(&global)
|
||||
.unwrap();
|
||||
assert!(effective.enabled && effective.expose_status);
|
||||
let mut globally_disabled = global.clone();
|
||||
globally_disabled.enabled = false;
|
||||
assert!(!Overrides { enabled: Some(true), ..Overrides::default() }
|
||||
.resolve(&globally_disabled)
|
||||
.unwrap()
|
||||
.enabled);
|
||||
assert!(!Overrides { enabled: Some(false), ..Overrides::default() }
|
||||
.resolve(&global)
|
||||
.unwrap()
|
||||
.enabled);
|
||||
assert!(!Overrides::default()
|
||||
.resolve(&globally_disabled)
|
||||
.unwrap()
|
||||
.enabled);
|
||||
assert_eq!(effective.max_bytes, global.max_bytes);
|
||||
assert_eq!(effective.max_ttl_seconds, 30);
|
||||
assert!(Overrides {
|
||||
max_object_bytes: Some(global.max_object_bytes + 1),
|
||||
..Overrides::default()
|
||||
}
|
||||
.resolve(&global)
|
||||
.is_err());
|
||||
assert!(Overrides {
|
||||
max_ttl_seconds: Some(1),
|
||||
default_ttl_seconds: Some(2),
|
||||
..Overrides::default()
|
||||
}
|
||||
.resolve(&global)
|
||||
.is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsafe_force_cache_is_per_vhost_and_matches_path_or_target_prefix() {
|
||||
let force = UnsafeForce {
|
||||
enabled: true,
|
||||
targets: vec!["/convert?url=".into(), "/assets".into()],
|
||||
};
|
||||
force.validate().unwrap();
|
||||
assert!(force.matches(
|
||||
"/convert",
|
||||
"/convert?url=https%3A%2F%2Fexample.test%2Flist.txt"
|
||||
));
|
||||
assert!(force.matches("/assets/app.js", "/assets/app.js?v=1"));
|
||||
assert!(!force.matches("/api", "/api?url=x"));
|
||||
assert!(UnsafeForce {
|
||||
enabled: true,
|
||||
targets: vec!["https://example.test/".into()],
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forced_request_policy_keeps_protocol_safety_but_ignores_cookie_and_client_no_store() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("cookie", HeaderValue::from_static("session=abc"));
|
||||
headers.insert("authorization", HeaderValue::from_static("Bearer token"));
|
||||
headers.insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
assert_eq!(
|
||||
forced_request_policy(&Method::GET, &headers, true),
|
||||
RequestPolicy::Use
|
||||
);
|
||||
assert_eq!(
|
||||
forced_request_policy(&Method::POST, &headers, true),
|
||||
RequestPolicy::Bypass("request_method")
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user