This commit is contained in:
Mateusz Gruszczyński committed 2026-10-06 23:54:01 +02:00
1 parent 9d6614c1b4
commit a9f9450f80
16 files changed
+162 -38

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@
## 0.26.7
- Add HOT and COLD cache occupancy progress bars on `/cache` for configured object and byte limits, with explicit percentage/limit details.
- Verify cache `Age` progression on stored HITs, protect `Age` from custom response-header rules, and clarify that successful validator revalidation legitimately restarts the HTTP freshness age while `X-Cache-Resident-Age` continues to grow.
- Verify cache `Age` progression on stored HITs and retain 0.26.6 compatibility for existing custom `Age` rules. Response-side Age rules are deferred to the final wire stage so they cannot alter internal freshness/storage. Successful validator revalidation still legitimately restarts HTTP freshness age while `X-Cache-Resident-Age` continues to grow.
- Restore downstream compression for cookie-bearing responses only when RustProxy is serving a shared-cache HIT already accepted by cache policy or a local static Web Server response; privacy-sensitive dynamic/cookie responses remain excluded.
- Keep `Content-Encoding`/`Vary: Accept-Encoding` negotiation for gzip, Brotli, Zstandard and deflate, and make the bundled compression tester stop forcing `Cache-Control: no-cache` unless explicitly requested.
- Preserve compatible backend upstream clients across unrelated active-snapshot rebuilds so generation age, generation request count and rotation totals do not reset spuriously.
+1
View File
@@ -9,6 +9,7 @@ Current direction: 0.26.6 -> 0.26.7
- [x] Keep request-count and age-based backend client recycling covered by tests and visible diagnostics.
- [x] Refresh container documentation and include all supported Certbot DNS plugins in the shipped OCI image.
- [x] Review current installation/cache/backend lifecycle documentation for stale behavior statements.
- [x] Keep existing 0.26.6 custom `Age` rules loadable while isolating response-side Age overrides from cache freshness/storage semantics.
- [x] Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.6.
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
+15 -15
View File
@@ -2,12 +2,12 @@
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
c39fd74d633dc36cd7caa8d9b6785151b116752b79bd629d4d810cb03e1096cc AGENTS.md
abe0ebe15057929bec551cc1b92cfa25fb7aca616ba7e3bc8d23a4f02082a446 CHANGELOG.md
6308cefa554fa14a9e456eaeb5c98e329e4f8b922fc4b481062ac4323db8ac93 CHANGELOG.md
91e57afc931cf4cb929c85a5dca872ce1388f93d38645fbeb4bfd77ba89b2787 Cargo.lock
bd39e691ce35a1677c6c8a540423f83e51351d7aa9bddd090c9a92408ec2fc20 Cargo.toml
239713948df0d54504989f1ab30f5bf0128b90b4e5ac45d50a132aa033323baf Dockerfile
e88ff59b0e79d0c468178a4d9cbe6405175b61d7201112855c83b72b6e4f0fad README.md
f47bd96e96b4e87ab5fb7d319b96f3a57f9e24ce34ab9758aaaa17b255d74390 ROADMAP.md
340b8c205b773fbcfb096190bd854b320f05a0ac1e14a011ffb914f65dd87d70 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
834e8de4387354b23a2cb8c2d484f0f641dfed09472a9db12efcd0e683969a06 debian/README.md
@@ -42,7 +42,7 @@ ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2
d699f2261a0d7854e49e9b8bae44fa37008a7d2f6340a73cdc37e840722ea211 docs/API.md
96f0cdc4af8961b69c19d0f8204d467b2b70abf3882f1fe1a540c82cac9c063b docs/API_ACCESS.md
0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md
fec156479641ffa02c5702dd54f79de8efa1e9768557f2d38e44cd6868bba9e4 docs/CACHE.md
86469d17f9786c3e6f20c66791b47e10e11695d2f1a88c66c9ded157a585c85f docs/CACHE.md
6266163e17d2921468abdae92c1620266af8351321530e2c9b7dca6f3dd89c88 docs/CACHE_DIAGNOSTICS.md
c6003dcbc0b95e2cfa9523b7017186cc8b4826248b588af77111ac5eb15174f9 docs/CHANGELOG_TECHNICAL_ARCHIVE.md
d99fddfc982316c4e915cc6437fe339385350c49a2d06d570d42fd0100227e78 docs/CONTAINER.md
@@ -68,11 +68,11 @@ e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND
ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md
585ef0ac4492e03adca46949823e5a7538f56d9768090cd4dc080bfce4305b52 docs/ROADMAP_HISTORY.md
4bf7b3ea0bfe6c6b0ae0b7884b48b10ba29ff3a71611ec96957a3327d3c6ec88 docs/SECRET_STORE_BACKUP.md
63e033f08acafa0bb6ac3c6ff58ba56a185e34c9ad29f94725837aba4f36b2be docs/SECURITY.md
5dbdf45fddba198a28f511dbbd2f12bf7d2b88c41c3cbedbc20dfea3858779d3 docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
0c312c26930153a5e7ee77431d3df4d544b5aadbfcbbb1c8aba2a2d0a7d5e656 docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
365d85700ce3672afd419f74f3dfbf774278a08fddfed34e43b35bc168a42eca docs/TEST_REPORT.md
007f768f6989e49ee90c6d3c51036630042bdad3bca69b44d453a3c7f11380e4 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md
@@ -106,7 +106,7 @@ bf9b4cf4fec2c8ce287e1b3c2e7d4828e101dafe4d6c173f099819f22493d144 docs/V0.25.5_A
da5ec87b9482f2fc506c4334d9365f3bfaec89254a14e5585dd6460326b20e10 docs/V0.26.4_IMPLEMENTATION.md
0ddf5215c40c8366844350bc6462d8013dba747b6f3e8fbf477985af215e5e8d docs/V0.26.5_IMPLEMENTATION.md
dd9b5dcdbd53a04fd32458caaf03b56aa335f0153b1a0e332fdf230fec12680e docs/V0.26.6_IMPLEMENTATION.md
d21acc458442278628e524ba0b45eb7baa06a46633376a4abbcc35a8c34e5ff2 docs/V0.26.7_IMPLEMENTATION.md
f9667fe81b31762e64d36126a7e94b609316dfa856de8f8e3b96661dd2a918c6 docs/V0.26.7_IMPLEMENTATION.md
0f0e0e2920c76b2f82f38c6e60ed992a532c63851f593f2a1fa59e8d0014bb41 docs/VHOSTS.md
09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md
f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md
@@ -126,8 +126,8 @@ c294a5dcb94c1cf7edf72243bb533cc487a17452a7c0ac8c72f23cce0ec42fc6 grafana/rustpr
30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
5a36a455c9ed45749aca05fe28b3d3036e49cd748c6b89c2334973e7b070f2cb langs/en_US.json
ec860b4940fd237b8336154824b40b8f09fc009d3baa505771fc56c9ba2bda9e langs/pl_pl.json
9f367283c849d5217cb9386cf743221cc1d44ea4e76aa50a08234bbe71455837 langs/en_US.json
028c8d841c8cf246a021675054e638d6ab9f0c9785f823e2e4eec60a70fa8823 langs/pl_pl.json
8b258ac4654a046dc649c7fa8cef273c0f9431c9a12ea9b772dd0a1c95bb281f make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
@@ -147,7 +147,7 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
f79fa03aa2b5f0c04d37c108af55d8c43f9faf402f16553d5348c33a088e6108 scripts/check_source.py
1d88238e867dcc45c9666f765667674a7dce595a6db578dbfec15311b751f567 scripts/check_source.py
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
fe28d6d580a53d95a0fc318ebc4da5bbc71a23781bb827158848934985477035 scripts/deploy_core.py
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
@@ -235,7 +235,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js
631860ab3793ffd827c7cf19ddd049d14419509f2c2d8b27994b0e54baa0e32a src/admin/web/host404.js
1167c964000bb5dae1ba92b850ff76162570af340029d8788296160adc9f67b5 src/admin/web/index.html
4e8c9a1a1bd9870f0cd8aad5c9da80ff130497f11dbda116d79f91d9bfc129c6 src/admin/web/index.html
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json
a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html
@@ -247,7 +247,7 @@ f1ab20afa09f75f3635511756e36afea46a7917f04937d6f4e89c4e9f3f43204 src/admin/web/
f5831c2188233e425beecedc9e5e2829b620f80fc5326b73198024d27b81d1e7 src/admin/web/style.css
a8a73a4fa849ae49391fcd6143e900c515c8b577f71b55cbae9701fa9f5bbe47 src/admin/web/traffic-panel.js
e686d68fd6a368110a85dc4c5dc64e8093950d20d63e9aa44790c03dea6fc193 src/admin/web/transform-panel.js
106db335a6dd3e326955241e1d67caa5562a427b0356e7433cd0d92087342ce8 src/admin/web/vhosts/core.js
6856c230c4f6b18145e16efb6636433f8b21ce3ec5d34246d6561f0150ea0554 src/admin/web/vhosts/core.js
4d4ee863a3d77e5213ae4bdcad1f533a6222353b9c32e0b5f12f7ea60ab62bea src/admin/web/vhosts/editor.js
e0011edc708d245cb514be170c1bbe846a0298b81d86276fe29af05010d215a4 src/admin/web/vhosts/render.js
b3d8d3e22e4cbab10c5d588c268da6eb3075a9ec857728ce6b5d56e3ffd3c9a6 src/admin/web/web-server.js
@@ -340,8 +340,8 @@ fe571c85a924c827097f4be01b3a827a16bc5dd17a55fbf991ee3ad016641305 src/docker/tes
057e652f216a1478638cbb53b1c64ac88c7f413a65825caacbf893c5af004a63 src/forwarding/tests.rs
72215ef6911893f55cc82278169268f03d869b94728f118bf007fc555d609fde src/geoip.rs
e40171e72327bc4350ba95cedacde2f3c9148c26a5a232d9f790f4449f20a685 src/geoip/tests.rs
4e009ec10151a068fa83cf93cac78d2254d152e0386442586d6f5860ab2710b4 src/headers.rs
01cb98570ec388170420883e4ba18b2bf85ad1af97854dbc3a12b135db511c5c src/headers/tests.rs
13269be4ad8d3e4c91893753f654c3022e6f86d171c5d1ff21eda2d9185ec61f src/headers.rs
fbb6859780309f8cca1aeaba43fa67d13a1edf6f5a60c5c982d2067b71fb0846 src/headers/tests.rs
d0cd8589321595309834690fa9ddb98488886bc98f34ea45aa0ccb3035412fb5 src/http3.rs
2165a0a4b27d621985e2030326ad52b8bbd59d7b9e4f092f72f7e92b4e58b497 src/http3/body.rs
68bc11160bc5275a904e48af8698b37433096fb6d592364d8281d59bb9e2b3f4 src/http3/tests.rs
@@ -376,7 +376,7 @@ d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus
e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs
b5856fd38a957b2ca9c7e3606d84d13d5cfcf3a45b487b15ae1d2dc248cbda57 src/proxy/body.rs
16b9d4db3b20b2ab14c47ac14fcd18d3d8ca77c3ad9a1f37866ebfa60f379862 src/proxy/body/tests.rs
00e50a914511754266b4eae2626244587174bfa71fff91509c530bcde7f0d397 src/proxy/cache_helpers.rs
f0cb26ed34c35bacc74b914457acac8a611390de14cf46e2e6d3bcc848e914e8 src/proxy/cache_helpers.rs
cfb0910af8cd868bde3f6c9992319e46d5328d6d8f69c50701df0ea12bef2108 src/proxy/forward/cache_lookup.rs
212de22f407dd96800afcf65fd1fc7c40fede1b88f80d04850eb8d1227b08a5b src/proxy/forward/response.rs
a74deecbdbc5953c46b885a4627b3d1e820752b03921b80406ae11080ea56cc4 src/proxy/forward/route_cache.rs
@@ -389,7 +389,7 @@ c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/head
f6aef54204b5bb9305dd1367818c7ffd3cb273008404c9fab0317af1d13ba7a6 src/proxy/mod.rs
d75ff3e38affa49f9a8b9d6801454c1449458e5fbf07e08d51aa3f42abebafd5 src/proxy/tests/authority_tests.rs
622d44511b82883a34830d0ae3ea665fd40b58eaa85653f58588db92d55a17c3 src/proxy/types.rs
352260ef9e956534bc91b5cb888e3f8e881913c0bb988e6af95e49933d98814d src/proxy/upstream_helpers.rs
b09929990cd34db718c6bc82bd7ac9c711cea6dd52c1977febec6d800fbdd991 src/proxy/upstream_helpers.rs
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
d05e655ba0b3b322c657aaa9c2cbe7a0181064a896026b5eeaca347c4a0b7831 src/proxy/websocket/handshake.rs
b21dc9cf3817dd4e3273ce25887d63d7a772c477b86ca57f8d5b72b8154456e7 src/proxy/websocket/tests.rs
+4 -2
View File
@@ -284,8 +284,10 @@ Cache-Status: RustProxy; hit; ttl=556488; detail="cache_hit"; transformed
Origin lookalike values (`Cache-Status`, `X-Cache-Status` and the other RustProxy
cache diagnostics) are removed before RustProxy emits its own diagnostics. HTTP `Age`
remains present on reused cached responses because it is protocol freshness metadata,
not a debug header. The Cache dashboard also marks transformed objects explicitly.
is generated from cache freshness metadata on reused responses. For 0.26.6 configuration
compatibility an explicit response-header rule may set/append/remove `Age`, but that rule
runs only after cache lookup/revalidation/storage decisions and therefore cannot alter the
internal freshness clock. The Cache dashboard also marks transformed objects explicitly.
## Pool behavior
+4 -2
View File
@@ -121,8 +121,10 @@ update and purge paths. Purge is authenticated, CSRF-protected and audited, not
public PURGE/BAN capability.
Compression is opt-in, with authenticated/private/no-transform exclusions. Response
rules cannot overwrite protected framing/identity fields or the cache-controlled `Age`
header. Required Age remains on cache hits even when debug headers are disabled. Server hiding is cosmetic and
rules cannot overwrite protected framing/identity fields. For compatibility with 0.26.6,
custom `Age` rules remain accepted. Response-side `Age` is deferred to the final wire stage,
after RustProxy's cache freshness/revalidation logic, so it cannot alter stored age, TTL or
cache eligibility. Server hiding is cosmetic and
must not be considered an access-control or vulnerability mitigation substitute.
Standard public 80/443 ports are supported without globally forbidding remote
+1 -1
View File
@@ -12,7 +12,7 @@ Static source contracts require all four `/cache` occupancy meters (HOT objects,
The existing upstream-client unit coverage retains age-triggered rotation without user traffic. A new pool regression, `compatible_pool_rebuild_reuses_upstream_clients`, verifies that a compatible rebuild reuses the previous compiled backend/client object so rotation state remains continuous. Source contracts also require the `reusable_with` compatibility check, maintenance calls for normal/WebSocket clients and backend reuse path. These Rust tests require a compiler-enabled release host.
Cache regressions now require a stored HIT/dashboard object to advance `Age`, while compression regressions require `Age` to survive encoding and require cookie-bearing compression to stay blocked unless the internal cache/static safety marker is present. Source contracts protect the `Age` header from custom response rules and require the bounded HIT/static cookie-compression exception. The bundled compression tester no longer forces `Cache-Control: no-cache` unless `--revalidate` is requested.
Cache regressions now require a stored HIT/dashboard object to advance `Age`, while compression regressions require `Age` to survive encoding and require cookie-bearing compression to stay blocked unless the internal cache/static safety marker is present. Existing 0.26.6 `Age` rules stay loadable; response-side Age is tagged as a late wire-only override so the generic response-rule pass cannot influence cache freshness/storage. The bundled compression tester no longer forces `Cache-Control: no-cache` unless `--revalidate` is requested.
Documentation checks cover the 0.26.7 release note, Reader index, local Markdown links, cache diagnostics and container/ACME provider guidance. Packaging smoke tests verify the 0.26.7 release identity and deterministic source archive contract.
+6 -3
View File
@@ -36,9 +36,12 @@ Current cache, vhost/backend and installation documentation was reviewed alongsi
The cache freshness clock remains RFC-oriented: ordinary stored HITs advance `Age` from
`initial_age + resident elapsed time`, while a successful validator revalidation starts a
new freshness calculation. `X-Cache-Resident-Age` remains the monotonic residency clock
across revalidation and tier movement. `Age` is now protected from generic response-header
rules, and regression coverage explicitly checks that both the wire HIT header and `/cache`
object age advance from the stored clock.
across revalidation and tier movement. Existing 0.26.6 custom `Age` rules remain accepted
for configuration compatibility. Response-side Age rules are isolated to the final wire
stage, so set/append/remove cannot feed back into stored freshness, TTL calculation or
validator revalidation. Regression coverage explicitly checks that both the wire HIT header
and `/cache` object age advance from the stored clock before any optional presentation
override.
Compression still refuses Authorization, private/no-store/no-transform, Set-Cookie, ranges,
SSE, upgrades and already encoded responses. Cookie-bearing requests now have one bounded
+1 -1
View File
@@ -270,7 +270,7 @@
"cacheDefaultTtl": "Fallback TTL for public responses (0 = off)",
"bypassQuery": "Bypass URLs with a query string",
"cachePrefixes": "Bypass path prefixes, one per line",
"cacheAgeHelp": "Age is always sent on cache hits. Counters remain available even when diagnostic headers are hidden. Only cache-affecting configuration changes invalidate incompatible objects; managed software updates hand hot objects to cold storage when the cold tier is enabled.",
"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. Counters remain available even when diagnostic headers are hidden. Only cache-affecting configuration changes invalidate incompatible objects; managed software updates hand hot objects to cold storage when the cold tier is enabled.",
"saveCache": "Apply cache policy",
"purgeCache": "Purge all cache",
"customHeaders": "Global custom headers",
+1 -1
View File
@@ -270,7 +270,7 @@
"cacheDefaultTtl": "Zapasowy TTL odpowiedzi publicznych (0 = wył.)",
"bypassQuery": "Pomijaj adresy z parametrami query",
"cachePrefixes": "Pomijane prefiksy ścieżek, po jednym w wierszu",
"cacheAgeHelp": "Trafienia zawsze zawierają Age. Liczniki działają również przy ukrytych nagłówkach diagnostycznych. Tylko zmiany wpływające na cache unieważniają niezgodne obiekty; podczas zarządzanej aktualizacji hot cache jest przenoszony do cold storage, jeśli cold tier jest włączony.",
"cacheAgeHelp": "Trafienia zawierają Age, chyba że jawna reguła nagłówka odpowiedzi go usunie lub zastąpi. Takie reguły Age działają tylko na odpowiedzi wysyłanej do klienta i nie zmieniają świeżości cache. Liczniki działają również przy ukrytych nagłówkach diagnostycznych. Tylko zmiany wpływające na cache unieważniają niezgodne obiekty; podczas zarządzanej aktualizacji hot cache jest przenoszony do cold storage, jeśli cold tier jest włączony.",
"saveCache": "Zastosuj ustawienia cache",
"purgeCache": "Wyczyść cały cache",
"customHeaders": "Globalne reguły nagłówków",
+11 -2
View File
@@ -801,8 +801,17 @@ for marker in [
assert marker in compression_finalize_source, marker
headers_source_0267 = rust_source(root, "src/headers.rs")
vhost_core_0267 = (root / "src/admin/web/vhosts/core.js").read_text()
assert '| "age"' in headers_source_0267, "Age must be protected from generic response rules"
assert '["host", "age", "connection"' in vhost_core_0267, "UI must protect Age consistently"
for marker in [
'pub fn apply_wire_age',
'wire_only: bool',
'wire_only: direction == Direction::Response',
'rule.name.eq_ignore_ascii_case("age")',
'if !rule.wire_only',
]:
assert marker in headers_source_0267, marker
assert '["host", "connection", "content-length"' in vhost_core_0267, "UI protected header set must stay intact"
assert '["host", "age", "connection"' not in vhost_core_0267, "0.26.6 Age rules must remain UI-loadable"
assert 'apply_response_age_rules(' in compression_finalize_source, "Age wire rules must run in final response stage"
compression_tester_0267 = (root / "scripts/test_compression.py").read_text()
for marker in ["--revalidate", "if revalidate:", 'headers["Cache-Control"] = "no-cache"', "headers.get('Age', '-')"]:
assert marker in compression_tester_0267, marker
+3 -3
View File
@@ -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"
+1 -1
View File
@@ -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;
+27 -6
View File
@@ -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,12 +107,13 @@ 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()
}
pub fn apply(headers: &mut HeaderMap, rules: &[CompiledRule]) {
for rule in rules {
fn apply_rule(headers: &mut HeaderMap, rule: &CompiledRule) {
match (&rule.op, &rule.value) {
(Operation::Remove, _) => {
headers.remove(&rule.name);
@@ -120,6 +126,21 @@ pub fn apply(headers: &mut HeaderMap, rules: &[CompiledRule]) {
}
_ => unreachable!("only compiled header rules reach the request path"),
}
}
pub fn apply(headers: &mut HeaderMap, rules: &[CompiledRule]) {
for rule in rules {
// 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);
}
}
}
#[cfg(test)]
+58
View File
@@ -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);
}
}
+14
View File
@@ -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,
+14
View File
@@ -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>()