v0.26.7
This commit is contained in:
1 parent
9d6614c1b4
commit
a9f9450f80
16 files changed
+171
-47
No files matched your search
@@ -2172,9 +2172,9 @@
|
||||
type="checkbox" /><span>Cache-Status (RFC 9211)</span></label><label
|
||||
class="check"><input id="cache-resident-age-header"
|
||||
type="checkbox" /><span>X-Cache-Resident-Age</span></label></div>
|
||||
<p class="fine" data-i18n="cacheAgeHelp">Age is always sent on cache hits. Counters
|
||||
remain available even when diagnostic headers are hidden. Every configuration
|
||||
apply invalidates the cache.</p>
|
||||
<p class="fine" data-i18n="cacheAgeHelp">Age is sent on cache hits unless an
|
||||
explicit response-header rule removes or replaces it. Such Age rules are
|
||||
wire-only and do not change cache freshness.</p>
|
||||
</div>
|
||||
<div class="form-actions"><button class="primary" data-i18n="saveCache"
|
||||
type="submit">Apply cache policy</button><button class="outline"
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
for (const direction of ["request", "response"]) {
|
||||
const rules = value[direction] || []; if (!Array.isArray(rules) || rules.length > 32) fail(); for (const rule of rules) {
|
||||
if (!rule || typeof rule !== "object" || Object.keys(rule).some(k => !["op", "name", "value"].includes(k)) || !["set", "append", "remove"].includes(rule.op) || typeof rule.name !== "string" || rule.name.length > 128 || !/^[-!#$%&'*+.^_`|~0-9A-Za-z]+$/.test(rule.name)) fail();
|
||||
const name = rule.name.toLowerCase(); if (["host", "age", "connection", "content-length", "transfer-encoding", "te", "trailer", "content-encoding", "content-range", "upgrade", "keep-alive", "proxy-connection", "proxy-authorization", "proxy-authenticate", "forwarded", "x-real-ip", "x-request-id", "x-rust-proxy-backend", "x-rust-proxy-backend-time", "x-rustproxy-backend-time"].includes(name) || name.startsWith("x-forwarded-") || name.startsWith("sec-websocket-")) throw new Error(t("headersProtected"));
|
||||
const name = rule.name.toLowerCase(); if (["host", "connection", "content-length", "transfer-encoding", "te", "trailer", "content-encoding", "content-range", "upgrade", "keep-alive", "proxy-connection", "proxy-authorization", "proxy-authenticate", "forwarded", "x-real-ip", "x-request-id", "x-rust-proxy-backend", "x-rust-proxy-backend-time", "x-rustproxy-backend-time"].includes(name) || name.startsWith("x-forwarded-") || name.startsWith("sec-websocket-")) throw new Error(t("headersProtected"));
|
||||
if (rule.op === "remove") { if (Object.hasOwn(rule, "value") && rule.value !== null) fail(); } else if (typeof rule.value !== "string" || rule.value.length > 8192 || /[\r\n\0]/.test(rule.value)) fail();
|
||||
}
|
||||
} return value;
|
||||
|
||||
+36
-15
@@ -31,6 +31,7 @@ pub struct CompiledRule {
|
||||
op: Operation,
|
||||
name: HeaderName,
|
||||
value: Option<HeaderValue>,
|
||||
wire_only: bool,
|
||||
}
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct CompiledHeaders {
|
||||
@@ -43,16 +44,20 @@ impl HeaderRules {
|
||||
}
|
||||
pub fn compile(&self) -> Result<CompiledHeaders> {
|
||||
Ok(CompiledHeaders {
|
||||
request: compile_rules(&self.request)?,
|
||||
response: compile_rules(&self.response)?,
|
||||
request: compile_rules(&self.request, Direction::Request)?,
|
||||
response: compile_rules(&self.response, Direction::Response)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Direction {
|
||||
Request,
|
||||
Response,
|
||||
}
|
||||
fn protected(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"host"
|
||||
| "age"
|
||||
| "connection"
|
||||
| "content-length"
|
||||
| "transfer-encoding"
|
||||
@@ -74,7 +79,7 @@ fn protected(name: &str) -> bool {
|
||||
) || name.starts_with("x-forwarded-")
|
||||
|| name.starts_with("sec-websocket-")
|
||||
}
|
||||
fn compile_rules(rules: &[Rule]) -> Result<Vec<CompiledRule>> {
|
||||
fn compile_rules(rules: &[Rule], direction: Direction) -> Result<Vec<CompiledRule>> {
|
||||
ensure!(
|
||||
rules.len() <= 32,
|
||||
"at most 32 header rules per direction and scope"
|
||||
@@ -102,23 +107,39 @@ fn compile_rules(rules: &[Rule]) -> Result<Vec<CompiledRule>> {
|
||||
op: rule.op,
|
||||
name,
|
||||
value,
|
||||
wire_only: direction == Direction::Response
|
||||
&& rule.name.eq_ignore_ascii_case("age"),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
fn apply_rule(headers: &mut HeaderMap, rule: &CompiledRule) {
|
||||
match (&rule.op, &rule.value) {
|
||||
(Operation::Remove, _) => {
|
||||
headers.remove(&rule.name);
|
||||
}
|
||||
(Operation::Set, Some(value)) => {
|
||||
headers.insert(rule.name.clone(), value.clone());
|
||||
}
|
||||
(Operation::Append, Some(value)) => {
|
||||
headers.append(rule.name.clone(), value.clone());
|
||||
}
|
||||
_ => unreachable!("only compiled header rules reach the request path"),
|
||||
}
|
||||
}
|
||||
pub fn apply(headers: &mut HeaderMap, rules: &[CompiledRule]) {
|
||||
for rule in rules {
|
||||
match (&rule.op, &rule.value) {
|
||||
(Operation::Remove, _) => {
|
||||
headers.remove(&rule.name);
|
||||
}
|
||||
(Operation::Set, Some(value)) => {
|
||||
headers.insert(rule.name.clone(), value.clone());
|
||||
}
|
||||
(Operation::Append, Some(value)) => {
|
||||
headers.append(rule.name.clone(), value.clone());
|
||||
}
|
||||
_ => unreachable!("only compiled header rules reach the request path"),
|
||||
// Response Age rules are a wire-only presentation override. Applying
|
||||
// them here would let a configured value enter cache freshness/storage.
|
||||
if !rule.wire_only {
|
||||
apply_rule(headers, rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn apply_wire_age(headers: &mut HeaderMap, rules: &[CompiledRule]) {
|
||||
for rule in rules {
|
||||
if rule.wire_only {
|
||||
apply_rule(headers, rule);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,62 @@ mod tests {
|
||||
};
|
||||
assert!(rules.compile().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_age_is_allowed_but_applied_only_at_wire_stage() {
|
||||
let request_age = HeaderRules {
|
||||
request: vec![Rule {
|
||||
op: Operation::Set,
|
||||
name: "Age".into(),
|
||||
value: Some("7".into()),
|
||||
}],
|
||||
response: vec![],
|
||||
};
|
||||
let request_compiled = request_age.compile().unwrap();
|
||||
let mut request_headers = HeaderMap::new();
|
||||
apply(&mut request_headers, &request_compiled.request);
|
||||
assert_eq!(request_headers["age"], "7");
|
||||
|
||||
let response_age = HeaderRules {
|
||||
request: vec![],
|
||||
response: vec![Rule {
|
||||
op: Operation::Remove,
|
||||
name: "Age".into(),
|
||||
value: None,
|
||||
}],
|
||||
};
|
||||
let compiled = response_age.compile().unwrap();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("age", HeaderValue::from_static("42"));
|
||||
apply(&mut headers, &compiled.response);
|
||||
assert_eq!(headers["age"], "42");
|
||||
apply_wire_age(&mut headers, &compiled.response);
|
||||
assert!(!headers.contains_key("age"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn response_age_keeps_0266_rule_compatibility() {
|
||||
let rules = HeaderRules {
|
||||
request: vec![],
|
||||
response: vec![
|
||||
Rule {
|
||||
op: Operation::Set,
|
||||
name: "age".into(),
|
||||
value: Some("custom".into()),
|
||||
},
|
||||
Rule {
|
||||
op: Operation::Append,
|
||||
name: "age".into(),
|
||||
value: Some("7".into()),
|
||||
},
|
||||
],
|
||||
};
|
||||
let compiled = rules.compile().unwrap();
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("age", HeaderValue::from_static("42"));
|
||||
apply(&mut headers, &compiled.response);
|
||||
assert_eq!(headers["age"], "42");
|
||||
apply_wire_age(&mut headers, &compiled.response);
|
||||
assert_eq!(headers.get_all("age").iter().count(), 2);
|
||||
}
|
||||
}
|
||||
@@ -219,6 +219,20 @@ fn apply_response_rules(
|
||||
headers::strip_admin_set_cookie(headers);
|
||||
}
|
||||
}
|
||||
fn apply_response_age_rules(
|
||||
headers: &mut http::HeaderMap,
|
||||
snapshot: &crate::config::Snapshot,
|
||||
frontend: Option<&crate::config::CompiledFrontend>,
|
||||
route: Option<&crate::config::CompiledRoute>,
|
||||
) {
|
||||
crate::headers::apply_wire_age(headers, &snapshot.headers.response);
|
||||
if let Some(frontend) = frontend {
|
||||
crate::headers::apply_wire_age(headers, &frontend.headers.response);
|
||||
}
|
||||
if let Some(route) = route {
|
||||
crate::headers::apply_wire_age(headers, &route.headers.response);
|
||||
}
|
||||
}
|
||||
/// Final policies apply to cache hits, errors, static pages and ALL transports.
|
||||
fn finish_headers(
|
||||
response: &mut Response,
|
||||
|
||||
@@ -87,6 +87,12 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
&request_id,
|
||||
&state.active_listeners,
|
||||
);
|
||||
apply_response_age_rules(
|
||||
response.headers_mut(),
|
||||
&snapshot,
|
||||
frontend_context.as_deref(),
|
||||
route_context.as_ref(),
|
||||
);
|
||||
return response;
|
||||
};
|
||||
state.metrics.active.fetch_add(1, Ordering::Relaxed);
|
||||
@@ -356,6 +362,14 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
|
||||
cache.annotate_transformed(response.headers_mut(), transformed);
|
||||
}
|
||||
}
|
||||
if !minimal_welcome {
|
||||
apply_response_age_rules(
|
||||
response.headers_mut(),
|
||||
&snapshot,
|
||||
frontend_context.as_deref(),
|
||||
route_context.as_ref(),
|
||||
);
|
||||
}
|
||||
if response
|
||||
.extensions()
|
||||
.get::<crate::pages::PageServed>()
|
||||
|
||||
Reference in new issue
Block a user