v0.32.2
This commit is contained in:
1 parent
101df02a5f
commit
bb025dcf44
25 files changed
+536
-80
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -1881,7 +1881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.32.1"
|
||||
version = "0.32.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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": [
|
||||
|
||||
@@ -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,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");
|
||||
|
||||
@@ -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;
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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).')
|
||||
@@ -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")
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"']:
|
||||
|
||||
@@ -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);
|
||||
|
||||
Executable
+31
@@ -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)')
|
||||
@@ -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"')
|
||||
|
||||
Reference in new issue
Block a user