This commit is contained in:
Mateusz Gruszczyński committed 2026-10-13 01:34:42 +02:00
1 parent 101df02a5f
commit bb025dcf44
25 files changed
+536 -80

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.32.1.
compatibility. Current source version: 0.32.2.
## Delivery
+7
View File
@@ -3,6 +3,13 @@
> Operator-facing release history. Historical engineering notes are kept in
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.32.2
- Fix Web Server shared-cache revalidation: browser `no-cache`, `max-age=0` and freshness-constrained requests now supply stored origin validators. Honor local origin `304 Not Modified` by refreshing the same cached body and preserving/incrementing `X-Cache-Hits`, matching the existing Reverse Proxy behavior.
- Keep client-facing conditional/HEAD/range semantics distinct from origin validation; do not re-use a stale body if the file changed. Remove old stored copies on changed/uncacheable local responses, and prevent an uncacheable `304` from re-enabling shared storage via vhost response rules.
- Preserve original representation headers when revalidating transformed assets, and guard validation across cache PURGE/config revision changes. Add focused regression tests using the local Web Server's actual static file handler.
- No changes to the database schema, cache format, API, Web Server configuration or Reverse Proxy source. **Rust compilation and real-browser verification are outstanding** until run on a Rust 1.99+ host.
## 0.32.1
- Restore Overview proportions from 0.31.9 by removing 0.32.0's desktop minimum heights. Increase compact metric typography so labels, values and captions remain readable at 90% UI scaling without large empty bottoms.
Generated
+1 -1
View File
@@ -1881,7 +1881,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.32.1"
version = "0.32.2"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rust-proxy"
version = "0.32.1"
version = "0.32.2"
edition = "2021"
rust-version = "1.99"
description = "RustProxy - independent reverse proxy with local administration"
+8 -2
View File
@@ -1,4 +1,4 @@
# RustProxy 0.32.1
# RustProxy 0.32.2
**0.31.0 panel/cache additions:** browser-negotiated Zstd/Brotli/gzip for HTML, JS and CSS (Settings → Administration); optional HTTP BAN/PURGE restricted to explicit IP/CIDR allowlist (Settings → Cache, disabled by default); System information Refresh button beside the API badge. See [Settings](docs/SETTINGS.md) and [Cache](docs/CACHE.md).
@@ -7,7 +7,13 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
**0.31.9 Certificate API management: inspect and edit credential settings without exposing/rotating the current token, or regenerate one existing token with one-time display. Rust/Cargo and live testing required.**
## What changed in 0.32.1
## What changed in 0.32.2
- **Web Server cache revalidation:** browser refresh (`Cache-Control: no-cache` or `max-age=0`), stale objects and freshness-constrained requests now use stored origin `ETag` / `Last-Modified` validators. An unchanged local static resource returns `304` to RustProxy; it reuses the cached body and increases `X-Cache-Hits` rather than resetting it to a new MISS.
- **Correct conditional delivery:** retain the client's original validators for downstream `304` decisions (or serve a body on an unconditional refresh). HEAD, single byte ranges, transformed-response headers and `Vary` isolation are retained. If the local resource changes, a successful response replaces the previous representation; an uncacheable 304 discards the shared copy.
- Reverse Proxy caching, frontend routing, PHP-FPM, configuration/storage schemas and the administration API remain unchanged. Rust/Cargo and browser-based runtime acceptance are required; see [verification report](docs/TEST_REPORT.md).
## Previously in 0.32.1
- **Overview readability at 90% UI scale:** compare the supplied 0.31.9 source with 0.32.0, remove the latter's forced desktop card heights and restore content-sized rows. The six-column desktop layout and visible chart legends remain intact. Increase compact Dashboard headings (10 → 11px), values (22 → 25px), secondary text (8.5 → 10px) and the Payload breakdown (9 → 10px).
- **Consistent tile spacing:** Overview now uses `--ui-gap`, the same density-aware spacing variable as other Dashboard sections, instead of its own fixed 10/8/6px spacing in normal/compact/ultra Dashboard modes. UI scale (including 90%) affects all gaps equally.
+6
View File
@@ -1,5 +1,11 @@
# RustProxy roadmap
## 0.32.2 - Web Server conditional cache revalidation (runtime acceptance pending)
- [x] Reuse local Web Server `ETag` / `Last-Modified` revalidation candidates for forced browser refresh and stale/freshness-constrained requests, keeping hit counts and cache bodies on `304`.
- [x] Preserve client conditional semantics, HEAD, range, response policy and transformed representations; invalidate superseded objects.
- [x] Add Rust regression cases and source-only checks.
## 0.32.1 - Dashboard Overview typography and tile height (runtime acceptance pending)
- Restore content-driven heights for desktop Overview tiles (six columns) seen in 0.31.9, removing 0.32.0's fixed-height whitespace at 90% scale.
+45 -42
View File
@@ -1,18 +1,18 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
5090b92a2b7a045f90da92c369c16ac5a9704d466f38650f9215124d5e8cf785 AGENTS.md
a17c615a6a46227bc66118f6e557c18c40fc1db0fcbdfe3c23fe339480b66357 CHANGELOG.md
ef41be4589ed16a84279f06232d3a7604beae512db47bd7c0d1be771c4446582 Cargo.lock
5c50094ca7773fbe586db61007783d2c600c5481a03f446e99b862dd724a9dac Cargo.toml
c0f4ff54579d47615a160930b327122ff88cc41e7fa09079c74f484d79a47ed4 AGENTS.md
d11f383dd8b2e00aab378e5d220da42681a7e663ae03351ec8ec70028456b40d CHANGELOG.md
55d6e709442850feab4c00791e511fbffbf69a5687eb3ae6880c619892ef699d Cargo.lock
9458cc57f7787ab61b3bc685761f22e40dde12d313ca3cc7603ae24f39b5e8a7 Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
bf10b4944a4cb45449eb3a9ec6a4cd91a9ca44c47a47e015320c2e03b6a67e59 README.md
7a0aacd9db1ea4fa42a59614498fea140f17d2375d5cac4db8819b3eb43786e4 ROADMAP.md
e40a3dc858b85ccf3895b960d0881db1b6576ff41f7afda9696c9cbaaacd591f README.md
025cfe7ef2551d6232108249aadccd4d587eccc66ecc344f7de2f8763000b074 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
3b7faf0c7f26387c908ed1f9d11bfd00e1fc042680826e73b01d7e01f2af4043 dev.sh
a895f2d1aaa2903ae8960a51145951a6f801da902dbea995375d836f5f610867 dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -30,7 +30,7 @@ ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2
ed9edea5a28c34aa8bacadf1f6d39257ae1421b1fe907b0cc038fb0de898c67f docs/API.md
f27053f8a93d7883025a04922d015e0b697cad653e714be632b757e167297de1 docs/API_ACCESS.md
6595694c6fb6c52e26c4a15822b5ca8564d4e0de7f5a7fea5c24ab67c24cb49a docs/ARCHITECTURE.md
e9e2327684a4ae2d1ca4a0b3fb67d0ff83b5a150d38d4de4cfad2c5d3102eab6 docs/CACHE.md
fb99fac0a490af262a0ecdad593ffe5164004c2166fe7ddd607476bcc56a1623 docs/CACHE.md
9113f3abbfe81bafc4855d8680df8aa2a381ecc358cc3e1a0f78b6b756421117 docs/CACHE_DIAGNOSTICS.md
3822d72e12f82c37a9e70ab40c7e89e89145ad37f29914e4702db4bf024a5501 docs/CHANGELOG_TECHNICAL_ARCHIVE.md
c8e69e492179ce39958cc63615cf46c4ac08cfdb7f3483aaab66868a3abcd687 docs/CONTAINER.md
@@ -63,7 +63,7 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
919aed5a97f8b08aac1a0784672170b49b420012cd8f2159dd3e46e654787dbd docs/SETTINGS.md
8fc112cb45a2f35ef5b0dd4371aa6f43aca4df85db62c6ae02e56d74a3b702ae docs/STREAMS.md
22568f54908dfda2cf830351e9f3394d81564b13473dea8baa9bb7e36646dcdb docs/TEST_REPORT.md
c81b3f1f86b33abb744d27dee8abffd7fc2a3149210522fee9ccf591f88cdf7e docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
660687fc961f71aae2bdf71595aa47a336fa782f4e78ceee8c1d1e63e46393ee docs/TRANSPORT.md
@@ -154,7 +154,7 @@ c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file
c720a7cad6a15c57b1f2ae7b3e5fbf30a97c64afb9137915f37b23f4e576bdc5 src/admin/file_logs_query.rs
7de7da489a367658b700f00776fa43604d3554500b5fbef4da2764e402e68afa src/admin/logging_api.rs
33be50109497d0adb23c36a10894acb5508dd38f3c01136bd0438a4ce0ffe74b src/admin/mod.rs
da852198f31d08e3e5f146683d149aef94d48343014a7074308c57aabeb1e3c4 src/admin/openapi.json
bcddb40675acbabd38ce5b1c73d8784052796c91030c9653dce7cc1913c5ce78 src/admin/openapi.json
d9120f7dc61d27621c5ac27ea49815dc7945c44d5b99d208da503818c198f649 src/admin/operations.rs
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
@@ -173,13 +173,13 @@ cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
d6c5448db12de34a7bb4924411b44d09c7144bc3e659bd3c8a7c0ebf1938da1c src/admin/web/app/config-history.js
cb9def393b7f57a6cfecbb8edaff4b7e20e7805231d08e354ad0b7deb294a103 src/admin/web/app/core.js
919019e3e71ae7dc2a7c9ba411dad2908ead42942c0702e7349b5ebe12908442 src/admin/web/app/preferences.js
729ed152eb8471c67fb0e55e44fe0669c2708802ce83228df9cb4e53c14cee02 src/admin/web/app/refresh.js
984d092961bfaaa19f982e54c7d4be474e8417b208d814ac88bda63f1ab19f4c src/admin/web/app/runtime.js
db56370db60e514a2241d231cd949bac2c7aa2f413a2c14035a877029a8d864f src/admin/web/app/session-resume.js
40ca254a6c7f4c2697e8ccf43d31fc24cc925be05b6070b974aaf044a928ee3e src/admin/web/app/preferences.js
6a544baaf40763b4b612e53845b3bc33396b9bb6518565fd6b7c128c278b8d5d src/admin/web/app/refresh.js
a3715a35e11b45395897e91003f90d2a4674f84cc93ebb90044cdf32eaddde1f src/admin/web/app/runtime.js
31201586925e6f4792071d7d03fb05dcc4a69a03c02a4e85eb0746f45af68074 src/admin/web/app/session-resume.js
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
fc27b21bb6692efccd51351a7b9510f699bad69e51f4ecc4b7bdbf75ad35414b src/admin/web/app/stream-dashboard.js
4fec1344be3cd540d1e6d0b87fb0aa94e1bef2968c4f86366a62c56d4085f452 src/admin/web/cache-panel.js
cc566f8293cc935f5080ebfb8076293c256bb6a87e1e6a69be03512448871ec9 src/admin/web/app/stream-dashboard.js
a854e08f947a4d13fc4b27286a096dcb2773dc0358a28b27703a2320ba7e8bf7 src/admin/web/cache-panel.js
02a9f5a61379e68e1723e64ef4c589500e9a993adcb02fe352f1cf9456c90c52 src/admin/web/certificate-api.js
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
d01a2e48f344fe074c1e63ee0a9bbf7151dfe26140690418535197e13a2a16d7 src/admin/web/docker.js
@@ -200,28 +200,28 @@ ff72fc4c6a11907a16234c4b3c7d7f7bcf82eb72868dc3fbf654b04aff806fc8 src/admin/web/
386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
a065af0a331a953dc51c72f0f589023cf8a917cda5baa5308dd0d8c105ec3d91 src/admin/web/file-logs.js
7cbdef9192a9dee1f8eac42b85f52b3190f7f6efae50004aa7ad116a910e4036 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
0ee5b6ac3cf2022c5b88166dc4816868161351471e81b424b53d0f174eca7ed1 src/admin/web/index.html
03ad9572bb14c34059636d612cf6b876d6361ce601817cff674c57b715dc65dd src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
99fe9b8ba7a8a24e5bbe9ae40999484fe13b0faf5eb1d7a9193701f667e51ca5 src/admin/web/login.js
136c093a907825e0d942c06186fffc834936b60dd6408bcafe81092cff7e3c64 src/admin/web/management.js
203f2b7e960d4cf6b3fc0970e73716e7da998e29c46dcb4a72f7f310aa8d2232 src/admin/web/login.js
36af1f8f83af7e6f9a60b4d9524714e02c077e539a90aac0ce990d0587c0c3ed src/admin/web/management.js
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
2efa65f4112ea5523bbb5583c6e320c93f0cfed520a0bc5cf4c22fb51b123649 src/admin/web/settings.js
ba245b331ab045b25c9fe62670d108b5e7cfa1d1cbbcce5dc22ce37521fa92f4 src/admin/web/settings.js
8eadbada619b15087783dcfa0e889a81f88e78034d084c5ab616f7912b835940 src/admin/web/status_200.html
6e0036e3915de17529f14a7e47fa57d52ed3e03e65a3e4c0aa78cd6486342e96 src/admin/web/streams.js
39f5e489813ae397ab03bbfca0d3daca85244da93ec4e895a3fc75d40234b1e3 src/admin/web/style.css
e3075af59644e42fa5735574d8e9caa1bcebfe8de184b78aff9f4026b91915f1 src/admin/web/streams.js
500e3d77e981ad2fe5f09539b0ed47f0d1f85b3a593dfa2c0d6e193c3955ffe9 src/admin/web/style.css
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
a6e7bbff4fd2d50ecf759ed28161a672d7b483af2b35c9f912746a7a06a9f476 src/admin/web/transform-panel.js
00f59a6f656e44c364ed3c8eed33ce1df878a488908b30a9523a87b64f50f9ef src/admin/web/vhosts/core.js
f3aded010313a31e3f8599891be2f9bccfcf3d1e7a1649ccf81df95fbee3232f src/admin/web/vhosts/editor.js
30a6344e76de77b60cd7247a33c68f1cb5d972aebfde56469f2ebd44440c7bf9 src/admin/web/vhosts/health.js
137aeabed99cbdf9f122da6350e596e6a4e32244fa23101ccf6378305148d38d src/admin/web/vhosts/rates.js
f2689283cfe43d6aae911b0df778a69335089be230fbfc6de5dcce1801c38427 src/admin/web/vhosts/render.js
91e3417fba7e8f59023cd64813befccc8341741cd4aa53dfa1ec68a03c7f4632 src/admin/web/web-server-profiles.js
de3326d4fae254eb0b8c190f02e762f4094dcba86bfdcb24c5fb472676ebcc4b src/admin/web/web-server.js
22f29bb06cb650b6adefe257f5f706656e36e7fdd5088f7abc10f63247e04165 src/admin/web/vhosts/core.js
b31782c61da3623389b57fdbba5bd41d2287d45fc172e9c14360a9dd7accf061 src/admin/web/vhosts/editor.js
8338ce13b5e3e3f421437631124cb691aeb5b9e17409d256b4b9bcac457b3c59 src/admin/web/vhosts/health.js
2170cfddf5c611672de34b0525d36ade8fcbe459cc21269281d6b615d8601b93 src/admin/web/vhosts/rates.js
dfab53c9b9d4c633db639c37e99a46169aa86f7a28e6731e305c27ac8587a7ef src/admin/web/vhosts/render.js
4dfdeb7c73c2775eacaca65fd8ef1dc80b275421b15522d319d338f5b96288cc src/admin/web/web-server-profiles.js
355684980d1caf8918d1773ce17934b51e3a85a60f38491217d10035bf68ce9f src/admin/web/web-server.js
53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs
b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs
775178ee5781e64574bdd7c1fb7d64e7e283977fbd9c98c99896532a7f1df612 src/auth/mod.rs
@@ -359,17 +359,19 @@ d3b9aa0c0055fa9a29f180ca8b10c890c0b91127ee603e5d89cce4c99d5a6bd4 src/proxy/forw
cea9e47ecb31b0e4c0bbe0eb83e0e53b5e7bdcb9930107b67a8c34eef01a3f49 src/proxy/forward/success_finish.rs
902cbf8dc3c1ec8d25411bc7fe0400d301f3a7cc885afada594e4eaee3bedd08 src/proxy/forward/success_prepare.rs
23b6bd26f6175b52c13d2571318fddd12550ba34ee77b87b5ac0e4811f8e2bee src/proxy/forward/upstream.rs
8961d8d649d8f4708f443f1b8b8a73c8b5f42811d2cad8cc6f596833511933f4 src/proxy/forward/web_source.rs
b05c4f5c009c5e410f472ceaa999fa3a9c7fb60364d5b62de219f0924c9d524e src/proxy/forward/web_source.rs
a9c011061585c067f04f7885215d53e6c40474bdced15d2be5a5ba65190910a0 src/proxy/forward_entry.rs
c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs
5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs
632ee37457d9bf1dbcd3211afdf489f4fc9faf018007d5c1036169436e8e5dea src/proxy/mod.rs
8d17b97d90fd6a749ba8d37a8d28c94e6d7302b6c86905f073e2dc595506961a src/proxy/mod.rs
55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs
cc682f0d5754e0b806a4eb5950e4cc00611aca5ce4668414fddef1d692489805 src/proxy/tests/authority_tests.rs
0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs
e80c7fe44abb7607c64ea02d56542753dc25290b3bcf4c4d45b75f2db6ca90d0 src/proxy/types.rs
9999f717492755d62fc05cd5c1aff802ff0da901fe3e6b928bd8a000833c8799 src/proxy/upstream_helpers.rs
2d9142e3ae95895b5271b4009ab48236a3a2074ce51179c021193b35777560e4 src/proxy/web_cache_revalidation.rs
5af991311e4ed22cd8aaff15a4b9d45bb55fcdfcd0ebbfb4879dc1c7edf88ac4 src/proxy/web_cache_revalidation/tests.rs
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
d866e7e72dee6ff54c2418c34e9899cbcd7e4e027d9cbe2b625044dc9062e4ee src/proxy/websocket/handshake.rs
7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs
@@ -482,19 +484,19 @@ b04362ad082a32c9ccc4cf38c46f44d71347adecf7aba3224b3af3ea4872e3d0 tools/README.m
e5c58fc9b7685ec911bfa86f6ca4f0cc97c19a90c0cff5b60ea9887f1afe9ced tools/checks/admin_public_origin_0314_config_sim.mjs
0060f8217640403c6af2d5091fdc9087d0fb2fc30cd545a3c58277a396d8121c tools/checks/admin_public_origin_0314_sim.mjs
fd082812411cff7afb71b2289a6f1a469b8d3c8a7cfdcdd9e567a2a4baf81be4 tools/checks/admin_surface_audit_0310.py
412255d383fcc8306c6086784fb416d9814e3ec790e440a0927431ca615f3411 tools/checks/backend_cache_0297_ui_test.mjs
2a493dbce2991a23d807c01952f58608a364f541560836b4cb385e75e108e9e7 tools/checks/backend_health_0300_ui_test.mjs
b25381435bddb9d91ddf057bf5447877080b3bfb6444e8222bffc9034e3c5684 tools/checks/certificate_api_0319.py
a7dfb58212d2172f3fbe0ccdcd23e797a7baadc5627d14615c94427e8ad24b21 tools/checks/backend_cache_0297_ui_test.mjs
8946c2f93915322062fca5a1a406ed5d88efa9a8b1cefc7e2d1c7cafb2ba8e57 tools/checks/backend_health_0300_ui_test.mjs
1255853a2a53db73a2820d9aadad5f30c8e404de95287410fa4f2443f74b2eed tools/checks/certificate_api_0319.py
526d16db9de7be414a64483a1e9276d4a4402e3c7efd2479f79d01cc5b388f46 tools/checks/certificate_api_0319_ui_test.mjs
49829119968828bfbb276ff408716ba6f2706a8463878af3d92ec963e7071b6b tools/checks/check_source.py
4907ec98d4fd64f8670f0ef5783553cfbed86a2882348f07089c6c43ff176b0c tools/checks/check_source.py
dc4df51fc15d4b6345c6625890ddbe014103851ebfa16abc583fbb0d809f4015 tools/checks/dashboard_boot_0311_ui_test.mjs
259d4b0985c82b631e151a07e8ef3b4c760019cb99bb2af458ce577497f7df58 tools/checks/dashboard_startup_0310_ui_test.mjs
a6f8b7ee2cf0cc212032c2b76588e360143e9b2e20ff87caa8f29675595cd885 tools/checks/dependency_upgrade_0312.py
45981344fb95c2b64e24fa8b20af6ad53c0e101c5511f2182456bc0069e7c2d5 tools/checks/dependency_upgrade_0312.py
38e35bed4faf984932b39bb5a00819fadc664387e8050a803ff4b5348b31369f tools/checks/docker_startup_0318_ui_test.mjs
2f4fecdad966d9898f7a9168c3f6e216ffb3ea165d06b62fe5865f2c3314ed83 tools/checks/docs_check.py
86d0d1674a713f1d10d93be5b920fd3de2ebc4b69ac1259907d96d10233ae176 tools/checks/docs_check.py
6ca66c0998d1da852ad66976c91e4d7d3628195aa49ddb44cea95f531eb50033 tools/checks/file_logs_tabs_0315_ui_test.mjs
72f3c4e3274d33e02d46646395bc0cd18a8f723f8747969fc81997c7a7259e76 tools/checks/h3_cache_mobile_0315_test.mjs
94396cf2fc2a2a683e1198c15d887ed878730ee36101ba980a8b11c5b99db830 tools/checks/h3_navigation_0316.py
e9cdaf709f16760b01389c00cc05974459573fd3ae89e30c21e826781408c191 tools/checks/h3_navigation_0316.py
e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
a8bda904a92f9436761db1460f14da3c0c9b3db9edeeee581497700242784d64 tools/checks/memory_contracts.py
@@ -505,14 +507,14 @@ eea7cdc9c7d8dd0f5fb079cfff657a6feadfc0b856f63265bfbd8257f1beca19 tools/checks/p
021a97bd6763b59f3f7eca15f51efdf88d0c863f1f81a2e42ba80a9a09ca3fec tools/checks/refresh_0310_ui_test.mjs
c11583dad0899f8b945cae6e2f6cb0bdb55f5526bd5f42c248ac41badec83679 tools/checks/release_0317.py
b84b66a6f8f63cd0a354ef607b96ff60c77ef37f80f49ea9a9036b33147c0b6a tools/checks/release_0317_ui_test.mjs
4b4e9e84b21d78dcee9cb8ef4bc40155ebdb3dceb7e064715a93e89345a098ce tools/checks/release_0318.py
43db52365768cf1c40474544c8201ee0993e5cba1b3dd112cca82811cc0e790f tools/checks/release_0318.py
e73afc62214e9092c180730f624a2039ac3fc850dcd865b2b117c11f7caaf88b tools/checks/release_0318_ui_test.mjs
d7483d0456ccd9d8b328587b733598020a0a6fa4496be0527c603c90fbd380b4 tools/checks/release_smoke.py
aaf65a5a9ce7494728035ecd7c654778cacd878a9b095be688b9c1491986d654 tools/checks/release_smoke.py
31b31e976962e2c53e0f1f71bf67f7d2686d30cb26f638defa1bd7636cf1c1e8 tools/checks/rust_module_contracts.py
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py
adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py
23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py
1c747fd65f48a0ec7650c7b8144ea97d9de8d78014d5ddaef82792d18a026f2b tools/checks/security_logs_0315_ui_test.mjs
4823b7e30d70a9bf7f856907cc9ee74f274d326322df86c367a0909e1da3b922 tools/checks/security_logs_0315_ui_test.mjs
70e5632f36e2387be8638c33f371aacca57bf264ef67846a6e0bd7f2dd19a5e0 tools/checks/source_module_contracts.py
37eebe90c0a60f7d7f4fbdd09986171b7dbd50c4d3760d94c31595dd93a2e680 tools/checks/source_pruning_0317.py
e9f96a2a44712feb090ddb0eb273124ac780cb68e428292f7c7f106de11be050 tools/checks/stream_0320_ui_test.mjs
@@ -523,7 +525,8 @@ a5105ab53de67e6b1114974c92b019014e1aa93acc445bc5dd6c82af0b0d3a16 tools/checks/v
161637cb5e2e613e3c71cf8489cc936fa84913a2a02649463f8893a28a674242 tools/checks/vhost_origin_errors_0297_ui_test.mjs
ea98e8ce2bdc841814e486ea4c13f94e284b2c50a5887598b7a288f75a14deaf tools/checks/vhost_www_ui_test.mjs
7aeef339092ab07aa0d906fce223ff34ecc1ee8dbd6b3f173d94c521a79558ac tools/checks/web_assets.py
618f9c6b20c043d151eac319d1712ec50358ae86b2e7fa23418c8e4bbcc45963 tools/checks/web_server_contracts.py
8d2e1837f94302d09a704aca452cc367e6892f289aca3dc2d82c086a2e7de26d tools/checks/web_cache_revalidation_0322.py
8ec055a16198701dd4fa35eac42a71b666d2b9bbe90edb1c797c7ed759615487 tools/checks/web_server_contracts.py
35f76060e585a96c623ef83bd5f771969f5be16a0e1ae626a3092d7458b84e84 tools/checks/web_service_ui_test.mjs
4c541640a7edcdb69319770d6359e7715617df8f5443f82b29f867a5e820a08e tools/checks/www_redirect_ui_test.mjs
7c30c280d0b0a8bf855eeffab583ca34dc161255f7ef6124148aa979c17c26d3 tools/dev/memory_load.py
+1
View File
@@ -103,6 +103,7 @@ if [[ "$mode" == "test" ]]; then
python3 tools/checks/memory_contracts.py
python3 tools/checks/high_traffic_memory_model.py
python3 tools/checks/web_server_contracts.py
python3 tools/checks/web_cache_revalidation_0322.py
python3 tools/checks/security_contracts.py
python3 tools/checks/admin_surface_audit_0310.py
python3 tools/checks/security_dependency_contracts.py
+13 -5
View File
@@ -30,11 +30,19 @@ Vhost response rules run before cache fill, and transformed output is what is
stored. Private/`Set-Cookie`/`no-store` origin responses remain ineligible,
even if a later response-header rule replaces their visible cache headers.
For Web Server sources, stale entries are refreshed by a new local-source
request rather than the remote-backend revalidation worker. As with reverse
proxy origins, range requests and conditional requests can reuse fresh stored
representations. Do not rely on stale-if-error remote-backend refresh behavior
for local Web Server routes.
For Web Server sources (0.32.2), requests with `Cache-Control: no-cache`,
`max-age=0`, or an expired/inadequately fresh representation check the local
origin using the stored origin `ETag` / `Last-Modified`. When the Web Server
returns `304 Not Modified`, RustProxy refreshes the same shared-cache object,
keeps its body and increments `X-Cache-Hits`. The original browser conditional
headers determine whether the client receives 304 or the cached 200 body.
Changed static files cause a new response/cache fill; `no-store`/`private`
origin policy continues to prevent shared storage. HEAD and single-range
requests can reuse a successfully revalidated full object. A Web Server
source does not run the remote-backend background revalidation worker or its
stale-if-error fallback. A PHP handler that ignores validators may still
return 200, which must be handled as a real origin response rather than an
assumed unchanged object.
## HTTP BAN / PURGE from authorized client IPs
+45
View File
@@ -0,0 +1,45 @@
# Verification report - RustProxy 0.32.2
## Scope
Source-based patch on the supplied 0.32.1 archive. The Web Server's cache
revalidation now mirrors the existing Reverse Proxy's 304 handling for local
files, while preserving original browser conditionals, cache policy and cached
transformed representations. No backend, PHP-FPM, API or SQLite schema change.
## Regression cases added (Rust, execution pending)
- F5-style `Cache-Control: max-age=0` and hard-refresh `no-cache` preserve body
and advance `X-Cache-Hits` when local origin returns 304.
- Browser `If-None-Match` produces downstream 304 only when the client's validator
matches the public stored representation, not merely the origin's validator.
- Unrelated client validators cannot incorrectly validate the stored body.
- `HEAD` and single-range conditional refresh reuse the validated full body.
- A changed asset (different on-disk ETag) returns 200 rather than a false 304.
- Newly private origin revalidation metadata cannot remain in shared storage.
## Results and limitations
- Python static/structural checks via `./dev.sh test`: **PASS** (including
version, full source preflight, cache regression contracts, HTTP/cache,
security, documentation, schema-v9/upgrade and release smoke contracts).
- All **24 Node.js UI simulations** configured in `./dev.sh test`: **PASS**.
Three existing fixture assumptions were aligned with current unindented
functions/version (`backend_cache_0297_ui_test.mjs`,
`backend_health_0300_ui_test.mjs`, `security_logs_0315_ui_test.mjs`);
the production panel logic is unchanged.
- Python load-tool selftest against a Python peer: **PASS**; not a RustProxy
server test or a memory profile of the proxy.
- `./dev.sh check`: **unavailable (exit 127)**; Rust/Cargo is not installed.
- `./dev.sh test`: **pre-Cargo checks PASS; entire command exits 127** because
Rust/Cargo is not installed, so no Rust tests executed.
- Rust `cargo check --locked --all-targets`: **not run, Cargo unavailable**.
- Rust `cargo test --locked --all-targets`: **not run, Cargo unavailable**.
- Real SQLite schema migration/update on an installed instance: **not run**;
only source/upgrade contracts were checked.
- Actual RustProxy process, HTTP/1.1, HTTP/2, HTTP/3, PHP-FPM/WordPress and
browser DevTools reproduction: **not run** in this source-only environment.
**Production acceptance remains open.** Run `./dev.sh check`, `./dev.sh test`
and repeat browser-refresh/GET comparisons on a Rust 1.99+ build host before
rolling the release out. The patched source is delivered, not a tested binary.
+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.32.1",
"version": "0.32.2",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
},
"servers": [
+52 -2
View File
@@ -68,7 +68,15 @@
let range = crate::cache::request_range(&request_method, &request_headers);
let if_range = request_headers.get("if-range").cloned();
let range_requested = !matches!(range, crate::cache::RangeSelection::None);
let needs_validation = policy.revalidation_reason().is_some();
// Unlike the original Web Server path, retain stale and client-forced
// revalidation candidates. A local 304 can refresh them without replacing
// the cached body or resetting X-Cache-Hits (same as Reverse Proxy).
let cached_revalidation = if let Some(ticket) = cache_ticket.as_ref() {
state.cache.revalidation_tiered(ticket).await
} else { None };
let needs_validation = policy.revalidation_reason().is_some()
|| cached_revalidation.as_ref().is_some_and(|cached|
!cached.reusable_for_request(&request_headers));
if let Some(ticket) = &cache_ticket {
if !needs_validation {
if let Some(mut hit) = state.cache.get_tiered(ticket, cache_settings).await {
@@ -114,9 +122,12 @@
// Neither the Request nor the streaming Body is held across a borrowed
// await: the filesystem/PHP source owns the body for its full lifetime.
let static_body = std::mem::replace(request.body_mut(), Body::empty());
let (origin_request_headers, revalidation_sent) =
web_cache_revalidation::web_origin_request_headers(
&request_headers, cached_revalidation.as_ref());
let started = Instant::now();
let response = crate::web_server::serve(service, request_method.clone(),
request_headers.clone(), request_uri, &service_path, static_body,
origin_request_headers, request_uri, &service_path, static_body,
crate::web_server::RequestContext {
client_ip: context.client_ip,
client_port: Some(peer.0.port()),
@@ -131,6 +142,45 @@
).await { return fallback; }
let mut response = intercept_route_origin_error(response, route, &snapshot);
if revalidation_sent && response.status() == StatusCode::NOT_MODIFIED {
if let (Some(ticket), Some(cached)) =
(cache_ticket.as_ref(), cached_revalidation.as_ref())
{
let revalidated = web_cache_revalidation::finish_web_revalidation(
&state.cache, ticket, cached, response.headers(), cache_settings,
started.elapsed(), &request_headers, &request_method,
ignore_cookie, force_cache,
|headers| apply_response_rules(headers, &snapshot,
route.frontend.as_deref(), Some(route), false),
);
if let Some(mut hit) = revalidated {
state.cache.record(scope, &host, "HIT", hit.reason);
if hit.transformed { state.transform_metrics.record_cache_hit(scope); }
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
exchange.cache_status.store(1, Ordering::Relaxed);
*exchange.cache_reason.lock().unwrap_or_else(|e| e.into_inner()) = hit.reason;
}
hit.response.extensions_mut().insert(ResponseRulesApplied);
hit.response.extensions_mut().insert(StaticSourceResponse);
return intercept_route_origin_error(hit.response, route, &snapshot);
}
// A simultaneous PURGE/config change invalidated the ticket. Do
// not accidentally return an origin 304 without a reusable body.
return error(StatusCode::SERVICE_UNAVAILABLE,
"RustProxy: Web Server cache entry invalidated during revalidation\n");
}
}
if cached_revalidation.is_some() && response.status() != StatusCode::NOT_MODIFIED
&& response.status().as_u16() < 500
{
// A changed response (including a 206, 404 or an uncacheable 200)
// supersedes the old object. Also discard when the old entry has no
// validators and therefore must be fetched unconditionally. A complete
// cacheable GET below can refill the entry.
if let Some(ticket) = cache_ticket.as_ref() {
state.cache.discard(ticket);
}
}
let origin_headers = response.headers().clone();
let original_plan = crate::cache::plan(response.status(), &origin_headers,
cache_settings, started.elapsed());
+1
View File
@@ -1,6 +1,7 @@
include!("types.rs");
include!("error_fallback.rs");
include!("cache_helpers.rs");
mod web_cache_revalidation;
include!("cache_http_invalidation.rs");
include!("upstream_helpers.rs");
include!("redirect_helpers.rs");
+129
View File
@@ -0,0 +1,129 @@
//! Conditional validation for a local Web Server origin. The reverse-proxy
//! source has an equivalent 304 path; both must preserve the stored body and
//! its hit counter when the origin reports that the representation is unchanged.
use crate::cache::{Cache, RangeSelection, Revalidation, Settings, Ticket};
use axum::{body::Body, response::Response};
use http::{HeaderMap, HeaderValue, Method, StatusCode};
use std::time::Duration;
pub(super) struct WebRevalidated {
pub response: Response,
pub transformed: bool,
pub reason: &'static str,
}
/// The client's validators describe the *public* representation (possibly
/// transformed). Only validators belonging to our stored origin representation
/// may be sent to the Web Server. Never allow an unrelated client ETag to turn
/// into a 304 that would incorrectly validate our cached body.
pub(super) fn web_origin_request_headers(
client: &HeaderMap,
cached: Option<&Revalidation>,
) -> (HeaderMap, bool) {
let mut headers = client.clone();
let Some(cached) = cached else { return (headers, false) };
headers.remove("if-none-match");
headers.remove("if-modified-since");
let sent = cached.apply_validators(&mut headers);
(headers, sent)
}
/// A transformed body uses public representation-specific headers. A local
/// origin's 304 can describe its own encoding/ranges, which must not be copied
/// onto the cached, already transformed body.
fn web_merged_validation_headers(cached: &Revalidation, validation: &HeaderMap) -> HeaderMap {
let mut merged = cached.merge_validation_headers(validation);
if cached.transformed() {
for name in [
"content-encoding", "vary", "accept-ranges", "content-md5", "digest", "content-digest",
] {
merged.remove(name);
for value in cached.headers().get_all(name) {
merged.append(name, value.clone());
}
}
}
merged
}
/// Handle a real 304 from the local origin, *after* matching it to a stored
/// object using our own validator. A successful validation increases HITs and
/// refreshes freshness metadata; an uncacheable 304 is served once and purged.
/// `apply_rules` keeps vhost response rules identical to the ordinary Web
/// Server 200/fill path. Invalidated tickets are never resurrected.
pub(super) fn finish_web_revalidation(
cache: &Cache,
ticket: &Ticket,
cached: &Revalidation,
validation: &HeaderMap,
settings: &Settings,
elapsed: Duration,
client: &HeaderMap,
method: &Method,
ignore_cookie: bool,
force_cache: bool,
apply_rules: impl FnOnce(&mut HeaderMap),
) -> Option<WebRevalidated> {
let origin_merged = web_merged_validation_headers(cached, validation);
// Vhost response rules must never turn a newly private/no-store origin
// into a cacheable shared representation.
let origin_safe = crate::cache::plan(StatusCode::OK, &origin_merged, settings, elapsed).is_some();
let mut merged = origin_merged;
apply_rules(&mut merged);
if force_cache {
if client.contains_key("cookie") {
merged.append("vary", HeaderValue::from_static("Cookie"));
}
if client.contains_key("authorization") {
merged.append("vary", HeaderValue::from_static("Authorization"));
}
}
let can_store = origin_safe && !(ignore_cookie && crate::cache::varies_on(&merged, "cookie"));
let client_not_modified = crate::cache::conditional_not_modified(client, &merged);
let range = crate::cache::request_range(method, client);
let if_range = client.get("if-range");
let published = if can_store {
cache.publish_revalidated(ticket.clone(), cached, merged.clone(), validation, settings, elapsed)
} else { None };
let (mut response, hits, ttl, resident_age, transformed, reason) =
if let Some(mut hit) = published {
if !client_not_modified {
if let RangeSelection::Single(selection) = range {
hit = hit.apply_range(selection, if_range);
}
}
(
hit.response, hit.hits, hit.remaining_ttl, hit.resident_age_seconds,
hit.transformed, "cache_revalidated",
)
} else {
if !cache.ticket_current(ticket) {
return None;
}
cache.discard(ticket);
let response = if !client_not_modified {
if let RangeSelection::Single(selection) = range {
cached.response_with_range(merged, selection, if_range)
} else {
cached.response(merged)
}
} else {
cached.response(merged)
};
(response, 0, 0, 0, cached.transformed(), "cache_revalidated_uncached")
};
if client_not_modified {
response = crate::cache::make_not_modified(response);
}
if *method == Method::HEAD {
*response.body_mut() = Body::empty();
}
settings.annotate_reason(response.headers_mut(), "HIT", hits, ttl, reason);
settings.annotate_transformed(response.headers_mut(), transformed);
settings.annotate_resident_age(response.headers_mut(), resident_age);
Some(WebRevalidated { response, transformed, reason })
}
#[cfg(test)]
mod tests;
+166
View File
@@ -0,0 +1,166 @@
use super::*;
use crate::{cache, web_server};
use http_body_util::BodyExt;
use tempfile::TempDir;
use std::sync::Arc;
fn cache_settings() -> Settings {
Settings {
enabled: true,
expose_status: true,
expose_hits: true,
expose_cache_status: true,
..Default::default()
}
}
async fn fixture() -> (TempDir, web_server::CompiledService, Arc<Cache>, Ticket, Settings) {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join("shCore.js"), b"before").unwrap();
let service = web_server::Service {
id: "wordpress".into(),
name: "WordPress".into(),
document_root: dir.path().to_path_buf(),
cache_control: Some("public, max-age=604800".into()),
..Default::default()
}.compile().unwrap();
let cache = Cache::new(1);
let settings = cache_settings();
let ticket = cache.ticket(
1, "https", "blog.example.test", "wordpress",
"/shCore.js?ver=3.0.9b", &HeaderMap::new(),
).unwrap();
let initial = serve_file(&service, Method::GET, HeaderMap::new()).await;
assert_eq!(initial.status(), StatusCode::OK);
let plan = cache::plan(initial.status(), initial.headers(), &settings, Duration::ZERO)
.expect("static origin response is cacheable");
cache.wrap(initial, ticket.clone(), plan, &settings)
.into_body().collect().await.unwrap();
(dir, service, cache, ticket, settings)
}
async fn serve_file(service: &web_server::CompiledService, method: Method, headers: HeaderMap) -> Response {
web_server::serve(
service, method, headers,
"/shCore.js?ver=3.0.9b".parse().unwrap(), "/shCore.js", Body::empty(),
web_server::RequestContext {
client_ip: "127.0.0.1".parse().unwrap(),
client_port: Some(12345),
server_name: "blog.example.test".into(),
server_port: 443,
tls: true,
version: http::Version::HTTP_2,
},
).await
}
async fn validate(
service: &web_server::CompiledService, cache: &Cache, ticket: &Ticket,
settings: &Settings, method: Method, client: HeaderMap,
) -> Option<WebRevalidated> {
let cached = cache.revalidation_tiered(ticket).await.unwrap();
let (headers, sent) = web_origin_request_headers(&client, Some(&cached));
assert!(sent, "stored validator must be used for local source revalidation");
let origin = serve_file(service, method.clone(), headers).await;
if origin.status() != StatusCode::NOT_MODIFIED {
return None;
}
finish_web_revalidation(cache, ticket, &cached, origin.headers(), settings,
Duration::ZERO, &client, &method, false, false, |_| {})
}
#[tokio::test]
async fn browser_refresh_304_preserves_web_server_hit_counter() {
let (_dir, service, cache, ticket, settings) = fixture().await;
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 1);
for directive in ["max-age=0", "no-cache"] {
let mut client = HeaderMap::new();
client.insert("cache-control", directive.parse().unwrap());
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
.await.unwrap().response;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.headers()["x-cache"], "HIT");
let hits = response.headers()["x-cache-hits"].to_str().unwrap().parse::<u64>().unwrap();
assert!(hits >= 2);
assert_eq!(response.headers()["x-cache-reason"], "cache_revalidated");
assert_eq!(response.into_body().collect().await.unwrap().to_bytes(), "before");
}
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 4);
}
#[tokio::test]
async fn browser_conditional_refresh_returns_304_without_resetting_hits() {
let (_dir, service, cache, ticket, settings) = fixture().await;
let etag = cache.revalidation_tiered(&ticket).await.unwrap().headers()["etag"].clone();
let mut client = HeaderMap::new();
client.insert("cache-control", HeaderValue::from_static("no-cache"));
client.insert("if-none-match", etag);
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
.await.unwrap().response;
assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
assert!(!response.headers().contains_key("content-length"));
assert_eq!(response.headers()["x-cache-hits"], "1");
assert!(response.into_body().collect().await.unwrap().to_bytes().is_empty());
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 2);
}
#[tokio::test]
async fn mismatched_browser_validator_cannot_validate_different_cached_object() {
let (_dir, service, cache, ticket, settings) = fixture().await;
let mut client = HeaderMap::new();
client.insert("cache-control", HeaderValue::from_static("no-cache"));
client.insert("if-none-match", HeaderValue::from_static("\"unrelated\""));
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
.await.unwrap().response;
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(response.into_body().collect().await.unwrap().to_bytes(), "before");
}
#[tokio::test]
async fn range_and_head_requests_use_validated_cached_body() {
let (_dir, service, cache, ticket, settings) = fixture().await;
let mut range = HeaderMap::new();
range.insert("cache-control", HeaderValue::from_static("max-age=0"));
range.insert("range", HeaderValue::from_static("bytes=0-2"));
let partial = validate(&service, &cache, &ticket, &settings, Method::GET, range)
.await.unwrap().response;
assert_eq!(partial.status(), StatusCode::PARTIAL_CONTENT);
assert_eq!(partial.into_body().collect().await.unwrap().to_bytes(), "bef");
let head = validate(&service, &cache, &ticket, &settings, Method::HEAD, HeaderMap::new())
.await.unwrap().response;
assert_eq!(head.status(), StatusCode::OK);
assert!(head.into_body().collect().await.unwrap().to_bytes().is_empty());
}
#[tokio::test]
async fn changed_local_file_is_not_falsely_revalidated() {
let (dir, service, cache, ticket, settings) = fixture().await;
// A size change guarantees different ETag even on filesystems with
// coarse modification-time resolution.
std::fs::write(dir.path().join("shCore.js"), b"new file content").unwrap();
let cached = cache.revalidation_tiered(&ticket).await.unwrap();
let (headers, sent) = web_origin_request_headers(&HeaderMap::new(), Some(&cached));
assert!(sent);
let origin = serve_file(&service, Method::GET, headers).await;
assert_eq!(origin.status(), StatusCode::OK);
let plan = cache::plan(origin.status(), origin.headers(), &settings, Duration::ZERO).unwrap();
cache.discard(&ticket);
cache.wrap(origin, ticket.clone(), plan, &settings)
.into_body().collect().await.unwrap();
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 1);
}
#[tokio::test]
async fn 304_new_private_policy_discards_cached_representation() {
let (_dir, _service, cache, ticket, settings) = fixture().await;
let cached = cache.revalidation_tiered(&ticket).await.unwrap();
let mut headers = HeaderMap::new();
headers.insert("cache-control", HeaderValue::from_static("private, max-age=300"));
headers.insert("etag", cached.headers()["etag"].clone());
let outcome = finish_web_revalidation(&cache, &ticket, &cached, &headers,
&settings, Duration::ZERO, &HeaderMap::new(), &Method::GET,
false, false, |_| {}).unwrap();
assert_eq!(outcome.reason, "cache_revalidated_uncached");
assert_eq!(outcome.response.into_body().collect().await.unwrap().to_bytes(), "before");
assert!(cache.revalidation_tiered(&ticket).await.is_none());
}
+2 -2
View File
@@ -7,7 +7,7 @@ import tomllib
root = Path(__file__).resolve().parents[2]
read = lambda path: (root / path).read_text(encoding='utf-8')
version = tomllib.loads(read('Cargo.toml'))['package']['version']
assert version == '0.32.1', version
assert version == '0.32.2', version
spec = json.loads(read('src/admin/openapi.json'))
assert spec['info']['version'] == version
paths = spec['paths']
@@ -64,5 +64,5 @@ for language in ('en_US', 'pl_pl'):
for key in ('certificateApiView', 'certificateApiEdit', 'certificateApiSave', 'certificateApiRegenerate',
'certificateApiRegenerateConfirm', 'certificateApiTokenHidden', 'certificateApiCreatedAt'):
assert strings.get(key), (language, key)
assert '0.32.1' in read('docs/TEST_REPORT.md')
assert '0.32.2' in read('docs/TEST_REPORT.md')
print('PASS: 0.31.9 per-credential API routes, separated metadata editing/token regeneration, OpenAPI security, UI and locales (source-only).')
+9 -9
View File
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
preflight(root)
manifest = tomllib.loads((root / "Cargo.toml").read_text())
assert manifest["package"]["version"] == "0.32.1"
assert manifest["package"]["version"] == "0.32.2"
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
h3dep = manifest["dependencies"]["h3"]
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.20" and "quinn" in h3dep["features"]
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
lock_path = root / "Cargo.lock"
if lock_path.exists():
lock = lock_path.read_text()
assert 'name = "rust-proxy"\nversion = "0.32.1"' in lock
assert 'name = "rust-proxy"\nversion = "0.32.2"' in lock
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
for release_doc, marker in {
"README.md": "# RustProxy 0.32.1",
"CHANGELOG.md": "## 0.32.1",
"ROADMAP.md": "## 0.32.1",
"AGENTS.md": "Current source version: 0.32.1.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.32.1",
"README.md": "# RustProxy 0.32.2",
"CHANGELOG.md": "## 0.32.2",
"ROADMAP.md": "## 0.32.2",
"AGENTS.md": "Current source version: 0.32.2.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.32.2",
}.items():
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
@@ -285,7 +285,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
for marker in ['"version": "0.32.1"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
for marker in ['"version": "0.32.2"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
assert marker in openapi, marker
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker
@@ -1396,7 +1396,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
assert 'location.pathname === "/logging"' in settings_js_0241
assert '"RuntimeInformation"' in openapi and '"version": "0.32.1"' in openapi
assert '"RuntimeInformation"' in openapi and '"version": "0.32.2"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs")
+1 -1
View File
@@ -14,7 +14,7 @@ parser = argparse.ArgumentParser()
parser.add_argument('--require-lock', action='store_true', help='Require the post-upgrade Cargo.lock produced by cargo update')
args = parser.parse_args()
manifest = tomllib.loads((ROOT / 'Cargo.toml').read_text())
assert manifest['package']['version'] == '0.32.1'
assert manifest['package']['version'] == '0.32.2'
assert manifest['package']['rust-version'] == '1.99'
deps = manifest['dependencies']
expected_requirements = {
+1 -1
View File
@@ -51,7 +51,7 @@ assert '"/docs/content.json"' in admin
readme = (ROOT / "README.md").read_text(encoding="utf-8")
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert readme.startswith("# RustProxy 0.32.1")
assert readme.startswith("# RustProxy 0.32.2")
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
+1 -1
View File
@@ -4,7 +4,7 @@ from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
html = (ROOT / 'src/admin/web/index.html').read_text()
assert '0.32.1' in (ROOT / "Cargo.toml").read_text()
assert '0.32.2' in (ROOT / "Cargo.toml").read_text()
assert '<a class="brand" href="/"' in html, 'logo must remain a true full-page navigation'
assert 'addEventListener("click"' not in html.split('<a class="brand" href="/"',1)[1].split('</a>',1)[0]
body = (ROOT / 'src/proxy/cache_helpers.rs').read_text()
+3 -3
View File
@@ -8,9 +8,9 @@ from html.parser import HTMLParser
ROOT = Path(__file__).resolve().parents[2]
def read(rel): return (ROOT / rel).read_text(encoding='utf-8')
assert tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.32.1'
assert 'name = "rust-proxy"\nversion = "0.32.1"' in read('Cargo.lock')
assert json.loads(read('src/admin/openapi.json'))['info']['version'] == '0.32.1'
assert tomllib.loads(read('Cargo.toml'))['package']['version'] == '0.32.2'
assert 'name = "rust-proxy"\nversion = "0.32.2"' in read('Cargo.lock')
assert json.loads(read('src/admin/openapi.json'))['info']['version'] == '0.32.2'
pages = read('src/pages.rs')
for code in (400, 401, 403, 404, 408, 410, 413, 429, 500, 502, 503, 504):
+4 -4
View File
@@ -135,7 +135,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert backup.is_dir()
assert (backup / "src/obsolete.rs").is_file()
assert not (target / "src/obsolete.rs").exists()
assert 'version = "0.32.1"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert 'version = "0.32.2"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
assert (target / "langs/site.json").is_file()
@@ -146,7 +146,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert (target / "docker-agent/certificate.go").is_file()
assert (target / "docker-agent/server.go").is_file()
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
assert manifest["to_version"] == "0.32.1"
assert manifest["to_version"] == "0.32.2"
assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists()
assert not (backup / "legacy-data").exists()
@@ -617,7 +617,7 @@ def source_contract_smoke() -> None:
assert marker in admin, marker
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
assert '"/api/status"' not in anonymous_block
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.32.1"' in openapi_0241
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.32.2"' in openapi_0241
assert 'id="panel-logging"' not in html
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
assert marker in html, marker
@@ -1102,7 +1102,7 @@ def source_contract_smoke() -> None:
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
assert '"/api/config/history/restore"' in openapi
for marker in ['"version": "0.32.1"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
for marker in ['"version": "0.32.2"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker
assert '"software_update"' in openapi and '"update_available"' in openapi
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
+2 -2
View File
@@ -21,7 +21,7 @@ const openapi = json('src/admin/openapi.json');
const en = json('langs/en_US.json').translations;
const pl = json('langs/pl_pl.json').translations;
assert.match(manifest, /version = "0\.32\.1"/);
assert.match(manifest, /version = "0\.32\.2"/);
assert.match(lib, /pub mod user_agent;/);
assert.match(model, /user_agent_blocking: crate::user_agent::Policy/);
assert.match(vhost, /user_agent_blocking: crate::user_agent::Policy/);
@@ -52,7 +52,7 @@ assert.match(fileLogs, /localStorage\.setItem\(CONSOLIDATION_STORAGE_KEY/);
assert.match(fileLogs, /aggregate_min_count/);
assert.match(fileLogs, /file-log-aggregate-copy/);
assert.equal(openapi.info.version, '0.32.1');
assert.equal(openapi.info.version, '0.32.2');
assert.ok(openapi.components.schemas.Config.properties.user_agent_blocking);
assert.ok(openapi.components.schemas.Vhost.properties.user_agent_blocking);
assert.ok(openapi.components.schemas.FileLogSource.properties.group_id);
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env python3
"""Rust-independent structural checks; the Rust tests require Cargo."""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
source = (ROOT / 'src/proxy/forward/web_source.rs').read_text()
helper = (ROOT / 'src/proxy/web_cache_revalidation.rs').read_text()
tests = (ROOT / 'src/proxy/web_cache_revalidation/tests.rs').read_text()
assert 'mod web_cache_revalidation;' in (ROOT / 'src/proxy/mod.rs').read_text()
assert 'state.cache.revalidation_tiered(ticket).await' in source
assert '!cached.reusable_for_request(&request_headers)' in source
assert 'web_origin_request_headers(' in source
assert 'revalidation_sent && response.status() == StatusCode::NOT_MODIFIED' in source
assert 'finish_web_revalidation(' in source
assert 'state.cache.discard(ticket)' in source
assert 'cache.publish_revalidated(' in helper
assert 'cache.ticket_current(ticket)' in helper
assert 'cache.discard(ticket)' in helper
assert 'origin_safe' in helper
assert 'cached.apply_validators(&mut headers)' in helper
assert 'headers.remove("if-none-match")' in helper
assert 'headers.remove("if-modified-since")' in helper
for name in (
'browser_refresh_304_preserves_web_server_hit_counter',
'browser_conditional_refresh_returns_304_without_resetting_hits',
'mismatched_browser_validator_cannot_validate_different_cached_object',
'range_and_head_requests_use_validated_cached_body',
'changed_local_file_is_not_falsely_revalidated',
'304_new_private_policy_discards_cached_representation',
):
assert f'async fn {name}()' in tests
print('PASS: RustProxy 0.32.2 Web Server revalidation wiring and Rust regression contracts (static only)')
+5 -2
View File
@@ -136,8 +136,11 @@ assert basic_identity_pos < web_source_pos
assert identity_pos < web_source_pos
assert web_source_pos < proxy.index("let (backend, backend_lease, affinity_cookie)")
web_source = rust_source(ROOT, "src/proxy/forward/web_source.rs")
assert web_source.index("apply_response_rules(") > web_source.index("let origin_headers =")
assert web_source.index("crate::transform::apply(") > web_source.index("apply_response_rules(")
# The 0.32.2 local-304 path also applies response rules, before the normal
# 200-origin processing. Enforce order using the latter's distinct call site.
normal_origin_rules = "apply_response_rules(response.headers_mut(),"
assert web_source.index(normal_origin_rules) > web_source.index("let origin_headers =")
assert web_source.index("crate::transform::apply(") > web_source.index(normal_origin_rules)
need(acl, "ambiguous encoded path character", '"/%2e%2e/secret"', '"/a%2fb"', '"/a%5cb"', '"/bad%"')
need(admin, '"/web-server"', '"web-services"', '"vhost_metrics"')
need((ROOT / "src/admin/panel_assets.rs").read_text(), '"/assets/web-server.js"')