This commit is contained in:
Mateusz Gruszczyński committed 2026-10-11 12:21:26 +02:00
1 parent 0facfe6f5c
commit 95c55da2ee
39 files changed
+715 -151

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. 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 The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.31.2. compatibility. Current source version: 0.31.3.
## Delivery ## Delivery
+6
View File
@@ -3,6 +3,12 @@
> Operator-facing release history. Historical engineering notes are kept in > Operator-facing release history. Historical engineering notes are kept in
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md). > [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.31.3
- Prevent duplicate session requests after BFCache/tab restoration; suspend SSE/polling until session validation finishes, then resume once. Replace automatic resume reload loops with a visible, localized retry screen and preserve the startup recovery limit until initialization is complete.
- Return a readable 403 document for invalid-Origin dashboard navigations; continue rejecting cross-origin API requests and cross-site fetches. Add redacted, bounded server diagnostics for rejected Origin checks and security regressions.
- Keep proxy routing, cookies/CSRF, local administration policy, SQLite schema 9 and user configuration unchanged. Rust/Cargo and real mobile reverse-proxy acceptance remain required.
## 0.31.2 ## 0.31.2
- Refresh core runtime, security, TLS certificate generation, database, Swagger UI and monitoring libraries for current stable versions. Keep persisted data, cache format, configuration, panel behavior, HTTP/3 and proxy routing compatible. - Refresh core runtime, security, TLS certificate generation, database, Swagger UI and monitoring libraries for current stable versions. Keep persisted data, cache format, configuration, panel behavior, HTTP/3 and proxy routing compatible.
Generated
+1 -1
View File
@@ -1881,7 +1881,7 @@ dependencies = [
[[package]] [[package]]
name = "rust-proxy" name = "rust-proxy"
version = "0.31.2" version = "0.31.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rust-proxy" name = "rust-proxy"
version = "0.31.2" version = "0.31.3"
edition = "2021" edition = "2021"
rust-version = "1.99" rust-version = "1.99"
description = "RustProxy - independent reverse proxy with local administration" description = "RustProxy - independent reverse proxy with local administration"
+11 -3
View File
@@ -1,11 +1,11 @@
# RustProxy 0.31.2 # RustProxy 0.31.3
**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). **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).
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration, Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools. streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
**0.31.2 dependency-refresh candidate source (Cargo verification required).** **0.31.3 mobile-session recovery patch source (Cargo verification required).**
## Layer-4 streams ## Layer-4 streams
@@ -13,7 +13,15 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners. Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
## What changed in 0.31.2 ## What changed in 0.31.3
- Correct mobile tab/BFCache recovery: share in-flight session validation across resume events and defer SSE/polling until verification succeeds. A failed check keeps a visible retry UI instead of reloading the page, and 401 still requires a fresh login.
- Preserve the one-time initial Origin reload guard until **full** panel initialization succeeds. Authenticated dashboard navigation rejected for invalid Origin now returns a visible 403 HTML explanation rather than raw JSON; API requests retain JSON errors and the same Origin/Sec-Fetch-Site authorization policy.
- Log bounded, credential-redacted Origin mismatch diagnostics (HTTP method, path, public host/scheme, fetch site and trusted forwarding metadata). No cookies, authorization headers, query strings or session tokens are logged.
- New deterministic UI tests cover simultaneous visibility/BFCache events, session errors, manual recovery, stale session epochs and repeated transitions. No configuration or database migration; SQLite schema 9 and proxy traffic remain unchanged.
- **Release gate remains open:** Rust/Cargo were unavailable in the validation environment, and the inherited 0.31.2 Cargo.lock still requires a real `cargo update`/build on Rust 1.99+. Reproduce mobile behavior in Safari/Chrome through the actual reverse proxy before deploying to production.
## Previously in 0.31.2
- Refresh Rust dependencies to current stable API families, including SQLx 0.9, Rand 0.10, Argon2 0.6, rcgen 0.14, Tower HTTP 0.7, Brotli 9 and Sysinfo 0.39. Retain SQLite schema 9, serialized settings, TLS/QUIC behavior and administration contracts. - Refresh Rust dependencies to current stable API families, including SQLx 0.9, Rand 0.10, Argon2 0.6, rcgen 0.14, Tower HTTP 0.7, Brotli 9 and Sysinfo 0.39. Retain SQLite schema 9, serialized settings, TLS/QUIC behavior and administration contracts.
- Raise the minimum compiler version to Rust 1.99 for current dependency requirements, including Docker and RouterOS build images. - Raise the minimum compiler version to Rust 1.99 for current dependency requirements, including Docker and RouterOS build images.
+7
View File
@@ -1,5 +1,12 @@
# RustProxy roadmap # RustProxy roadmap
## 0.31.3 - Mobile session recovery and Origin diagnostics
- [x] Reproduce the overlapping resume-session checks, reload-loop marker reset and early SSE restart using the 0.31.2 source in a deterministic JavaScript simulation.
- [x] Coalesce page visibility/BFCache session verification, defer background requests until revalidated and offer a visible retry on failure without weakening Origin enforcement.
- [x] Display a safe 403 HTML message for invalid-Origin dashboard navigation and provide credential-redacted rejection diagnostics.
- [ ] Pass Rust/Cargo compile/clippy/test gates and live mobile Safari/Chrome reverse-proxy verification; diagnose the original 403 trigger using the logs.
## 0.31.2 - Dependency updates ## 0.31.2 - Dependency updates
- [x] Prepare stable dependency-family and code API migrations, preserving existing wire/configuration/SQLite formats. - [x] Prepare stable dependency-family and code API migrations, preserving existing wire/configuration/SQLite formats.
+46 -43
View File
@@ -1,18 +1,18 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
047139f3c7c40778001602e77ef69302e3bef8f495d48d780953c97c401fa2d4 AGENTS.md 2326433366f5ee39f50cc0c8fd308a1aa4bf913dd29c60d36d6ad40596e2057e AGENTS.md
9b42590541917e8d92fa26a5831b9dd43503f179337f8ed6c3a4d7d1de84d3b7 CHANGELOG.md fe15afa19911918df2cb8f754d42b23e367056b2350c4421273e4ea15558e04c CHANGELOG.md
50238f33805987e7301f9ed06a8fd51b74b0e01330dbbae42c3a2fd8bd0d8e86 Cargo.lock a527e1bdd3ad6217a4a0bb06f29d33b4f5718525b8de81d4170ec7233bee5155 Cargo.lock
c6cc53538b89420b82526bc766d73a5fc93a82017f8fdb4bf767335dd613c90a Cargo.toml 267b8d8ff64905e059ea6626a6ab48fb20f42cf5ae4af25ffc69b07535a9d42b Cargo.toml
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile 25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
f3ad7c8b4b450bcb32a7c68f90e868726154b3f1ea88c7d9ba5173850334d24e README.md 1ceec1cdf7e056bf26096eedcf95f0efb192b69c0f614ba13ecf2808e534ebf3 README.md
e2187e12e611638f85bae7808947b46a5de3c12db234ec217767ac8aefd1919a ROADMAP.md 4c8c8a4efa169b7c92d27715d15d467c95c705bd8b439302e6bbde26e56f1dfd ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service 4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
31d018c9ef676d397bafa66c5099a57bc41ee56bbb272df59bc521574d920c82 dev.sh 5147ac3233914fe580b4c4d5ae1b74af2948019f34ff6629d21a7811b27a5b84 dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md 5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go 7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example 5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -41,6 +41,7 @@ befa50579e3475b22589f26380d85b3345b80e4fa947d49649a3b91a8dd8b8c2 docs/CUSTOM_LO
8dfc89a8b3512103b87864bd7c24a3dc3de4465edf8f9874e6bed1f4634dd930 docs/HTTP3.md 8dfc89a8b3512103b87864bd7c24a3dc3de4465edf8f9874e6bed1f4634dd930 docs/HTTP3.md
5301e321611d494fd97b04ab648df1f66a6a0d1f1f4c3475eab485e138d5605b docs/IMPLEMENTATION_0.31.0.md 5301e321611d494fd97b04ab648df1f66a6a0d1f1f4c3475eab485e138d5605b docs/IMPLEMENTATION_0.31.0.md
eb52e72325e1307d05cb04878b6d22dfedea491908c5f36110b32421765096c8 docs/IMPLEMENTATION_0.31.1.md eb52e72325e1307d05cb04878b6d22dfedea491908c5f36110b32421765096c8 docs/IMPLEMENTATION_0.31.1.md
991ff3c09ca1e2bcf73b8fa2975405b955dd640c3f1e1246e75038c043c17183 docs/IMPLEMENTATION_0.31.3.md
a00ec3c1d3b28ba10677435128e031e5f18fe8c5cef2ac90eb0f6779cefeb434 docs/INSTALL.md a00ec3c1d3b28ba10677435128e031e5f18fe8c5cef2ac90eb0f6779cefeb434 docs/INSTALL.md
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md 3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md 4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
@@ -54,7 +55,7 @@ e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND
e68d5a5cf0991de9308f78afb0264e02979f7463a5f395eb7a6e9ec2bd3a3d54 docs/PHP_FPM.md e68d5a5cf0991de9308f78afb0264e02979f7463a5f395eb7a6e9ec2bd3a3d54 docs/PHP_FPM.md
bc068a176056f5ea4d3b637071e644ed58a2140494c233ef22aa66e9700e3299 docs/PHP_FPM_ISOLATION.md bc068a176056f5ea4d3b637071e644ed58a2140494c233ef22aa66e9700e3299 docs/PHP_FPM_ISOLATION.md
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md 572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
7f3b78e71f91bc985dc5e40509264292ddd51a9a9b54f38d160fac4aa2021b6c docs/README.md e87c81d2eb2e90d5b1ea9e40abb81a65ca81593eb5cc3e39717b818c9a75b287 docs/README.md
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md 6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md 0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.md d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.md
@@ -65,7 +66,7 @@ a2bb837cdd3559d58158880c1f832badf64035376a3623050bb4e40c2bca104c docs/ROADMAP_H
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md 007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
2cd9dadc49a5474e4ab06d5c01fa38c27cab436578cb9c9f327a37b3406d2e13 docs/SETTINGS.md 2cd9dadc49a5474e4ab06d5c01fa38c27cab436578cb9c9f327a37b3406d2e13 docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md 3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
8ce2502e64a159a23eadebafdc9f0d65d384e8099eb63368748ee7e0269e299d docs/TEST_REPORT.md 1de7cf885e596ccfab31a434d597c3a8d9113d206305810ea628a5ee6d96f986 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md 751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
660687fc961f71aae2bdf71595aa47a336fa782f4e78ceee8c1d1e63e46393ee docs/TRANSPORT.md 660687fc961f71aae2bdf71595aa47a336fa782f4e78ceee8c1d1e63e46393ee docs/TRANSPORT.md
@@ -125,7 +126,7 @@ d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhost
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
4068b79ddd898af43b85617d8de9e9f48cfee09a4fb9ed26bf5b692571a30196 grafana/README.md 4068b79ddd898af43b85617d8de9e9f48cfee09a4fb9ed26bf5b692571a30196 grafana/README.md
0d97fbf31f60563bb41f2ea8d798bccebc3d4c57f10bad8b7faa17296722e36b grafana/rustproxy-overview.json 0d97fbf31f60563bb41f2ea8d798bccebc3d4c57f10bad8b7faa17296722e36b grafana/rustproxy-overview.json
fde541cd76bc3fa2b6765822bf04baf9f9ea3e4b6e185e76ff467270ddbd945e install-debian13.sh a05cbbf101b288bf9cf83fedfc5017925a8b360a78f313d31c1a70f15dc1dd22 install-debian13.sh
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh 909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md 42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 integrations/opnsense-acme/README.md 2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 integrations/opnsense-acme/README.md
@@ -137,8 +138,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh 96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh 3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md 71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md
70c85a656bac197b7dcf7048188ec2d3320c3da39908f79477a6484b968f6901 langs/en_US.json 19c17d16960539f9811cd551bbcbba25863711b064618069a58d48de3cc9fade langs/en_US.json
0352d8d3c65ba5f60870a2fd3e945a79333e69a94d3349799ef285b22c3a9476 langs/pl_pl.json fcdb2db09c7a105893fc13462da4d0341a873e305bcc3fdcd0bd5e10b9a110ba langs/pl_pl.json
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
@@ -164,7 +165,7 @@ a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py 31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
308ceeca51ddfb43db8be8286d5286aff1a44979f84af87d9346fb4176499c0c scripts/resolve_dependencies_0312.sh 3d8cfbc8177ef82f28dfba3fd555955dac4a29003267f93572ea26a98fb8255e scripts/resolve_dependencies_0312.sh
20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py 20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py 22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py
@@ -182,14 +183,14 @@ e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests
bc9eb32f0d2785bac122d5eff44742cd24306c503daa065564383d46416b2211 src/admin/access.rs bc9eb32f0d2785bac122d5eff44742cd24306c503daa065564383d46416b2211 src/admin/access.rs
019f4aa33e3596bc9c11ef8cf5703bcfffe46c15288c0eba83c8dd9e38055fb2 src/admin/certificates.rs 019f4aa33e3596bc9c11ef8cf5703bcfffe46c15288c0eba83c8dd9e38055fb2 src/admin/certificates.rs
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs 1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
09498a66fefa1db0500d42b73833be67690af07ea33edb4ba57b939e8220322a src/admin/documentation/documents.rs 69095640bb0dca081317c7776cd73e83fab11ddd04fe903acea8f8c1b4899210 src/admin/documentation/documents.rs
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs 67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs 002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs
2ebaf24e9d0d87383d2eea073ee2d3ac37962eeb503a0f1ee2c2de4f3f6d1b7a src/admin/file_logs.rs 2ebaf24e9d0d87383d2eea073ee2d3ac37962eeb503a0f1ee2c2de4f3f6d1b7a src/admin/file_logs.rs
d7aa68a78a4cb5a61342d92c1f3209af230d0f9ca3fea1115f67078372e6f37c src/admin/file_logs_io.rs d7aa68a78a4cb5a61342d92c1f3209af230d0f9ca3fea1115f67078372e6f37c src/admin/file_logs_io.rs
899e87a73bfe46c4078a46f9e1eb1c17b384472e8a30d2bab5486872b43fc809 src/admin/logging_api.rs 899e87a73bfe46c4078a46f9e1eb1c17b384472e8a30d2bab5486872b43fc809 src/admin/logging_api.rs
58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs 58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs
8c58aec76463fc512ee87a8cceff7b905488c6f70185c055c496d235badea6d7 src/admin/openapi.json a857bcbdef2a673be2f01c5817ca6d75dd5627b7e3b80777c394aa2edd933cd8 src/admin/openapi.json
0d1a5324e8ad32f5ab0861b722663f50f066e256bc5fd5dcced6a63f2bd399d1 src/admin/operations.rs 0d1a5324e8ad32f5ab0861b722663f50f066e256bc5fd5dcced6a63f2bd399d1 src/admin/operations.rs
2b0ec6df0ed9e1bdc5c9e4ff1758b59dd14abc150f98365ac3edcf4672908e48 src/admin/panel_assets.rs 2b0ec6df0ed9e1bdc5c9e4ff1758b59dd14abc150f98365ac3edcf4672908e48 src/admin/panel_assets.rs
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs 75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
@@ -198,14 +199,14 @@ d9e90166d2199152585a5208feedfc1dd1395dc32ddbd2a094ae4a1aee45e115 src/admin/pref
9baf3c0ec4c543bda51605930ac57d6425c5e03e06d29ff755a7ce3ff611aeaf src/admin/public.rs 9baf3c0ec4c543bda51605930ac57d6425c5e03e06d29ff755a7ce3ff611aeaf src/admin/public.rs
c9f952bec99dfe1d5a60998a38bd177f82f54de567230413e0c322ee561eba7c src/admin/public/tests.rs c9f952bec99dfe1d5a60998a38bd177f82f54de567230413e0c322ee561eba7c src/admin/public/tests.rs
6125419b099cf4d330767bc07424d52a2b400f273d31084c058c43a72a6427b7 src/admin/refresh_preferences.rs 6125419b099cf4d330767bc07424d52a2b400f273d31084c058c43a72a6427b7 src/admin/refresh_preferences.rs
f99655cf19b04b1eb7fb49d12bfd2ba1ef2c095b81e80ef46cfddfacefc91936 src/admin/router.rs 765c14e52bdc9e542d6ff651c250c24d33b77b7e46f1b5f19acf96cc2c99bdc8 src/admin/router.rs
8708427263cd4caf8454b9115e89acb09172483a090c6b0bd43811dd9e52c7f6 src/admin/session.rs daf0f7393f2350e569a5e8dda52dbf3e3bbefcb989f07f723dca540ae0fe487e src/admin/session.rs
6fa2e586feb335ea9ce2c4dc523e422e4b2aa52c823ba8f8851f9e0f37cd2fef src/admin/status.rs 6fa2e586feb335ea9ce2c4dc523e422e4b2aa52c823ba8f8851f9e0f37cd2fef src/admin/status.rs
e79d3dc0c4fde1ca4a69c204fb41cc5aa1db02aefdabff3435f27b5db0c6eb8d src/admin/status_live.rs e79d3dc0c4fde1ca4a69c204fb41cc5aa1db02aefdabff3435f27b5db0c6eb8d src/admin/status_live.rs
c97881b17b7a42511868b0e48e2f25aab6fb5b613d538f5e1f1967508454e486 src/admin/status_system.rs c97881b17b7a42511868b0e48e2f25aab6fb5b613d538f5e1f1967508454e486 src/admin/status_system.rs
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
3277a08915eb2e272ad4c0f3aa1ffa0c7fed4e6a1d1fb66d4bfd8a4a4ea515aa src/admin/tests.rs 3277a08915eb2e272ad4c0f3aa1ffa0c7fed4e6a1d1fb66d4bfd8a4a4ea515aa src/admin/tests.rs
5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs 18aa617a0bf60a3d8d2ad7dd059a369e7549ef46683d8f912e07f8fb091c26d1 src/admin/tests/part_01.rs
6f8c1e30babb2cc4cb7accf4bfa28338651a469b16341f6e7eaec881dd9abf2b src/admin/tests/part_02.rs 6f8c1e30babb2cc4cb7accf4bfa28338651a469b16341f6e7eaec881dd9abf2b src/admin/tests/part_02.rs
34cf08cafb63cf7a8a6336e5c81fc99abfce8669662e387a3702c3a532be8056 src/admin/tests/part_03.rs 34cf08cafb63cf7a8a6336e5c81fc99abfce8669662e387a3702c3a532be8056 src/admin/tests/part_03.rs
2137a53991c9337988545c4472b4ffcb9a111cac9c460ebbec7e51c0be8f5630 src/admin/tests/part_04.rs 2137a53991c9337988545c4472b4ffcb9a111cac9c460ebbec7e51c0be8f5630 src/admin/tests/part_04.rs
@@ -217,10 +218,11 @@ cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail
bffa6611f3776e97d0065b510b63f083aabf7b3429fcc5cb26b1883fdb7c47dd src/admin/tests/part_10.rs bffa6611f3776e97d0065b510b63f083aabf7b3429fcc5cb26b1883fdb7c47dd src/admin/tests/part_10.rs
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js 48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
446011c2c7afddf7734ceb268c75a3b4146dbe8e915b11986495e956b6c62c69 src/admin/web/app/config-history.js 446011c2c7afddf7734ceb268c75a3b4146dbe8e915b11986495e956b6c62c69 src/admin/web/app/config-history.js
7ec3f1b199d3e0134c1097231ded61a23fd152cdb702c008bac8f496a47b52f0 src/admin/web/app/core.js 9527dcafb9c14e420cb841a6984d373ba2136d07c45350e9368dd7a88e89af22 src/admin/web/app/core.js
23721bfbac850c4f77b116cc32bb9d2db6361ac4dd7e310a8c8070ab906997f6 src/admin/web/app/preferences.js 23721bfbac850c4f77b116cc32bb9d2db6361ac4dd7e310a8c8070ab906997f6 src/admin/web/app/preferences.js
344faee969e9d3b8c2f59a443d8ab1090c556ceff325a109e745663baa5e644e src/admin/web/app/refresh.js 729ed152eb8471c67fb0e55e44fe0669c2708802ce83228df9cb4e53c14cee02 src/admin/web/app/refresh.js
397421b1fb680f5de1aa722e82f8a882877135b4a699e9fde0a9088c875341b3 src/admin/web/app/runtime.js 984d092961bfaaa19f982e54c7d4be474e8417b208d814ac88bda63f1ab19f4c src/admin/web/app/runtime.js
db56370db60e514a2241d231cd949bac2c7aa2f413a2c14035a877029a8d864f src/admin/web/app/session-resume.js
86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js 86ac42bd4a6a84bb137d5a3c3dc3d1f4e404a543447845e6ea6e6e8210e268ce src/admin/web/app/settings.js
4fec1344be3cd540d1e6d0b87fb0aa94e1bef2968c4f86366a62c56d4085f452 src/admin/web/cache-panel.js 4fec1344be3cd540d1e6d0b87fb0aa94e1bef2968c4f86366a62c56d4085f452 src/admin/web/cache-panel.js
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html 50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
@@ -238,16 +240,16 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
6544bbebd94208551d5d5fecbfc9c6383dd01fb0a3680867ec9e6bbf05a9a29d src/admin/web/index.html 8490011e8164e6fab9601ff75a82eef3cc7972d1fdb8e8c4f73df5796ae72d37 src/admin/web/index.html
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js 24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
af0fbd94d9b88ce1cce28e39dfd2c35de031adcba412dd4a9a17d9b416c256dd src/admin/web/login.html af0fbd94d9b88ce1cce28e39dfd2c35de031adcba412dd4a9a17d9b416c256dd src/admin/web/login.html
99fe9b8ba7a8a24e5bbe9ae40999484fe13b0faf5eb1d7a9193701f667e51ca5 src/admin/web/login.js 99fe9b8ba7a8a24e5bbe9ae40999484fe13b0faf5eb1d7a9193701f667e51ca5 src/admin/web/login.js
810e7e5bf1af302b51d90771c3ec3c20227eca392f8c7d91d027e11a999bc236 src/admin/web/management.js 21ddae9743ca545c89f6df0db40810dd680812e58f2b2b6875ed64fd8fb97ac6 src/admin/web/management.js
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js 90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
53cdc1d9d136bbea44a2cf9f06193e90392beb3544c687fbec0fff896565a3c2 src/admin/web/settings.js df6efd27b1ebe1fafeb0dc6061b69be9aa46f71451f0fcdd00e2f7ef8692d3cb src/admin/web/settings.js
bb67e77af5514478485cfb69f339d63d7eb0cbadd093fa054814a416260ccc1d src/admin/web/streams.js bb67e77af5514478485cfb69f339d63d7eb0cbadd093fa054814a416260ccc1d src/admin/web/streams.js
727ad8dceae6f44ed2ebdb998fe29169bd61406700980b04390c0713c8d8b86c src/admin/web/style.css c4f051f54293156803b48c04d972e9d5a290209826138ce15356d19adf569979 src/admin/web/style.css
d01b615635c7373981b0633cc27298a34e67fe0cbe9d1384d277d46f0cafb1b4 src/admin/web/traffic-panel.js d01b615635c7373981b0633cc27298a34e67fe0cbe9d1384d277d46f0cafb1b4 src/admin/web/traffic-panel.js
a6e7bbff4fd2d50ecf759ed28161a672d7b483af2b35c9f912746a7a06a9f476 src/admin/web/transform-panel.js a6e7bbff4fd2d50ecf759ed28161a672d7b483af2b35c9f912746a7a06a9f476 src/admin/web/transform-panel.js
57a2b975716bdab32c33ec1bf9427b25998550e81ce191440ee66ffb7e2a249e src/admin/web/vhosts/core.js 57a2b975716bdab32c33ec1bf9427b25998550e81ce191440ee66ffb7e2a249e src/admin/web/vhosts/core.js
@@ -256,9 +258,9 @@ a6e7bbff4fd2d50ecf759ed28161a672d7b483af2b35c9f912746a7a06a9f476 src/admin/web/
f2689283cfe43d6aae911b0df778a69335089be230fbfc6de5dcce1801c38427 src/admin/web/vhosts/render.js f2689283cfe43d6aae911b0df778a69335089be230fbfc6de5dcce1801c38427 src/admin/web/vhosts/render.js
91e3417fba7e8f59023cd64813befccc8341741cd4aa53dfa1ec68a03c7f4632 src/admin/web/web-server-profiles.js 91e3417fba7e8f59023cd64813befccc8341741cd4aa53dfa1ec68a03c7f4632 src/admin/web/web-server-profiles.js
de3326d4fae254eb0b8c190f02e762f4094dcba86bfdcb24c5fb472676ebcc4b src/admin/web/web-server.js de3326d4fae254eb0b8c190f02e762f4094dcba86bfdcb24c5fb472676ebcc4b src/admin/web/web-server.js
76b0d92e4360e06ae97f87c0f6ddde4962c44efdeec9727e5b9a720598302891 src/auth/files.rs 53237ae8b08d261ce21e0aa6fd225bf4f3bb17a37a80529e2a4039d45b13f7c5 src/auth/files.rs
b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs
105cb934956457a68adc14f2c64569d9d1d40b8da0a7908a9a59fc2153a939c9 src/auth/mod.rs 775178ee5781e64574bdd7c1fb7d64e7e283977fbd9c98c99896532a7f1df612 src/auth/mod.rs
c96a87e82ff0c1c9ce3d0cd5135903bb53af78d1c4fb6ebfe5fea503bf5644ea src/auth/store.rs c96a87e82ff0c1c9ce3d0cd5135903bb53af78d1c4fb6ebfe5fea503bf5644ea src/auth/store.rs
745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs 745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs
09c3a622a7d2169e49e3369ea5f0f816eab2f717eaf1675c625d518c8f34d367 src/backup.rs 09c3a622a7d2169e49e3369ea5f0f816eab2f717eaf1675c625d518c8f34d367 src/backup.rs
@@ -270,8 +272,8 @@ f5afae218f3a6f91bc2f1f5a90094630ce5b7ca574de61807b5a317b13937ec2 src/backup/val
426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs 426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs
3d4b3986f54bed99212063b6d135d40a202d556a3e4d253e24c74be4cb9e8ebc src/balance/health.rs 3d4b3986f54bed99212063b6d135d40a202d556a3e4d253e24c74be4cb9e8ebc src/balance/health.rs
a1e5d43f5417d71f3974812cb539c0932f206e2290090917f067494f3587fc72 src/balance/lease_runtime.rs a1e5d43f5417d71f3974812cb539c0932f206e2290090917f067494f3587fc72 src/balance/lease_runtime.rs
46d739f14b510b35aaccf18fb9f21593c92d8b8d3ea47828271574df5ed008e5 src/balance/model.rs 4d3aa415af2fbb42a4e19f2ca10be8b1d1ad42f366a013cd9c33806abc1fbea2 src/balance/model.rs
2a6b20ffc2b326cb2380beb0805b52181306d3b9cc57330d67930000535c03f9 src/balance/pool_core.rs 41c3c020a60c372dbf965bd277c330dd9cf2ab079f6608b472adf72a656f4cb3 src/balance/pool_core.rs
477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs 477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs
519cb798040ac9a65d9ebe43ae11ef729bd78c5c4a5c756490458add4b6fdc24 src/balance/pool_view.rs 519cb798040ac9a65d9ebe43ae11ef729bd78c5c4a5c756490458add4b6fdc24 src/balance/pool_view.rs
7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/balance/tests.rs 7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/balance/tests.rs
@@ -412,7 +414,7 @@ c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/head
cc682f0d5754e0b806a4eb5950e4cc00611aca5ce4668414fddef1d692489805 src/proxy/tests/authority_tests.rs cc682f0d5754e0b806a4eb5950e4cc00611aca5ce4668414fddef1d692489805 src/proxy/tests/authority_tests.rs
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs
e80c7fe44abb7607c64ea02d56542753dc25290b3bcf4c4d45b75f2db6ca90d0 src/proxy/types.rs e80c7fe44abb7607c64ea02d56542753dc25290b3bcf4c4d45b75f2db6ca90d0 src/proxy/types.rs
55c44a5d0c6588beea67439d62e3c9f57041e4d07cb1f8cf293efd856ceb8eff src/proxy/upstream_helpers.rs 9999f717492755d62fc05cd5c1aff802ff0da901fe3e6b928bd8a000833c8799 src/proxy/upstream_helpers.rs
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs 69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
d866e7e72dee6ff54c2418c34e9899cbcd7e4e027d9cbe2b625044dc9062e4ee src/proxy/websocket/handshake.rs d866e7e72dee6ff54c2418c34e9899cbcd7e4e027d9cbe2b625044dc9062e4ee src/proxy/websocket/handshake.rs
7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs 7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs
@@ -429,7 +431,7 @@ d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
e90df3fff8a25465425b099ae358d19085809e56d948ba47e01e53f8b93d5dd0 src/runtime/tests.rs e90df3fff8a25465425b099ae358d19085809e56d948ba47e01e53f8b93d5dd0 src/runtime/tests.rs
0046525317ffbd5d3b34845656b0f58d9d31f38b3d8acd71e90d94bd7cdfb8ab src/runtime/types.rs 0046525317ffbd5d3b34845656b0f58d9d31f38b3d8acd71e90d94bd7cdfb8ab src/runtime/types.rs
88bc546fdda20d3f1f8f5e7d7872ede1fa334b8bb5102c729bd72f56e3fb94b9 src/runtime/updates.rs 88bc546fdda20d3f1f8f5e7d7872ede1fa334b8bb5102c729bd72f56e3fb94b9 src/runtime/updates.rs
b5062082d7fc7c9b08533283e1f0a841f85a42917b6ebf135760b04023884d9a src/secret_store.rs 208a378b14ca80e1412ffa19ef1b140594182971e2e797ee0bb95bcdb8404da7 src/secret_store.rs
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs 6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs 6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs
0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs 0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs
@@ -523,33 +525,34 @@ b04362ad082a32c9ccc4cf38c46f44d71347adecf7aba3224b3af3ea4872e3d0 tools/README.m
fd082812411cff7afb71b2289a6f1a469b8d3c8a7cfdcdd9e567a2a4baf81be4 tools/checks/admin_surface_audit_0310.py fd082812411cff7afb71b2289a6f1a469b8d3c8a7cfdcdd9e567a2a4baf81be4 tools/checks/admin_surface_audit_0310.py
412255d383fcc8306c6086784fb416d9814e3ec790e440a0927431ca615f3411 tools/checks/backend_cache_0297_ui_test.mjs 412255d383fcc8306c6086784fb416d9814e3ec790e440a0927431ca615f3411 tools/checks/backend_cache_0297_ui_test.mjs
2a493dbce2991a23d807c01952f58608a364f541560836b4cb385e75e108e9e7 tools/checks/backend_health_0300_ui_test.mjs 2a493dbce2991a23d807c01952f58608a364f541560836b4cb385e75e108e9e7 tools/checks/backend_health_0300_ui_test.mjs
4bb77602254ff8ed1673b6d31518c77ae40c9b9c44e85ac436b93a300115ca18 tools/checks/check_source.py bebe46609381f2050d90c809e9b5ac7b564d5b26f733a926f20e5363a4140d34 tools/checks/check_source.py
0b583ee59dcccc35b15ef4e211d7e6546a8aaa7b0286ceab03c0b0dc084c1e07 tools/checks/dashboard_boot_0311_ui_test.mjs 0b583ee59dcccc35b15ef4e211d7e6546a8aaa7b0286ceab03c0b0dc084c1e07 tools/checks/dashboard_boot_0311_ui_test.mjs
5f798699502c7a18e7b45aa84bdcf7ad0ccdb77f11cf2003527c97f0391c7abe tools/checks/dashboard_startup_0310_ui_test.mjs 259d4b0985c82b631e151a07e8ef3b4c760019cb99bb2af458ce577497f7df58 tools/checks/dashboard_startup_0310_ui_test.mjs
3a5703cd3e0e511e0ea8da0c7802ccdc5cf53289f9b9169b9cf96ea5209b4a57 tools/checks/dependency_upgrade_0312.py 6adc94ee148508a2e46f073eb6f3b89c4057983dea12c976ae7332fb8423ce41 tools/checks/dependency_upgrade_0312.py
5d4fa0b66ae05241236551c07c900ad0c437bd10e34406b17a8a002188caebc3 tools/checks/docs_check.py dd7b7512f5a988c1fe6fdbdeb78c3b0d33a23b57335eb4f881296cb252d8692c tools/checks/docs_check.py
e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py 4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
f8f87ba891338b565249d02c3cf5f02cec44e606b4205de99fcbea1eb2c24d57 tools/checks/memory_contracts.py f8f87ba891338b565249d02c3cf5f02cec44e606b4205de99fcbea1eb2c24d57 tools/checks/memory_contracts.py
af98579ee942e774d88775c261ac351847c28bb4a5396dc67e458bd6b1ff7963 tools/checks/origin_resume_0313_ui_test.mjs
5b7901747d879e12a39ed82b6efc513bcdddd15d508b5828210d2c83398fb6a4 tools/checks/panel_cache_http_0310_ui_test.mjs 5b7901747d879e12a39ed82b6efc513bcdddd15d508b5828210d2c83398fb6a4 tools/checks/panel_cache_http_0310_ui_test.mjs
8c5c64b681de7f72f01b414f2c77d1f9a44c13b47972d8252a0d392fd71c6ab4 tools/checks/preflight.py 7770cc0b1b70255003d19b63210cc3e21ca9c8804cb56c2fe01eaf2e04ebb0fa tools/checks/preflight.py
3485681d369b5f486dfc2d14660f19b4098df670bafd9a296cc9dabe68003a51 tools/checks/refresh_0310_ui_test.mjs 021a97bd6763b59f3f7eca15f51efdf88d0c863f1f81a2e42ba80a9a09ca3fec tools/checks/refresh_0310_ui_test.mjs
db468bc0ad68704e70a15d7dda894d1913db1e2ce94e04a84b20c7dadfaa80a6 tools/checks/release_smoke.py 951d184c67cf3b006f89dc6588f460797fcad9c15460f6638abd8316f8777736 tools/checks/release_smoke.py
f69845bc26ec33160fc756953991cf32aa8ad6a7fb4a131caa2093db91e783ef tools/checks/rust_module_contracts.py f69845bc26ec33160fc756953991cf32aa8ad6a7fb4a131caa2093db91e783ef tools/checks/rust_module_contracts.py
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py
adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py
23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py 23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py
5c1d428cbda5f08b8d6847732672e45e9da96b545337334e473986e119197de2 tools/checks/source_module_contracts.py 0d211440ae77f16187a5d833156911c7793ed27ac1d77e79de362e47c1ab1abd tools/checks/source_module_contracts.py
49c1fe96340d97c326943826dd8b3331b884c19f4a9631beaa820f01a12108e0 tools/checks/system_info_0301_ui_test.mjs 51afd5f31eeea8f1658d239dd34637d38a0a7eb1fb9f58a212040505d3b39b0c tools/checks/system_info_0301_ui_test.mjs
543a488951ea6ec8fe06e04f168ccc4d4f2af3cfc281c9a728ab7bd87377fa98 tools/checks/system_info_no_subprocess_contracts.py 543a488951ea6ec8fe06e04f168ccc4d4f2af3cfc281c9a728ab7bd87377fa98 tools/checks/system_info_no_subprocess_contracts.py
614ed0c775e32ec5bfb4ee75aaa90206a10da0f43659747a7ec76fd93e33767a tools/checks/version_contracts.py 614ed0c775e32ec5bfb4ee75aaa90206a10da0f43659747a7ec76fd93e33767a tools/checks/version_contracts.py
2020e171be1a044f7ca2ab83af7604a014c36fe4ff04791d0620ccbf1bf8e000 tools/checks/vhost_origin_errors_0297_ui_test.mjs 161637cb5e2e613e3c71cf8489cc936fa84913a2a02649463f8893a28a674242 tools/checks/vhost_origin_errors_0297_ui_test.mjs
0c071c26fc316934bdfd2bfd19f5fed9521790d8c887760eaabda81a9be96703 tools/checks/vhost_www_ui_test.mjs 0c071c26fc316934bdfd2bfd19f5fed9521790d8c887760eaabda81a9be96703 tools/checks/vhost_www_ui_test.mjs
597d6d2da052390e285f18a257d10e6ab8dbd7add666457656cd803416b7975d tools/checks/web_assets.py 08f75c17084352df8d4dd0d1e712486d6d600739451735976de2234903538bc5 tools/checks/web_assets.py
0398892be30fe416a00578915c588f360fd93244e3af77ece834e560e547d1c8 tools/checks/web_server_contracts.py 0398892be30fe416a00578915c588f360fd93244e3af77ece834e560e547d1c8 tools/checks/web_server_contracts.py
35f76060e585a96c623ef83bd5f771969f5be16a0e1ae626a3092d7458b84e84 tools/checks/web_service_ui_test.mjs 35f76060e585a96c623ef83bd5f771969f5be16a0e1ae626a3092d7458b84e84 tools/checks/web_service_ui_test.mjs
adaa1ed9e5958739df40190055c780c80d0a8e270ec8dc105ed3fa2dbb8fb74f tools/checks/www_redirect_ui_test.mjs adaa1ed9e5958739df40190055c780c80d0a8e270ec8dc105ed3fa2dbb8fb74f tools/checks/www_redirect_ui_test.mjs
05382366fd801a17f8d7c1166fd4e64b8aa70e465cb69121519b239ebf8ed55f tools/dev/memory_load.py cfa2dee224a11bf3948dbe528f5877a76b830b7efb01cd785007213cc245493d tools/dev/memory_load.py
d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py 2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py
538b5f6cfa738355a729940ee3d92e7ab3dc25ca69ae330ddaa08f831775b8ba tools/dev/test_compression.py 538b5f6cfa738355a729940ee3d92e7ab3dc25ca69ae330ddaa08f831775b8ba tools/dev/test_compression.py
+1
View File
@@ -110,6 +110,7 @@ if [[ "$mode" == "test" ]]; then
node tools/checks/refresh_0310_ui_test.mjs node tools/checks/refresh_0310_ui_test.mjs
node tools/checks/dashboard_startup_0310_ui_test.mjs node tools/checks/dashboard_startup_0310_ui_test.mjs
node tools/checks/dashboard_boot_0311_ui_test.mjs node tools/checks/dashboard_boot_0311_ui_test.mjs
node tools/checks/origin_resume_0313_ui_test.mjs
node tools/checks/panel_cache_http_0310_ui_test.mjs node tools/checks/panel_cache_http_0310_ui_test.mjs
node tools/checks/vhost_origin_errors_0297_ui_test.mjs node tools/checks/vhost_origin_errors_0297_ui_test.mjs
node tools/checks/www_redirect_ui_test.mjs node tools/checks/www_redirect_ui_test.mjs
+43
View File
@@ -0,0 +1,43 @@
# RustProxy 0.31.3 - Mobile admin session recovery
## Scope and evidence
The supplied 0.31.2 source was inspected and executed with a deterministic Node.js VM harness. The original functions permitted two simultaneous `/api/auth/session` requests from `pageshow` and `visibilitychange`. In the reproduced race, one `auth.invalid_origin` response called `location.reload()`, another successful response cleared the reload-protection marker, and a subsequent 403 triggered another reload. The event ordering and client-side failure were reproduced; the original server-side rejection on the reported mobile device remains unexplained because no captured mobile HTTP exchange was provided.
The dashboard's prior visibility handler also restarted the SSE channel and refresh timers before it checked the session. A full dashboard navigation rejected at the server could return raw JSON as the document, rather than an actionable page.
## Changes
- `src/admin/web/app/session-resume.js`: shared in-flight session validation; one visible retry path after 403/network errors, no `location.reload()` on resume; successful checks update CSRF/session display and release polling only if the auth epoch is unchanged. An HTTP 401 keeps the existing login/lock flow.
- `src/admin/web/app/refresh.js`: `visibilitychange`, `pagehide`, and BFCache `pageshow` share a resume gate. Pause/abort polling and close SSE before validating; allow background tasks only after success. Repeated callbacks during the same visible period do not initiate another check.
- `src/admin/web/app/runtime.js` and `core.js`: initial-startup reload marker is cleared only after successful full activation, including preferences/configuration. Startup errors stop the refresh coordinator and display a visible failure. Session failures show a localized manual retry button.
- `src/admin/session.rs`: unchanged Origin matching and `Sec-Fetch-Site` enforcement; 403 from invalid Origin now has bounded structured diagnostics showing method, path (without query), sanitized Origin, Host, expected external scheme/authority, fetch site, trust status, and safe forwarding metadata. No Cookie, Authorization, CSRF token, secret, query parameter or untrusted forwarded Host value is logged. Browser document navigation requesting HTML receives a standalone 403 error/retry page; API responses remain JSON.
- Localization (`en_US`, `pl_pl`), versioned panel assets, developer checks and release identity were updated. No schema/config/proxy data-plane or public route change.
## Test matrix
| Scenario | Source simulation | Live acceptance still needed |
| --- | --- | --- |
| Visibility + BFCache in one event sequence | One `/api/auth/session` check | iOS Safari / Android Chrome |
| 403 during resume | No automatic reload; error screen; paused SSE and tasks | Capture browser headers and proxy forwarding |
| Manual retry after temporary rejection | Single recheck; restored shell/SSE on success | Network interruption/recovery |
| HTTP 401 | Existing lock/login behavior | Expired/revoked real session |
| Pagehide/restore, 10 repeated cycles | One check per return; no leaked event handlers or parallel SSE | Real BFCache lifecycle |
| Stale session response after logout | Ignored without restoring session state | Concurrent logout/restore |
| Startup failure | Visible error, recovery marker retained | Missing JS or language/config response |
| Foreign Origin / cross-site | Rust regression test added, exact checks retained | Real server test with Rust/Cargo |
| Document navigation blocked by Origin | Rust regression test added, 403 HTML response | Mobile top-level navigation |
The deterministic browser suite uses actual application code and a simulated API. It is *not* a real browser, reverse proxy, or compiled Rust backend test. `Cargo.lock` comes from the prior 0.31.2 candidate, remains unresolved against the current dependency constraints and must be generated by a real Rust 1.99+ Cargo resolution. Do not deploy on the basis of static/source-only testing.
## Release-host validation
```bash
bash scripts/resolve_dependencies_0312.sh
./dev.sh check
./dev.sh test
./dev.sh release
(cd docker-agent && go test ./...)
```
Then verify on a real administrator endpoint with correct trusted reverse-proxy forwarding. Compare the external scheme/authority with `Origin`, `Host`, `Sec-Fetch-Site` and **only trusted** `X-Forwarded-Host` / `X-Forwarded-Proto` in the diagnostic log. Test at least 20 return-to-tab cycles on actual mobile browsers, including BFCache, session expiration, temporary 403 responses and recovery. Confirm that disallowed origins still return 403 and that polling/SSE do not grow across cycles. Do not turn off Origin/CSRF or loosen forwarded-header trust to hide the symptom.
+2 -1
View File
@@ -31,6 +31,7 @@ This is the canonical index for the current project documentation. The authentic
- [0.26.3 implementation note](V0.26.3_IMPLEMENTATION.md) - global interface scaling, chart themes, square draft dock and Settings navigation polish. - [0.26.3 implementation note](V0.26.3_IMPLEMENTATION.md) - global interface scaling, chart themes, square draft dock and Settings navigation polish.
- [0.26.2 implementation note](V0.26.2_IMPLEMENTATION.md) - unified Logs navigation, field-query search, dynamic JSON columns and RAW parsing. - [0.26.2 implementation note](V0.26.2_IMPLEMENTATION.md) - unified Logs navigation, field-query search, dynamic JSON columns and RAW parsing.
- [0.26.1 implementation note](V0.26.1_IMPLEMENTATION.md) - historical vhost-aware protocol logging and Stored Logs UX milestone. - [0.26.1 implementation note](V0.26.1_IMPLEMENTATION.md) - historical vhost-aware protocol logging and Stored Logs UX milestone.
- [0.31.3 mobile session / invalid-Origin recovery](IMPLEMENTATION_0.31.3.md) - deterministic BFCache and visibility session coordination, 403 diagnostics, recovery and remaining acceptance.
- [0.31.1 mobile panel bootstrap](IMPLEMENTATION_0.31.1.md) - panel script readiness, versioned assets, failure recovery and acceptance tests. - [0.31.1 mobile panel bootstrap](IMPLEMENTATION_0.31.1.md) - panel script readiness, versioned assets, failure recovery and acceptance tests.
- [0.31.0 security audit](SECURITY_AUDIT_0.31.0.md) - current API/panel/BAN/PURGE attack-surface review, hardening and remaining runtime verification. - [0.31.0 security audit](SECURITY_AUDIT_0.31.0.md) - current API/panel/BAN/PURGE attack-surface review, hardening and remaining runtime verification.
- [0.31.0 implementation report](IMPLEMENTATION_0.31.0.md) - consolidated changes, exact file inventory, validation results and outstanding Rust runtime acceptance. - [0.31.0 implementation report](IMPLEMENTATION_0.31.0.md) - consolidated changes, exact file inventory, validation results and outstanding Rust runtime acceptance.
@@ -81,7 +82,7 @@ This is the canonical index for the current project documentation. The authentic
## Historical or version-labelled context ## Historical or version-labelled context
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [IMPLEMENTATION_0.31.1.md](IMPLEMENTATION_0.31.1.md); [IMPLEMENTATION_0.31.0.md](IMPLEMENTATION_0.31.0.md) is retained as the previous milestone; [V0.29.1_IMPLEMENTATION.md](V0.29.1_IMPLEMENTATION.md) is retained as historical context, and [V0.29.0_IMPLEMENTATION.md](V0.29.0_IMPLEMENTATION.md) is retained as the prior redirect/health/cache/log milestone; [V0.28.2_IMPLEMENTATION.md](V0.28.2_IMPLEMENTATION.md) is retained as the prior certificate-hardening milestone; [V0.28.1_IMPLEMENTATION.md](V0.28.1_IMPLEMENTATION.md) is retained as the prior external-path milestone; [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md) is retained as the prior GeoIP/listener/cache milestone, and [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md) is retained as the prior HTTP/3/cache-maintenance milestone, [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability. Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [IMPLEMENTATION_0.31.3.md](IMPLEMENTATION_0.31.3.md); [IMPLEMENTATION_0.31.1.md](IMPLEMENTATION_0.31.1.md) is retained as the prior mobile milestone; [IMPLEMENTATION_0.31.0.md](IMPLEMENTATION_0.31.0.md) is retained as the previous milestone; [V0.29.1_IMPLEMENTATION.md](V0.29.1_IMPLEMENTATION.md) is retained as historical context, and [V0.29.0_IMPLEMENTATION.md](V0.29.0_IMPLEMENTATION.md) is retained as the prior redirect/health/cache/log milestone; [V0.28.2_IMPLEMENTATION.md](V0.28.2_IMPLEMENTATION.md) is retained as the prior certificate-hardening milestone; [V0.28.1_IMPLEMENTATION.md](V0.28.1_IMPLEMENTATION.md) is retained as the prior external-path milestone; [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md) is retained as the prior GeoIP/listener/cache milestone, and [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md) is retained as the prior HTTP/3/cache-maintenance milestone, [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability.
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above. Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above.
+13 -1
View File
@@ -1,4 +1,16 @@
# Verification report - RustProxy 0.31.2 # Verification report - RustProxy 0.31.3
## 0.31.3 - invalid-Origin resume and mobile panel recovery (2026-10-11)
- **Source:** the uploaded RustProxy 0.31.2 ZIP plus the user's case description. No remote repository/history or real mobile network capture was provided.
- **Reproduced in the original source:** two overlapping session validations; a 403 during return-to-tab caused `location.reload()`; a concurrent successful validation cleared the recovery marker; another 403 could reload again. Separately, refresh tasks and SSE restarted before a resumed session was validated.
- **Corrections:** single-flight session verification and coalesced visibility/BFCache events; suspend polling and SSE until verified; visible localized retry on 403/network errors; unchanged login handling on 401; clear the startup recovery marker only after full initialization; stop refresh on startup failure. Invalid-Origin top-level HTML navigation returns an explicit 403 error page rather than raw JSON. Server Origin/CSRF/cross-site enforcement is unchanged; structured diagnostic logging redacts credentials and untrusted forwarding data.
- **Regression coverage:** `tools/checks/origin_resume_0313_ui_test.mjs` exercises actual JavaScript source against deterministic session API outcomes, repeated tab cycles, stale auth epoch, SSE suspension/resume, retry, 401 and startup marker. Rust unit/integration tests were added for blocked foreign origins, safe diagnostic values and readable 403 document handling, but cannot run here without Cargo.
- **PASS (2026-10-11):** `tools/checks/origin_resume_0313_ui_test.mjs` and all 11 other bundled Node UI tests; actual assembled dashboard bundle parsed by Node; original-source Node reproduction recorded the duplicate request/reload race.
- **PASS (2026-10-11):** source/module/version/security/dependency contracts, SQLite schema-9 migration and upgrade smoke (`release_smoke.py`), documentation/Reader (`docs_check.py`), language packs (`langs_check.py`), shell syntax and Go Docker Agent `go test ./...`. Memory workload simulated 300,000 PURGEs; 2,000 HTTP requests targeted a **Python test peer**, not the RustProxy executable.
- **NOT VERIFIED:** `./dev.sh check` exits 127 because `cargo`/`rustc` are absent. `./dev.sh test` passed all Rust-independent stages through docs/locales and reached the same missing-Cargo gate; it did not run Rust unit tests. No compiled RustProxy, iOS/Android browser, trusted reverse-proxy trace or production rollout was exercised.
- **Verification limit:** the new Rust tests are written but unexecuted; the lockfile is an inherited dependency snapshot, not a resolved 0.31.3 graph. The code has not passed a compiler-enabled release gate.
- **Release gate remains OPEN:** the local environment has no rustc/cargo/rustfmt. The inherited lockfile is unresolved for 0.31.2/0.31.3 dependency families. Run Cargo resolution, fmt, clippy, Rust tests and real admin/reverse-proxy/mobile acceptance on a Rust 1.99+ host before production release.
## 0.31.2 — stable library upgrade candidate (2026-10-10) ## 0.31.2 — stable library upgrade candidate (2026-10-10)
Regular → Executable
View File
File mode changed.
+2
View File
@@ -2323,6 +2323,8 @@
"startupSlow": "The panel is taking longer than expected. Check your connection and try again.", "startupSlow": "The panel is taking longer than expected. Check your connection and try again.",
"startupRetry": "Reload panel", "startupRetry": "Reload panel",
"startupFailed": "Could not load RustProxy", "startupFailed": "Could not load RustProxy",
"sessionValidationFailed": "Could not verify your session",
"sessionRetry": "Retry connection",
"panelCompressionTitle": "Panel content compression", "panelCompressionTitle": "Panel content compression",
"panelCompressionHelp": "Compressed HTML, scripts, styles and language files. Browser negotiation: Zstd → Brotli → gzip at equal quality. Disable all for identity. Takes effect immediately; a reverse proxy may change Accept-Encoding or re-encode responses.", "panelCompressionHelp": "Compressed HTML, scripts, styles and language files. Browser negotiation: Zstd → Brotli → gzip at equal quality. Disable all for identity. Takes effect immediately; a reverse proxy may change Accept-Encoding or re-encode responses.",
"cacheHttpInvalidationCurlLabel": "How to send BAN and PURGE with curl", "cacheHttpInvalidationCurlLabel": "How to send BAN and PURGE with curl",
+2
View File
@@ -2323,6 +2323,8 @@
"startupSlow": "Wczytywanie trwa dłużej niż zwykle. Sprawdź połączenie i spróbuj ponownie.", "startupSlow": "Wczytywanie trwa dłużej niż zwykle. Sprawdź połączenie i spróbuj ponownie.",
"startupRetry": "Wczytaj panel ponownie", "startupRetry": "Wczytaj panel ponownie",
"startupFailed": "Nie udało się uruchomić RustProxy", "startupFailed": "Nie udało się uruchomić RustProxy",
"sessionValidationFailed": "Nie udało się sprawdzić sesji",
"sessionRetry": "Ponów połączenie",
"panelCompressionTitle": "Kompresja zasobów panelu", "panelCompressionTitle": "Kompresja zasobów panelu",
"panelCompressionHelp": "Obejmuje HTML, skrypty, style i języki. Negocjacja z przeglądarką: Zstd → Brotli → gzip przy równej jakości. Odznacz wszystkie, by wyłączyć. Działa od razu; reverse proxy może zmieniać Accept-Encoding lub kodowanie odpowiedzi.", "panelCompressionHelp": "Obejmuje HTML, skrypty, style i języki. Negocjacja z przeglądarką: Zstd → Brotli → gzip przy równej jakości. Odznacz wszystkie, by wyłączyć. Działa od razu; reverse proxy może zmieniać Accept-Encoding lub kodowanie odpowiedzi.",
"cacheHttpInvalidationCurlLabel": "Jak wywołać BAN i PURGE poleceniem curl", "cacheHttpInvalidationCurlLabel": "Jak wywołać BAN i PURGE poleceniem curl",
+2 -2
View File
@@ -8,8 +8,8 @@ if ! command -v cargo >/dev/null 2>&1 || ! command -v rustc >/dev/null 2>&1; the
exit 127 exit 127
fi fi
python3 tools/checks/preflight.py python3 tools/checks/preflight.py
if [[ "$(python3 scripts/version.py)" != '0.31.2' ]]; then if [[ "$(python3 scripts/version.py)" != '0.31.3' ]]; then
printf 'Expected RustProxy 0.31.2 sources.\n' >&2 printf 'Expected RustProxy 0.31.3 sources.\n' >&2
exit 1 exit 1
fi fi
printf 'Toolchain: '; rustc --version printf 'Toolchain: '; rustc --version
+1
View File
@@ -244,6 +244,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
"guides", "guides",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/STREAMS.md")), include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/STREAMS.md")),
), ),
reader_doc!("docs/IMPLEMENTATION_0.31.3.md", "technical"),
reader_doc!("docs/IMPLEMENTATION_0.31.1.md", "technical"), reader_doc!("docs/IMPLEMENTATION_0.31.1.md", "technical"),
reader_doc!("docs/IMPLEMENTATION_0.31.0.md", "technical"), reader_doc!("docs/IMPLEMENTATION_0.31.0.md", "technical"),
( (
+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3", "openapi": "3.0.3",
"info": { "info": {
"title": "RustProxy Administration API", "title": "RustProxy Administration API",
"version": "0.31.2", "version": "0.31.3",
"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." "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": [ "servers": [
+1
View File
@@ -35,6 +35,7 @@ fn admin_app_javascript() -> &'static str {
static ASSET: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| { static ASSET: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
[ [
include_str!("web/app/core.js"), include_str!("web/app/core.js"),
include_str!("web/app/session-resume.js"),
include_str!("web/app/refresh.js"), include_str!("web/app/refresh.js"),
include_str!("web/app/preferences.js"), include_str!("web/app/preferences.js"),
include_str!("web/app/config-history.js"), include_str!("web/app/config-history.js"),
+112 -3
View File
@@ -1,3 +1,108 @@
// Only log browser origin metadata: never cookies, CSRF tokens, Authorization,
// query strings, or untrusted forwarding data. Malformed origins are redacted.
fn diagnostic_admin_authority(value: &str) -> &str {
if value.len() <= 255 && !value.contains('@')
&& value.parse::<http::uri::Authority>().is_ok()
{
value
} else {
"(invalid)"
}
}
fn diagnostic_admin_origin(headers: &HeaderMap) -> &str {
if !headers.contains_key(ORIGIN) {
return "(absent)";
}
let Some(value) = single_header(headers, ORIGIN.as_str()) else {
return "(invalid)";
};
if value == "null" {
return "null";
}
if value.len() > 255 || value.contains('@') || value.contains('#') {
return "(invalid)";
}
let Ok(uri) = value.parse::<http::Uri>() else {
return "(invalid)";
};
if !matches!(uri.scheme_str(), Some("http" | "https"))
|| uri.query().is_some()
|| !matches!(uri.path(), "" | "/")
|| uri.authority().is_none()
{
return "(invalid)";
}
value
}
fn diagnostic_forwarded_header<'a>(headers: &'a HeaderMap, name: &str, trusted: bool) -> &'a str {
if !headers.contains_key(name) {
return "(absent)";
}
if !trusted {
return "(untrusted)";
}
let Some(value) = single_header(headers, name) else {
return "(invalid)";
};
if name == "x-forwarded-proto" {
if matches!(value, "http" | "https") { value } else { "(invalid)" }
} else {
diagnostic_admin_authority(value)
}
}
fn invalid_admin_origin(
request: &Request,
reason: &'static str,
scheme: Option<&str>,
authority: Option<&str>,
trusted_host: bool,
trusted_proto: bool,
) -> Response {
let headers = request.headers();
let path = request.uri().path();
let path = if path.len() <= 160 { path } else { "(long_path)" };
let browser_host = single_header(headers, HOST.as_str())
.map(diagnostic_admin_authority)
.unwrap_or("(absent_or_duplicate)");
let browser_fetch_site = match single_header(headers, "sec-fetch-site") {
None => "(absent)",
Some("same-origin" | "same-site" | "cross-site" | "none") =>
single_header(headers, "sec-fetch-site").unwrap_or("(invalid)"),
_ => "(invalid)",
};
tracing::warn!(
reason,
method = %request.method(),
path,
browser_origin = diagnostic_admin_origin(headers),
browser_host,
expected_scheme = scheme.unwrap_or("(unknown)"),
expected_authority = authority.map(diagnostic_admin_authority).unwrap_or("(unknown)"),
browser_fetch_site,
trusted_forwarded_host = trusted_host,
trusted_forwarded_proto = trusted_proto,
forwarded_host = diagnostic_forwarded_header(headers, "x-forwarded-host", trusted_host),
forwarded_proto = diagnostic_forwarded_header(headers, "x-forwarded-proto", trusted_proto),
"administration origin rejected"
);
// When a browser navigates to the dashboard document, JSON here would
// replace the entire page with a raw API error. Keep the 403 (no shell or
// protected data) but present a standalone recovery message instead.
let wants_html = request.headers().get(http::header::ACCEPT)
.and_then(|header| header.to_str().ok())
.is_some_and(|value| value.split(',').any(|entry| entry.trim_start().starts_with("text/html")));
if request.method() == Method::GET && panel_document_path(request.uri().path()) && wants_html {
return secure((
StatusCode::FORBIDDEN,
axum::response::Html("<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>RustProxy - Access blocked</title><h1>RustProxy - Could not open the panel</h1><p>The request origin was rejected. Check the proxy/URL and try again.</p><p><a href=\"\">Reload panel</a></p></html>"),
).into_response());
}
secure(reject(StatusCode::FORBIDDEN, "auth.invalid_origin"))
}
async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response { async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response {
let transport = request let transport = request
.extensions() .extensions()
@@ -10,9 +115,13 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
Err(()) => return secure(reject(StatusCode::FORBIDDEN, "auth.invalid_host")), Err(()) => return secure(reject(StatusCode::FORBIDDEN, "auth.invalid_host")),
}; };
let headers = request.headers(); let headers = request.headers();
let trusted_host = trusted_admin_proxy(&state, peer);
let trusted_proto = trusted_admin_forwarded_proto(&state, peer);
let scheme = match external_admin_scheme(&state, headers, peer, transport) { let scheme = match external_admin_scheme(&state, headers, peer, transport) {
Ok(value) => value, Ok(value) => value,
Err(()) => return secure(reject(StatusCode::FORBIDDEN, "auth.invalid_origin")), Err(()) => {
return invalid_admin_origin(&request, "external_scheme", None, None, trusted_host, trusted_proto);
}
}; };
let external_authority = let external_authority =
match external_admin_authority(&state, headers, peer, &raw_host, scheme == "https") { match external_admin_authority(&state, headers, peer, &raw_host, scheme == "https") {
@@ -26,13 +135,13 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
.is_none_or(|origin| !admin_origin_matches(origin, scheme, &external_authority)) .is_none_or(|origin| !admin_origin_matches(origin, scheme, &external_authority))
}) })
{ {
return secure(reject(StatusCode::FORBIDDEN, "auth.invalid_origin")); return invalid_admin_origin(&request, "origin_mismatch", Some(scheme), Some(&external_authority), trusted_host, trusted_proto);
} }
if headers if headers
.get("sec-fetch-site") .get("sec-fetch-site")
.is_some_and(|h| h == "cross-site") .is_some_and(|h| h == "cross-site")
{ {
return secure(reject(StatusCode::FORBIDDEN, "auth.invalid_origin")); return invalid_admin_origin(&request, "cross_site_fetch", Some(scheme), Some(&external_authority), trusted_host, trusted_proto);
} }
let path = request.uri().path().to_owned(); let path = request.uri().path().to_owned();
let public = anonymous_allowed(request.method(), &path); let public = anonymous_allowed(request.method(), &path);
+63
View File
@@ -253,6 +253,69 @@
)); ));
} }
#[test]
fn admin_origin_diagnostics_redact_non_origin_values() {
let mut headers = HeaderMap::new();
assert_eq!(diagnostic_admin_origin(&headers), "(absent)");
headers.insert(ORIGIN, HeaderValue::from_static("https://panel.example.test"));
assert_eq!(diagnostic_admin_origin(&headers), "https://panel.example.test");
headers.insert(ORIGIN, HeaderValue::from_static("https://user:password@panel.example.test"));
assert_eq!(diagnostic_admin_origin(&headers), "(invalid)");
headers.insert(ORIGIN, HeaderValue::from_static("https://panel.example.test/?token=secret"));
assert_eq!(diagnostic_admin_origin(&headers), "(invalid)");
headers.insert("x-forwarded-host", HeaderValue::from_static("other.example.test"));
assert_eq!(diagnostic_forwarded_header(&headers, "x-forwarded-host", false), "(untrusted)");
assert_eq!(diagnostic_forwarded_header(&headers, "x-forwarded-host", true), "other.example.test");
}
#[tokio::test]
async fn unauthorized_browser_origins_remain_rejected_for_session_checks() {
let (router, store, _dir, secret, _) = fixture().await;
for (origin, fetch_site) in [
(Some("https://attacker.example"), None),
(Some("null"), None),
(None, Some("cross-site")),
] {
let mut request = Request::builder()
.uri("/api/auth/session")
.header("host", "127.0.0.1:9090")
.header("cookie", format!("{}={secret}", auth::COOKIE_NAME));
if let Some(origin) = origin { request = request.header(ORIGIN, origin); }
if let Some(site) = fetch_site { request = request.header("sec-fetch-site", site); }
let response = router.clone().oneshot(request.body(Body::empty()).unwrap()).await.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
// Origin on an ordinary same-origin GET may be absent, or may match.
for origin in [None, Some("http://127.0.0.1:9090")] {
let mut request = Request::builder()
.uri("/api/auth/session")
.header("host", "127.0.0.1:9090")
.header("cookie", format!("{}={secret}", auth::COOKIE_NAME));
if let Some(origin) = origin { request = request.header(ORIGIN, origin); }
assert_eq!(router.clone().oneshot(request.body(Body::empty()).unwrap()).await.unwrap().status(), StatusCode::OK);
}
store.close().await;
}
#[tokio::test]
async fn invalid_origin_on_document_is_visible_but_still_forbidden() {
let (router, store, _dir, _secret, _) = fixture().await;
let response = router.oneshot(
Request::builder()
.uri("/overview")
.header("host", "127.0.0.1:9090")
.header("origin", "null")
.header("accept", "text/html,application/xhtml+xml")
.body(Body::empty())
.unwrap(),
).await.unwrap();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
assert!(response.headers().get(http::header::CONTENT_TYPE).unwrap().to_str().unwrap().starts_with("text/html"));
let body = axum::body::to_bytes(response.into_body(), 4096).await.unwrap();
assert!(String::from_utf8_lossy(&body).contains("Could not open the panel"));
store.close().await;
}
#[tokio::test] #[tokio::test]
async fn trusted_https_reverse_proxy_accepts_external_origin_and_refreshes_secure_cookie() { async fn trusted_https_reverse_proxy_accepts_external_origin_and_refreshes_secure_cookie() {
let mut config = Config::default(); let mut config = Config::default();
+6 -28
View File
@@ -25,7 +25,7 @@ function panelFromLocation() { if (typeof location === "undefined") return "over
let envelope = null, busy = false, connected = false, currentPanel = panelFromLocation(), previous = null; let envelope = null, busy = false, connected = false, currentPanel = panelFromLocation(), previous = null;
let samples = [], protocolSamples = { h1: [], h2: [], h3: [] }, websocketProtocolSamples = { h1: [], h2: [], h3: [] }, transformSamples = [], toastTimer, editingBackend = null, editingRoute = null; let samples = [], protocolSamples = { h1: [], h2: [], h3: [] }, websocketProtocolSamples = { h1: [], h2: [], h3: [] }, transformSamples = [], toastTimer, editingBackend = null, editingRoute = null;
let liveEvents = null, liveConnected = false, liveFallback = false, liveInterval = null, authEpoch = 0, pendingAuthDestination = null; let liveEvents = null, liveConnected = false, liveFallback = false, liveInterval = null, authEpoch = 0, pendingAuthDestination = null;
let lastApiLatencyMs = null; let lastApiLatencyMs = null, sessionCheckPromise = null;
let adminAuthChannel = null; try { adminAuthChannel = new BroadcastChannel("rustproxy-admin-session"); adminAuthChannel.addEventListener("message", event => { if (event.data === "locked" && authenticated) lock(false); }); } catch { } let adminAuthChannel = null; try { adminAuthChannel = new BroadcastChannel("rustproxy-admin-session"); adminAuthChannel.addEventListener("message", event => { if (event.data === "locked" && authenticated) lock(false); }); } catch { }
// Language content is discovered by build.rs from langs/*.json, never compiled // Language content is discovered by build.rs from langs/*.json, never compiled
// into a two-language switch here. Locale metadata also drives Intl and RTL. // into a two-language switch here. Locale metadata also drives Intl and RTL.
@@ -388,7 +388,7 @@ async function activate(info) {
document.documentElement.classList.remove("app-startup-failed"); document.documentElement.classList.remove("app-startup-failed");
rustProxyRefresh.start(); rustProxyRefresh.start();
const preferences = await loadAdminPreferences(); const preferences = await loadAdminPreferences();
if (!authenticated || epoch !== authEpoch) return; if (!authenticated || epoch !== authEpoch) return false;
window.rustProxyLogs?.applyPreferences(preferences); window.rustProxyLogs?.applyPreferences(preferences);
window.rustProxyFileLogs?.applyPreferences(preferences); window.rustProxyFileLogs?.applyPreferences(preferences);
window.rustProxyVhosts?.applyPreferences(preferences); window.rustProxyVhosts?.applyPreferences(preferences);
@@ -399,17 +399,18 @@ async function activate(info) {
for (let attempt = 0; attempt < 3 && authenticated && epoch === authEpoch; attempt++) { for (let attempt = 0; attempt < 3 && authenticated && epoch === authEpoch; attempt++) {
try { await loadConfig(); break; } try { await loadConfig(); break; }
catch (error) { catch (error) {
if (!authenticated || epoch !== authEpoch || error?.status === 401) return; if (!authenticated || epoch !== authEpoch || error?.status === 401) return false;
if (attempt === 2) { showStartupFailure(error); return; } if (attempt === 2) { showStartupFailure(error); return false; }
await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 400)); await new Promise(resolve => setTimeout(resolve, (attempt + 1) * 400));
} }
} }
if (!authenticated || epoch !== authEpoch) return; if (!authenticated || epoch !== authEpoch) return false;
// A single full snapshot seeds non-live diagnostics; subsequent updates // A single full snapshot seeds non-live diagnostics; subsequent updates
// continue over SSE rather than polling full status on a timer. // continue over SSE rather than polling full status on a timer.
void refresh(true).catch(error => { void refresh(true).catch(error => {
if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true); if (authenticated && epoch === authEpoch && error?.status !== 401) toast(error.message, true);
}); });
return true;
} }
async function loadAudit() { if (!authenticated) return; const epoch = authEpoch; try { const data = await api("/api/audit"); if (!authenticated || epoch !== authEpoch) return; const rows = data.entries.map(item => { const tr = el("tr"); for (const value of [formatDateTime(item.time * 1000), item.actor, item.action, item.detail]) { const td = el("td", value); td.title = value; tr.append(td); } return tr; }); $("audit-rows").replaceChildren(...rows); $("empty-audit").hidden = rows.length > 0; } catch (e) { toast(e.message, true); } } async function loadAudit() { if (!authenticated) return; const epoch = authEpoch; try { const data = await api("/api/audit"); if (!authenticated || epoch !== authEpoch) return; const rows = data.entries.map(item => { const tr = el("tr"); for (const value of [formatDateTime(item.time * 1000), item.actor, item.action, item.detail]) { const td = el("td", value); td.title = value; tr.append(td); } return tr; }); $("audit-rows").replaceChildren(...rows); $("empty-audit").hidden = rows.length > 0; } catch (e) { toast(e.message, true); } }
function tunnelMessage(value = "", error = false) { const node = $("tunnels-status"); if (!node) return; node.textContent = value; node.classList.toggle("error", !!error); } function tunnelMessage(value = "", error = false) { const node = $("tunnels-status"); if (!node) return; node.textContent = value; node.classList.toggle("error", !!error); }
@@ -475,26 +476,3 @@ $("tunnels-dialog").addEventListener("close", stopTunnelRefresh);
$("config-editor").addEventListener("input", () => setSettingsDirty("config-editor")); $("config-editor").addEventListener("input", () => setSettingsDirty("config-editor"));
$("save-json").addEventListener("click", async event => { const editor = $("config-editor"), page = editor.closest("[data-settings-page]"); if (!envelope || busy) return; try { await withFormSubmitState(page, event.currentTarget, async () => { let config; try { config = JSON.parse(editor.value); } catch { throw fieldError(editor, t("vhJsonInvalid")); } if (await save(config)) setSettingsDirty("config-editor", false); }); } catch (e) { toast(e.message, true); } }); $("save-json").addEventListener("click", async event => { const editor = $("config-editor"), page = editor.closest("[data-settings-page]"); if (!envelope || busy) return; try { await withFormSubmitState(page, event.currentTarget, async () => { let config; try { config = JSON.parse(editor.value); } catch { throw fieldError(editor, t("vhJsonInvalid")); } if (await save(config)) setSettingsDirty("config-editor", false); }); } catch (e) { toast(e.message, true); } });
$("export-config").addEventListener("click", () => { const url = URL.createObjectURL(new Blob([JSON.stringify(envelope.config, null, 2) + "\n"], { type: "application/json" })); const a = el("a"); a.href = url; a.download = `RustProxy-config-r${envelope.revision}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); }); $("export-config").addEventListener("click", () => { const url = URL.createObjectURL(new Blob([JSON.stringify(envelope.config, null, 2) + "\n"], { type: "application/json" })); const a = el("a"); a.href = url; a.download = `RustProxy-config-r${envelope.revision}.json`; a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000); });
async function revalidateSession() {
const epoch = authEpoch;
try {
const info = await api("/api/auth/session");
clearInvalidOriginRecovery();
if (epoch !== authEpoch) return;
if (!authenticated) { await activate(info); return; }
sessionInfo = info; csrf = info.csrf_token; renderSession(); window.rustProxyRefresh?.sync();
} catch (error) { if (recoverInvalidOrigin(error)) return; if (error.status !== 401) toast(error.message, true); }
}
window.addEventListener("pageshow", event => { if (event.persisted) void revalidateSession(); });
function showStartupFailure(error) {
// Never expose a blank page or an incomplete workspace after startup fails.
// The authenticated shell keeps its own retry link; 401 still redirects to login.
$("login").hidden = true;
$("workspace").hidden = true;
$("logout").hidden = !authenticated;
document.documentElement.classList.add("app-loading", "app-startup-failed");
$("app-startup-title").textContent = languagePacks.size ? t("startupFailed") : "Could not load RustProxy";
const detail = $("app-startup-detail");
detail.textContent = error?.message || String(error);
detail.hidden = false;
}
+49 -13
View File
@@ -21,9 +21,10 @@ const rustProxyRefresh = (() => {
const tasks = new Map(); const tasks = new Map();
let ready = false, options = { ...REFRESH_DEFAULTS }, panel = currentPanel; let ready = false, options = { ...REFRESH_DEFAULTS }, panel = currentPanel;
let settingsTab = "listeners", watchingVisibility = false; let settingsTab = "listeners", watchingVisibility = false;
let sessionReady = true, visibleResumeHandled = false;
const visible = () => !document.hidden; const visible = () => !document.hidden;
function allowed(task) { function allowed(task) {
return ready && authenticated && visible() && options.live_enabled && return ready && authenticated && sessionReady && visible() && options.live_enabled &&
task.interval() > 0 && (!task.active || task.active()); task.interval() > 0 && (!task.active || task.active());
} }
function clear(task, abort = false) { function clear(task, abort = false) {
@@ -39,7 +40,7 @@ const rustProxyRefresh = (() => {
task.timer = setTimeout(() => { task.timer = null; void execute(task, false); }, delay); task.timer = setTimeout(() => { task.timer = null; void execute(task, false); }, delay);
} }
function execute(task, manual) { function execute(task, manual) {
if (!authenticated || (!manual && !allowed(task))) return Promise.resolve(); if (!authenticated || !sessionReady || (!manual && !allowed(task))) return Promise.resolve();
if (task.inFlight) { if (task.inFlight) {
if (manual) task.queued = true; if (manual) task.queued = true;
return task.inFlight; return task.inFlight;
@@ -92,22 +93,56 @@ const rustProxyRefresh = (() => {
} }
task.seconds = seconds; task.seconds = seconds;
} }
if (ready && authenticated && visible() && options.live_enabled) startLive(); if (ready && authenticated && sessionReady && visible() && options.live_enabled) startLive();
else stopLive(); else stopLive();
} }
function onVisibility() { sync(); if (visible() && ready && authenticated) void revalidateSession(); } function suspendForSessionValidation() {
function start() { sessionReady = false;
ready = true; panel = currentPanel;
settingsTab = window.rustProxySettings?.current?.() || "listeners";
if (!watchingVisibility) { document.addEventListener("visibilitychange", onVisibility); watchingVisibility = true; }
sync();
}
function stop() {
ready = false;
for (const task of tasks.values()) { for (const task of tasks.values()) {
clear(task, true); task.queued = false; task.enabled = false; clear(task, true); task.queued = false; task.enabled = false;
} }
if (watchingVisibility) { document.removeEventListener("visibilitychange", onVisibility); watchingVisibility = false; } sync();
}
function completeSessionValidation(success) {
sessionReady = !!success;
sync();
}
function requestSessionResume() {
if (!ready || !authenticated || !visible() || visibleResumeHandled) return;
visibleResumeHandled = true;
void revalidateSession();
}
function onVisibility() {
if (visible()) requestSessionResume();
else { visibleResumeHandled = false; sync(); }
}
function onPageHide() {
visibleResumeHandled = false;
suspendForSessionValidation();
}
function onPageShow(event) { if (event.persisted) requestSessionResume(); }
function start() {
ready = true; sessionReady = true; visibleResumeHandled = false; panel = currentPanel;
settingsTab = window.rustProxySettings?.current?.() || "listeners";
if (!watchingVisibility) {
document.addEventListener("visibilitychange", onVisibility);
window.addEventListener("pagehide", onPageHide);
window.addEventListener("pageshow", onPageShow);
watchingVisibility = true;
}
sync();
}
function stop() {
ready = false; sessionReady = false; visibleResumeHandled = false;
for (const task of tasks.values()) {
clear(task, true); task.queued = false; task.enabled = false;
}
if (watchingVisibility) {
document.removeEventListener("visibilitychange", onVisibility);
window.removeEventListener("pagehide", onPageHide);
window.removeEventListener("pageshow", onPageShow);
watchingVisibility = false;
}
stopLive(); stopLive();
} }
function update(value) { options = normalizeRefreshPreferences(value); sync(); } function update(value) { options = normalizeRefreshPreferences(value); sync(); }
@@ -151,6 +186,7 @@ const rustProxyRefresh = (() => {
register("tunnels", { interval: () => 2, active: () => !!$("tunnels-dialog")?.open, register("tunnels", { interval: () => 2, active: () => !!$("tunnels-dialog")?.open,
run: async context => { await loadTunnels(context); } }); run: async context => { await loadTunnels(context); } });
return { register, manual, start, stop, sync, update, setPanel, setSettingsTab, return { register, manual, start, stop, sync, update, setPanel, setSettingsTab,
suspendForSessionValidation, completeSessionValidation,
current: () => ({ ...options }), diagnostics: () => [...tasks.values()].map(t => ({ id: t.id, enabled: t.enabled, inFlight: !!t.inFlight, scheduled: t.timer !== null })) }; current: () => ({ ...options }), diagnostics: () => [...tasks.values()].map(t => ({ id: t.id, enabled: t.enabled, inFlight: !!t.inFlight, scheduled: t.timer !== null })) };
})(); })();
window.rustProxyRefresh = rustProxyRefresh; window.rustProxyRefresh = rustProxyRefresh;
+1 -2
View File
@@ -21,12 +21,11 @@ async function startAdminPanel() {
// app.js belongs to the authenticated shell and must not fall back to its // app.js belongs to the authenticated shell and must not fall back to its
// legacy embedded sign-in form when another startup step fails. // legacy embedded sign-in form when another startup step fails.
const info = await api("/api/auth/session"); const info = await api("/api/auth/session");
clearInvalidOriginRecovery();
await loadLanguages(); await loadLanguages();
setupRequired = false; setupRequired = false;
translate(); translate();
syncAuthForm(); syncAuthForm();
await activate(info); if (await activate(info)) clearInvalidOriginRecovery();
} catch (e) { } catch (e) {
if (recoverInvalidOrigin(e)) return; if (recoverInvalidOrigin(e)) return;
if (e.status === 401) { if (e.status === 401) {
+74
View File
@@ -0,0 +1,74 @@
// One in-flight validation per authenticated document. Neither BFCache restore nor
// visibility resume may trigger a reload: a 403 is a recoverable, visible error.
function revalidateSession() {
if (!authenticated) return Promise.resolve(false);
if (sessionCheckPromise) return sessionCheckPromise;
const epoch = authEpoch;
window.rustProxyRefresh?.suspendForSessionValidation();
const check = (async () => {
try {
const info = await api("/api/auth/session");
if (!authenticated || epoch !== authEpoch) return false;
sessionInfo = info;
csrf = info.csrf_token;
renderSession();
hideSessionValidationFailure();
window.rustProxyRefresh?.completeSessionValidation(true);
return true;
} catch (error) {
// api() locks and navigates to the login document on a 401.
if (!authenticated || epoch !== authEpoch || error?.status === 401) return false;
window.rustProxyRefresh?.completeSessionValidation(false);
showSessionValidationFailure(error);
return false;
}
})();
sessionCheckPromise = check;
void check.then(
() => { if (sessionCheckPromise === check) sessionCheckPromise = null; },
() => { if (sessionCheckPromise === check) sessionCheckPromise = null; }
);
return check;
}
function displayStartupError(error, sessionResume = false) {
$("login").hidden = true;
$("workspace").hidden = true;
$("logout").hidden = !authenticated;
document.documentElement.classList.add("app-loading", "app-startup-failed");
document.documentElement.classList.toggle("app-session-failed", sessionResume);
$("app-startup-title").textContent = sessionResume
? t("sessionValidationFailed")
: (languagePacks.size ? t("startupFailed") : "Could not load RustProxy");
const detail = $("app-startup-detail");
detail.textContent = error?.message || String(error);
detail.hidden = false;
$("app-startup-reload").hidden = sessionResume;
$("app-session-retry").hidden = !sessionResume;
}
function showSessionValidationFailure(error) {
// Keep the workspace inaccessible while its session cannot be verified.
displayStartupError(error, true);
}
function hideSessionValidationFailure() {
if (!document.documentElement.classList.contains("app-session-failed")) return;
document.documentElement.classList.remove("app-session-failed", "app-loading", "app-startup-failed");
$("app-startup-detail").hidden = true;
$("app-startup-detail").textContent = "";
$("app-startup-reload").hidden = false;
$("app-session-retry").hidden = true;
$("workspace").hidden = false;
$("logout").hidden = false;
}
$("app-session-retry").addEventListener("click", () => {
const button = $("app-session-retry");
button.disabled = true;
void revalidateSession().then(
() => { button.disabled = false; },
() => { button.disabled = false; }
);
});
function showStartupFailure(error) {
// All startup failure paths stop background tasks and show visible recovery.
window.rustProxyRefresh?.stop();
displayStartupError(error);
}
+2 -1
View File
@@ -23,7 +23,8 @@
<div class="app-startup-recovery"> <div class="app-startup-recovery">
<p data-i18n="startupSlow">The panel is taking longer than expected. Check your connection and try again.</p> <p data-i18n="startupSlow">The panel is taking longer than expected. Check your connection and try again.</p>
<p id="app-startup-detail" class="app-startup-detail" hidden></p> <p id="app-startup-detail" class="app-startup-detail" hidden></p>
<a class="outline" href="" data-i18n="startupRetry">Reload panel</a> <a class="outline" id="app-startup-reload" href="" data-i18n="startupRetry">Reload panel</a>
<button class="outline" type="button" id="app-session-retry" data-i18n="sessionRetry" hidden>Retry connection</button>
</div> </div>
<noscript>JavaScript is required to use the administration panel.</noscript> <noscript>JavaScript is required to use the administration panel.</noscript>
</div> </div>
+1 -1
View File
@@ -232,7 +232,7 @@ html:not(.app-loading) .app-startup {
animation: app-startup-reveal 0s linear 12s forwards animation: app-startup-reveal 0s linear 12s forwards
} }
.app-startup-recovery .outline { .app-startup-recovery .outline:not([hidden]) {
margin-top: 10px; margin-top: 10px;
display: inline-flex display: inline-flex
} }
+15 -17
View File
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
preflight(root) preflight(root)
manifest = tomllib.loads((root / "Cargo.toml").read_text()) manifest = tomllib.loads((root / "Cargo.toml").read_text())
assert manifest["package"]["version"] == "0.31.2" assert manifest["package"]["version"] == "0.31.3"
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12" assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
h3dep = manifest["dependencies"]["h3"] h3dep = manifest["dependencies"]["h3"]
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.20" and "quinn" in h3dep["features"] 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" lock_path = root / "Cargo.lock"
if lock_path.exists(): if lock_path.exists():
lock = lock_path.read_text() lock = lock_path.read_text()
assert 'name = "rust-proxy"\nversion = "0.31.2"' in lock assert 'name = "rust-proxy"\nversion = "0.31.3"' 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" 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 { for release_doc, marker in {
"README.md": "# RustProxy 0.31.2", "README.md": "# RustProxy 0.31.3",
"CHANGELOG.md": "## 0.31.2", "CHANGELOG.md": "## 0.31.3",
"ROADMAP.md": "## 0.31.2", "ROADMAP.md": "## 0.31.3",
"AGENTS.md": "Current source version: 0.31.2.", "AGENTS.md": "Current source version: 0.31.3.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.31.2", "docs/TEST_REPORT.md": "# Verification report - RustProxy 0.31.3",
}.items(): }.items():
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}" 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" 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 '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
assert '"StreamMetrics"' in openapi and '"streams"' in openapi assert '"StreamMetrics"' in openapi and '"streams"' in openapi
for marker in ['"version": "0.31.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']: for marker in ['"version": "0.31.3"', '"/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 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"']: 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 assert marker in openapi, marker
@@ -542,7 +542,7 @@ for marker in ['"/api/config/{section}"', 'async fn update_section', 'section_up
for route in ['/overview', '/vhosts', '/redirects', '/404-hosts', '/streams', '/acl-profiles', '/configuration', '/tls', '/cache', '/transform', '/logging', '/settings']: for route in ['/overview', '/vhosts', '/redirects', '/404-hosts', '/streams', '/acl-profiles', '/configuration', '/tls', '/cache', '/transform', '/logging', '/settings']:
assert f'.route("{route}", get(public::index))' in admin, route assert f'.route("{route}", get(public::index))' in admin, route
app_js = read_app_js(root) app_js = read_app_js(root)
for marker in ['const PANEL_PATHS', 'history.pushState', 'window.addEventListener("popstate"', 'async function saveSection', 'async function revalidateSession', 'Object.values(PANEL_PATHS), "/configuration"', 'certificateCovers', 'covered_hosts', 'refreshCertificateInventory', 'certificateDetails', 'certificate-domain-pills', 'certificateIssuerName', 'certificateDaysRemaining', 'certificateRenewalStatus', 'recoverInvalidOrigin', 'auth.invalid_origin', 'ORIGIN_RECOVERY_KEY']: for marker in ['const PANEL_PATHS', 'history.pushState', 'window.addEventListener("popstate"', 'async function saveSection', 'function revalidateSession', 'Object.values(PANEL_PATHS), "/configuration"', 'certificateCovers', 'covered_hosts', 'refreshCertificateInventory', 'certificateDetails', 'certificate-domain-pills', 'certificateIssuerName', 'certificateDaysRemaining', 'certificateRenewalStatus', 'recoverInvalidOrigin', 'auth.invalid_origin', 'ORIGIN_RECOVERY_KEY']:
assert marker in app_js, marker assert marker in app_js, marker
for marker in ['.certificate-details', '.certificate-domain-pills', '.certificate-card .pill', '.certificate-operation-card', '.renewal-meta', '#panel-tls .certificate-tabs']: for marker in ['.certificate-details', '.certificate-domain-pills', '.certificate-card .pill', '.certificate-operation-card', '.renewal-meta', '#panel-tls .certificate-tabs']:
assert marker in css, marker assert marker in css, marker
@@ -862,12 +862,10 @@ for marker in [
for forbidden in ["pub struct AllowCookie", "private_or_no_transform"]: for forbidden in ["pub struct AllowCookie", "private_or_no_transform"]:
assert forbidden not in compression_source, forbidden assert forbidden not in compression_source, forbidden
compression_finalize_source = rust_source(root, "src/proxy/upstream_helpers.rs") compression_finalize_source = rust_source(root, "src/proxy/upstream_helpers.rs")
for marker in [ assert "let global_compression = &snapshot.envelope.config.compression" in compression_finalize_source
"let global_compression = &snapshot.envelope.config.compression", for selector in ("frontend", "service"):
"frontend.compression.resolve(global_compression, &compression)", # rustfmt splits member chains across lines; check the expression, not its layout.
"service.compression.resolve(global_compression, &compression)", assert re.search(rf"\b{selector}\s*\.\s*compression\s*\.\s*resolve\(global_compression, &compression\)", compression_finalize_source), selector
]:
assert marker in compression_finalize_source, marker
assert "AllowCookie" not in compression_finalize_source assert "AllowCookie" not in compression_finalize_source
openapi_0270 = json.loads((root / "src/admin/openapi.json").read_text()) openapi_0270 = json.loads((root / "src/admin/openapi.json").read_text())
compression_override_properties = openapi_0270["components"]["schemas"]["CompressionOverrides"]["properties"] compression_override_properties = openapi_0270["components"]["schemas"]["CompressionOverrides"]["properties"]
@@ -1396,7 +1394,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 '"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 '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 'location.pathname === "/logging"' in settings_js_0241
assert '"RuntimeInformation"' in openapi and '"version": "0.31.2"' in openapi assert '"RuntimeInformation"' in openapi and '"version": "0.31.3"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions. # 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs") tls_0241 = rust_source(root, "src/tls.rs")
@@ -1635,5 +1633,5 @@ for marker_0282 in [
assert 'Zewnętrzne ścieżki plików' in (root / 'langs/pl_pl.json').read_text() assert 'Zewnętrzne ścieżki plików' in (root / 'langs/pl_pl.json').read_text()
assert 'setfacl -m u:rustproxy:--x /etc/ssl/' in (root / 'docs/V0.28.2_IMPLEMENTATION.md').read_text() assert 'setfacl -m u:rustproxy:--x /etc/ssl/' in (root / 'docs/V0.28.2_IMPLEMENTATION.md').read_text()
print("PASS: 0.31.2 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.") print("PASS: 0.31.3 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.")
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.") print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
@@ -12,13 +12,14 @@ const html = read('src/admin/web/index.html');
const css = read('src/admin/web/style.css'); const css = read('src/admin/web/style.css');
const core = read('src/admin/web/app/core.js'); const core = read('src/admin/web/app/core.js');
const runtime = read('src/admin/web/app/runtime.js'); const runtime = read('src/admin/web/app/runtime.js');
const resume = read('src/admin/web/app/session-resume.js');
assert.match(html, /<html class="app-loading"/); assert.match(html, /<html class="app-loading"/);
assert.match(html, /id="app-startup"[^>]*role="status"/); assert.match(html, /id="app-startup"[^>]*role="status"/);
assert.match(html, /id="app-startup-title"/); assert.match(html, /id="app-startup-title"/);
assert.match(html, /class="app-startup-spinner"/); assert.match(html, /class="app-startup-spinner"/);
assert.match(html, /class="app-startup-recovery"/); assert.match(html, /class="app-startup-recovery"/);
assert.match(html, /class="outline" href="" data-i18n="startupRetry"/); assert.match(html, /class="outline" id="app-startup-reload" href="" data-i18n="startupRetry"/);
assert.match(html, /<noscript>/); assert.match(html, /<noscript>/);
assert.doesNotMatch(css, /html\.app-loading\s+body\s*\{\s*visibility:\s*hidden/); assert.doesNotMatch(css, /html\.app-loading\s+body\s*\{\s*visibility:\s*hidden/);
assert.match(css, /html\.app-loading \.shell/); assert.match(css, /html\.app-loading \.shell/);
@@ -37,13 +38,13 @@ assert.match(css, /\.page-intro-title-row \.overview-refresh/);
// Exercise the real activate() and failure handler against a small DOM fake. // Exercise the real activate() and failure handler against a small DOM fake.
const activateSource = core.slice(core.indexOf('async function activate(info) {'), core.indexOf('async function loadAudit()')); const activateSource = core.slice(core.indexOf('async function activate(info) {'), core.indexOf('async function loadAudit()'));
const failureSource = core.slice(core.indexOf('function showStartupFailure(error)')); const failureSource = resume.slice(resume.indexOf('function displayStartupError(error, sessionResume = false)'));
assert(activateSource.startsWith('async function activate(info)')); assert(activateSource.startsWith('async function activate(info)'));
assert(failureSource.startsWith('function showStartupFailure(error)')); assert(failureSource.startsWith('function displayStartupError(error, sessionResume = false)'));
const nodes = new Map(); const nodes = new Map();
const node = key => { const node = key => {
if (!nodes.has(key)) nodes.set(key, { hidden: true, value: 'x', textContent: '' }); if (!nodes.has(key)) nodes.set(key, { hidden: true, value: 'x', textContent: '', addEventListener() {} });
return nodes.get(key); return nodes.get(key);
}; };
const classes = new Set(['app-loading']); const classes = new Set(['app-loading']);
@@ -55,6 +56,8 @@ const context = {
document: { documentElement: { classList: { document: { documentElement: { classList: {
add(...keys) { for (const key of keys) classes.add(key); }, add(...keys) { for (const key of keys) classes.add(key); },
remove(...keys) { for (const key of keys) classes.delete(key); }, remove(...keys) { for (const key of keys) classes.delete(key); },
toggle(key, force) { if (force) classes.add(key); else classes.delete(key); },
contains(key) { return classes.has(key); },
} } }, } } },
pendingAuthDestination: null, pendingAuthDestination: null,
authMessage() {}, authMessage() {},
@@ -65,7 +68,7 @@ const context = {
loadConfig: () => { configCount++; return configPromise; }, loadConfig: () => { configCount++; return configPromise; },
refresh: async () => { statusCount++; }, refresh: async () => { statusCount++; },
toast() { throw new Error('Unexpected toast'); }, toast() { throw new Error('Unexpected toast'); },
window: {}, languagePacks: new Map(), t: key => key, window: { rustProxyRefresh: { stop() {} } }, languagePacks: new Map(), t: key => key,
}; };
runInNewContext(`${activateSource}\n${failureSource}`, context); runInNewContext(`${activateSource}\n${failureSource}`, context);
const activating = context.activate({ username: 'admin', csrf_token: 'CSRF' }); const activating = context.activate({ username: 'admin', csrf_token: 'CSRF' });
+3 -3
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Check the 0.31.2 dependency/source migration; optionally require a resolved lock. """Check the 0.31.3 dependency/source migration; optionally require a resolved lock.
This is not a substitute for rustc, cargo metadata or runtime tests. This is not a substitute for rustc, cargo metadata or runtime tests.
""" """
@@ -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') parser.add_argument('--require-lock', action='store_true', help='Require the post-upgrade Cargo.lock produced by cargo update')
args = parser.parse_args() args = parser.parse_args()
manifest = tomllib.loads((ROOT / 'Cargo.toml').read_text()) manifest = tomllib.loads((ROOT / 'Cargo.toml').read_text())
assert manifest['package']['version'] == '0.31.2' assert manifest['package']['version'] == '0.31.3'
assert manifest['package']['rust-version'] == '1.99' assert manifest['package']['rust-version'] == '1.99'
deps = manifest['dependencies'] deps = manifest['dependencies']
expected_requirements = { expected_requirements = {
@@ -78,4 +78,4 @@ if missing:
print(f'PENDING: lockfile still requires cargo update ({details})') print(f'PENDING: lockfile still requires cargo update ({details})')
else: else:
print('PASS: pinned key packages found in post-upgrade lock; cargo --locked verification is still required') print('PASS: pinned key packages found in post-upgrade lock; cargo --locked verification is still required')
print('PASS: 0.31.2 source dependency APIs, manifest, TLS features, toolchain images and Swagger pin (static only)') print('PASS: 0.31.3 source dependency APIs, manifest, TLS features, toolchain images and Swagger pin (static only)')
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Rust-independent documentation/Reader consistency checks for the 0.31.2 release.""" """Rust-independent documentation/Reader consistency checks for the 0.31.3 release."""
from pathlib import Path from pathlib import Path
import re import re
from rust_source import rust_source from rust_source import rust_source
@@ -65,7 +65,7 @@ assert '"/docs/content.json"' in admin
readme = (ROOT / "README.md").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8")
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8") roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert readme.startswith("# RustProxy 0.31.2") assert readme.startswith("# RustProxy 0.31.3")
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme 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 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 assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env node
// Real-source simulation of mobile tab restore, BFCache, Origin 403 and retry.
// This does not replace a real Safari/Chrome session or a compiled Rust backend.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { runInNewContext } from 'node:vm';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
const read = path => readFileSync(resolve(root, path), 'utf8');
const core = read('src/admin/web/app/core.js');
const refresh = read('src/admin/web/app/refresh.js');
const resume = read('src/admin/web/app/session-resume.js');
const runtime = read('src/admin/web/app/runtime.js');
const html = read('src/admin/web/index.html');
const server = read('src/admin/session.rs');
assert.match(html, /id="app-session-retry"[^>]*data-i18n="sessionRetry"/);
assert.match(server, /origin_mismatch/);
assert.match(server, /cross_site_fetch/);
assert.match(server, /administration origin rejected/);
function sourceBetween(source, start, end) {
const a = source.indexOf(start), b = source.indexOf(end, a);
assert(a >= 0 && b > a, `Missing source slice: ${start}`);
return source.slice(a, b);
}
const actualApi = sourceBetween(core, 'function apiArea(path)', 'function setPanel(name');
const actualLive = sourceBetween(core, 'function renderLiveStatus(value)', 'async function refreshCertificateInventory()');
const actualResume = resume;
const recovery = sourceBetween(core, 'const ORIGIN_RECOVERY_KEY', 'const PANEL_PATHS');
const startup = sourceBetween(runtime, 'function verifyAdminModules()', 'function certificateUsesExternalFiles');
function fixture() {
const events = new Map(), windows = new Map(), pending = [], streams = [], nodes = new Map(), timers = new Map();
const classes = new Set(), originMarker = new Map();
let timerId = 0, reloads = 0, lockCount = 0, renderSessionCalls = 0;
const node = id => {
if (!nodes.has(id)) {
const handlers = new Map();
nodes.set(id, { hidden: id !== 'workspace', disabled: false, textContent: '',
addEventListener(type, fn) { handlers.set(type, fn); },
click() { handlers.get('click')?.(); },
});
}
return nodes.get(id);
};
class FakeEventSource {
constructor(url) { this.url = url; this.closed = false; streams.push(this); }
addEventListener() {}
close() { this.closed = true; }
}
const classList = {
contains(k) { return classes.has(k); },
add(...keys) { keys.forEach(k => classes.add(k)); },
remove(...keys) { keys.forEach(k => classes.delete(k)); },
toggle(k, forced) { if (forced) classes.add(k); else classes.delete(k); },
};
const document = {
hidden: false, body: {}, documentElement: { classList },
addEventListener(name, handler) { assert(!events.has(name)); events.set(name, handler); },
removeEventListener(name, handler) { assert.equal(events.get(name), handler); events.delete(name); },
};
const window = {
rustProxySettings: { current: () => 'listeners', render() {} },
addEventListener(name, handler) { assert(!windows.has(name)); windows.set(name, handler); },
removeEventListener(name, handler) { assert.equal(windows.get(name), handler); windows.delete(name); },
};
const ctx = {
document, window, $: node, AbortController, EventSource: FakeEventSource,
MutationObserver: class { observe() {} },
location: { origin: 'https://panel.example', pathname: '/overview', search: '',
reload() { reloads++; }, replace() { throw new Error('Unexpected replace'); } },
sessionStorage: { getItem: key => originMarker.get(key), setItem: (key, value) => originMarker.set(key, value), removeItem: key => originMarker.delete(key) },
performance: { now: () => 1 },
fetch: (path, options) => new Promise((resolve, reject) => pending.push({ path, options, resolve, reject })),
setTimeout(fn, ms) { const id = ++timerId; timers.set(id, { fn, ms }); return id; },
clearTimeout(id) { timers.delete(id); },
console: { warn() {} },
csrf: 'old', authenticated: true, authEpoch: 1, sessionInfo: null, sessionCheckPromise: null,
languagePacks: new Map([['en_US', {}]]), latestStatus: null,
currentPanel: 'overview', liveEvents: null, liveInterval: null, liveConnected: false, liveFallback: false,
t: key => key === 'sessionValidationFailed' ? 'Could not verify your session' : key,
renderSession() { renderSessionCalls++; },
renderFooter() {}, renderStatus() {}, renderLogs() {}, renderDiagnostics() {},
toast() { throw new Error('Resume errors must not be transient toasts'); },
loadTunnels: async () => {},
lock() { lockCount++; ctx.authEpoch++; ctx.authenticated = false; ctx.window.rustProxyRefresh.stop(); },
};
runInNewContext(`${recovery}\n${actualApi}\n${actualLive}\n${actualResume}\n${refresh}`, ctx,
{ filename: 'actual-rustproxy-admin-source.js' });
const tick = async () => { for (let i = 0; i < 25; i++) await Promise.resolve(); };
const answer = (status, value) => {
const request = pending.shift();
assert(request && request.path === '/api/auth/session', `Unexpected request: ${request?.path}`);
request.resolve({ ok: status >= 200 && status < 300, status, text: async () => JSON.stringify(value) });
return request;
};
const visibility = hidden => { document.hidden = hidden; events.get('visibilitychange')?.(); };
const pagehide = () => windows.get('pagehide')?.({});
const pageshow = persisted => windows.get('pageshow')?.({ persisted });
const activeStreams = () => streams.filter(stream => !stream.closed);
return { ctx, events, windows, pending, streams, timers, originMarker, nodes, classes, tick, answer,
visibility, pagehide, pageshow, activeStreams, node, get reloads() { return reloads; },
get lockCount() { return lockCount; }, get renderSessionCalls() { return renderSessionCalls; } };
}
const normal = fixture();
normal.ctx.window.rustProxyRefresh.start();
assert.equal(normal.activeStreams().length, 1, 'authenticated shell starts one SSE stream');
normal.visibility(true);
assert.equal(normal.activeStreams().length, 0, 'hidden document pauses SSE');
normal.visibility(false);
normal.pageshow(true);
assert.equal(normal.pending.length, 1, 'pageshow and visibilitychange share one session GET');
assert.equal(normal.activeStreams().length, 0, 'SSE cannot resume before session verification');
assert.equal(normal.ctx.window.rustProxyRefresh.diagnostics().filter(task => task.scheduled).length, 0);
normal.answer(403, { error: 'auth.invalid_origin' }); await normal.tick();
assert.equal(normal.reloads, 0, '403 during resume must not reload');
assert.equal(normal.originMarker.size, 0, 'resume must not update startup reload limit');
assert(normal.classes.has('app-loading') && normal.classes.has('app-startup-failed'));
assert(normal.classes.has('app-session-failed'), 'show an actionable resume failure');
assert.equal(normal.node('workspace').hidden, true);
assert.equal(normal.node('app-session-retry').hidden, false);
assert.equal(normal.node('app-startup-reload').hidden, true);
assert.equal(normal.node('app-startup-title').textContent, 'Could not verify your session');
assert.equal(normal.activeStreams().length, 0);
assert.equal(normal.ctx.window.rustProxyRefresh.diagnostics().filter(task => task.scheduled).length, 0);
normal.pageshow(true);
assert.equal(normal.pending.length, 0, 'no automatic error-loop retries on duplicate resume events');
normal.node('app-session-retry').click();
assert.equal(normal.pending.length, 1, 'user may explicitly retry without a reload');
assert.equal(normal.node('app-session-retry').disabled, true);
normal.answer(200, { username: 'admin', csrf_token: 'fresh' }); await normal.tick();
assert.equal(normal.ctx.csrf, 'fresh');
assert.equal(normal.node('workspace').hidden, false);
assert.equal(normal.classes.has('app-loading'), false);
assert.equal(normal.node('app-session-retry').hidden, true);
assert.equal(normal.node('app-session-retry').disabled, false);
assert.equal(normal.activeStreams().length, 1, 'SSE resumes only after verified session');
assert.equal(normal.reloads, 0);
// A real 401 follows the existing lock/redirect flow, never displaying stale data.
normal.visibility(true);
normal.visibility(false);
assert.equal(normal.pending.length, 1);
normal.answer(401, { error: 'auth.required' }); await normal.tick();
assert.equal(normal.lockCount, 1);
assert.equal(normal.activeStreams().length, 0);
assert.equal(normal.classes.has('app-session-failed'), false);
const restored = fixture();
restored.ctx.window.rustProxyRefresh.start();
restored.pagehide();
assert.equal(restored.activeStreams().length, 0);
restored.pageshow(true);
restored.visibility(false);
assert.equal(restored.pending.length, 1, 'BFCache pagehide/pageshow + visibility triggers one session check');
restored.answer(200, { username: 'admin', csrf_token: 'bfcache' }); await restored.tick();
assert.equal(restored.activeStreams().length, 1);
// Ten tab cycles must not add listeners or duplicate SSE sessions.
for (let i = 0; i < 10; i++) {
restored.visibility(true); restored.visibility(false); restored.pageshow(true);
assert.equal(restored.pending.length, 1);
assert.equal(restored.activeStreams().length, 0);
restored.answer(200, { username: 'admin', csrf_token: `cycle-${i}` }); await restored.tick();
assert.equal(restored.activeStreams().length, 1);
assert.equal(restored.windows.size, 2);
assert.equal(restored.events.size, 1);
}
// A response from a prior auth epoch may not restore the dashboard.
restored.visibility(true); restored.visibility(false);
assert.equal(restored.pending.length, 1);
restored.ctx.authEpoch++;
restored.ctx.authenticated = false;
restored.ctx.window.rustProxyRefresh.stop();
restored.answer(200, { username: 'stale', csrf_token: 'stale' }); await restored.tick();
assert.equal(restored.activeStreams().length, 0);
assert.equal(restored.ctx.csrf, 'cycle-9');
assert.equal(restored.windows.size, 0);
assert.equal(restored.events.size, 0);
// Startup reload-marker is released only after the complete panel activation,
// never merely when /api/auth/session happened to return successfully.
async function checkStartup(activationResult, failLanguage) {
let bootstrap, clears = 0, failures = 0;
const modules = Object.fromEntries([
['rustProxyIntegrations', 'bindings'], ['rustProxyVhosts', 'render'], ['rustProxySettings', 'render'],
['rustProxyCachePanel', 'refresh'], ['rustProxyTrafficPanel', 'refresh'],
['rustProxyPreferences', 'load'], ['rustProxyRefresh', 'start'],
].map(([name, method]) => [name, { [method]() {} }]));
const ctx = {
window: modules, document: { readyState: 'loading', addEventListener(_name, cb) { bootstrap = cb; } },
api: async () => ({ csrf_token: 'valid' }), loadLanguages: async () => { if (failLanguage) throw new Error('Language assets missing'); },
translate() {}, setupRequired: false, syncAuthForm() {}, activate: async () => activationResult,
clearInvalidOriginRecovery() { clears++; }, recoverInvalidOrigin: () => false,
showStartupFailure() { failures++; },
location: { pathname: '/overview', search: '', replace() { throw new Error('unexpected redirect'); } },
};
runInNewContext(startup, ctx);
bootstrap(); for (let i = 0; i < 12; i++) await Promise.resolve();
return { clears, failures };
}
assert.deepEqual(await checkStartup(true, false), { clears: 1, failures: 0 });
assert.deepEqual(await checkStartup(false, false), { clears: 0, failures: 0 });
assert.deepEqual(await checkStartup(true, true), { clears: 0, failures: 1 });
console.log('PASS: 0.31.3 real-source resume race, 403 retry, BFCache, 401 lock, SSE pause, stale epochs, startup recovery limit and repeated cycles');
+3 -3
View File
@@ -73,12 +73,12 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
flat = module.parent.with_suffix(".rs") flat = module.parent.with_suffix(".rs")
if flat.is_file(): if flat.is_file():
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}") raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]: for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/session-resume.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]:
if not (root / path).is_file(): if not (root / path).is_file():
raise ValueError(f"Incomplete RustProxy 0.31.2 source tree: missing {path}") raise ValueError(f"Incomplete RustProxy 0.31.3 source tree: missing {path}")
text = (root / "src/lib.rs").read_text(encoding="utf-8") text = (root / "src/lib.rs").read_text(encoding="utf-8")
if '#[path = "auth/mod.rs"]' not in text: if '#[path = "auth/mod.rs"]' not in text:
raise ValueError("src/lib.rs is not the 0.31.2 entry point; extract the complete package, not individual files.") raise ValueError("src/lib.rs is not the 0.31.3 entry point; extract the complete package, not individual files.")
return messages return messages
+14 -5
View File
@@ -14,7 +14,7 @@ assert.match(bridge, /function startLive\(/);
function fixture({ withoutSse = false } = {}) { function fixture({ withoutSse = false } = {}) {
let time = 0, nextId = 1, renderCount = 0, logCount = 0, diagCount = 0; let time = 0, nextId = 1, renderCount = 0, logCount = 0, diagCount = 0;
const timers = new Map(), listeners = new Map(), streams = [], requests = [], rendered = [], failures = []; const timers = new Map(), listeners = new Map(), windowListeners = new Map(), streams = [], requests = [], rendered = [], failures = [];
let response = async () => ({}); let response = async () => ({});
class FakeEventSource { class FakeEventSource {
constructor(url) { this.url = url; this.closed = false; this.handlers = new Map(); streams.push(this); } constructor(url) { this.url = url; this.closed = false; this.handlers = new Map(); streams.push(this); }
@@ -37,7 +37,11 @@ function fixture({ withoutSse = false } = {}) {
console: { warn(...args) { failures.push(args); } }, console: { warn(...args) { failures.push(args); } },
authenticated: true, authEpoch: 1, currentPanel: "overview", liveConnected: false, liveFallback: false, authenticated: true, authEpoch: 1, currentPanel: "overview", liveConnected: false, liveFallback: false,
liveEvents: null, liveInterval: null, latestStatus: null, liveEvents: null, liveInterval: null, latestStatus: null,
window: { rustProxySettings: { current: () => "listeners", render: () => undefined } }, window: {
rustProxySettings: { current: () => "listeners", render: () => undefined },
addEventListener(type, callback) { assert(!windowListeners.has(type)); windowListeners.set(type, callback); },
removeEventListener(type, callback) { assert.equal(windowListeners.get(type), callback); windowListeners.delete(type); },
},
$: name => nodes[name] || null, $: name => nodes[name] || null,
t: name => name, t: name => name,
api: (url, args = {}) => { requests.push({ url, args, time }); return response(url, args); }, api: (url, args = {}) => { requests.push({ url, args, time }); return response(url, args); },
@@ -45,7 +49,11 @@ function fixture({ withoutSse = false } = {}) {
renderLogs() { logCount++; }, renderLogs() { logCount++; },
renderDiagnostics() { diagCount++; }, renderDiagnostics() { diagCount++; },
renderLiveStatus() { throw new Error("bridge not loaded"); }, renderLiveStatus() { throw new Error("bridge not loaded"); },
revalidateSession: async () => undefined, revalidateSession: async () => {
ctx.window.rustProxyRefresh.suspendForSessionValidation();
await Promise.resolve();
ctx.window.rustProxyRefresh.completeSessionValidation(true);
},
loadTunnels: async () => undefined loadTunnels: async () => undefined
}; };
runInNewContext(bridge + "\n" + scheduler, ctx, { filename: "coordinator-browser-bundle.js" }); runInNewContext(bridge + "\n" + scheduler, ctx, { filename: "coordinator-browser-bundle.js" });
@@ -76,7 +84,7 @@ function fixture({ withoutSse = false } = {}) {
} }
} }
return { return {
ctx, refresh, requests, rendered, streams, listeners, timers, failures, advance, switchPanel, count, ctx, refresh, requests, rendered, streams, listeners, windowListeners, timers, failures, advance, switchPanel, count,
registerViews, changeVisibility, registerViews, changeVisibility,
setResponse(fn) { response = fn; }, setResponse(fn) { response = fn; },
get renderCount() { return renderCount; }, get renderCount() { return renderCount; },
@@ -152,7 +160,7 @@ assert(h.streams.at(-1).closed, "SSE closes when hidden");
assert.equal(h.refresh.diagnostics().filter(row => row.scheduled).length, 0); assert.equal(h.refresh.diagnostics().filter(row => row.scheduled).length, 0);
await h.advance(120_000); await h.advance(120_000);
assert.equal(h.count("/api/traffic"), beforeHidden); assert.equal(h.count("/api/traffic"), beforeHidden);
h.changeVisibility(false); await h.advance(); h.changeVisibility(false); await h.advance(); await h.advance();
assert.equal(h.count("/api/traffic"), beforeHidden + 1); assert.equal(h.count("/api/traffic"), beforeHidden + 1);
assert.equal(h.streams.filter(stream => !stream.closed).length, 1); assert.equal(h.streams.filter(stream => !stream.closed).length, 1);
@@ -206,6 +214,7 @@ assert.throws(() => h.refresh.register("traffic", { interval: () => 5, run: asyn
// Logout clears timers, active SSE streams, observer and pending fetches. // Logout clears timers, active SSE streams, observer and pending fetches.
h.refresh.stop(); h.refresh.stop();
assert.equal(h.listeners.has("visibilitychange"), false); assert.equal(h.listeners.has("visibilitychange"), false);
assert.equal(h.windowListeners.size, 0, "pagehide/pageshow listeners are removed on logout");
assert.equal(h.streams.filter(stream => !stream.closed).length, 0); assert.equal(h.streams.filter(stream => !stream.closed).length, 0);
assert.equal(h.refresh.diagnostics().filter(row => row.scheduled).length, 0); assert.equal(h.refresh.diagnostics().filter(row => row.scheduled).length, 0);
const afterLogout = h.requests.length; const afterLogout = h.requests.length;
+6 -6
View File
@@ -135,7 +135,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert backup.is_dir() assert backup.is_dir()
assert (backup / "src/obsolete.rs").is_file() assert (backup / "src/obsolete.rs").is_file()
assert not (target / "src/obsolete.rs").exists() assert not (target / "src/obsolete.rs").exists()
assert 'version = "0.31.2"' in (target / "Cargo.toml").read_text(encoding="utf-8") assert 'version = "0.31.3"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n" 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 / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
assert (target / "langs/site.json").is_file() 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/certificate.go").is_file()
assert (target / "docker-agent/server.go").is_file() assert (target / "docker-agent/server.go").is_file()
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8")) manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
assert manifest["to_version"] == "0.31.2" assert manifest["to_version"] == "0.31.3"
assert manifest["removed_obsolete_runtime_state"] is True assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists() assert not legacy.exists()
assert not (backup / "legacy-data").exists() assert not (backup / "legacy-data").exists()
@@ -463,7 +463,7 @@ def ui_contract_smoke() -> None:
assert 'viewBox: "0 0 300 60"' in app and 'M0 15H300 M0 30H300 M0 45H300' in app assert 'viewBox: "0 0 300 60"' in app and 'M0 15H300 M0 30H300 M0 45H300' in app
assert '.network-interface-row {' in css and 'background: transparent' in css assert '.network-interface-row {' in css and 'background: transparent' in css
assert "hostTagEditor" in vhosts and "vhost-chart-line" in vhosts and "vhLifetimeChart" in vhosts assert "hostTagEditor" in vhosts and "vhost-chart-line" in vhosts and "vhLifetimeChart" in vhosts
for marker in ["refreshCertificateInventory", "certificateDetails", "certificate-domain-pills", "certificateCoverageUnknown", "async function revalidateSession", 'Object.values(PANEL_PATHS), "/configuration"', "recoverInvalidOrigin", "auth.invalid_origin", "ORIGIN_RECOVERY_KEY"]: for marker in ["refreshCertificateInventory", "certificateDetails", "certificate-domain-pills", "certificateCoverageUnknown", "function revalidateSession", 'Object.values(PANEL_PATHS), "/configuration"', "recoverInvalidOrigin", "auth.invalid_origin", "ORIGIN_RECOVERY_KEY"]:
assert marker in app, marker assert marker in app, marker
for marker in ['data-certificate-tab="installed"', 'data-certificate-tab="renewal"', 'data-certificate-tab="generate"', 'id="certificate-page-installed"', 'id="certificate-page-renewal"', 'id="certificate-page-generate"']: for marker in ['data-certificate-tab="installed"', 'data-certificate-tab="renewal"', 'data-certificate-tab="generate"', 'id="certificate-page-installed"', 'id="certificate-page-renewal"', 'id="certificate-page-generate"']:
assert marker in html, marker assert marker in html, marker
@@ -617,7 +617,7 @@ def source_contract_smoke() -> None:
assert marker in admin, marker assert marker in admin, marker
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0] 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 '"/api/status"' not in anonymous_block
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.31.2"' in openapi_0241 assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.31.3"' in openapi_0241
assert 'id="panel-logging"' not in html 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"']: for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
assert marker in html, marker assert marker in html, marker
@@ -1108,7 +1108,7 @@ def source_contract_smoke() -> None:
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main 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") openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
assert '"/api/config/history/restore"' in openapi assert '"/api/config/history/restore"' in openapi
for marker in ['"version": "0.31.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"']: for marker in ['"version": "0.31.3"', '"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 marker in openapi, marker
assert '"software_update"' in openapi and '"update_available"' in openapi assert '"software_update"' in openapi and '"update_available"' in openapi
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']: for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
@@ -1313,7 +1313,7 @@ def main() -> None:
source_contract_smoke() source_contract_smoke()
container_contract_smoke() container_contract_smoke()
grafana_contract_smoke() grafana_contract_smoke()
print("PASS: 0.31.2 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.") print("PASS: 0.31.3 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -35,7 +35,7 @@ for directory in ("scripts/runtime", "scripts/build", "scripts/deploy", "scripts
for old in (ROOT / "src/admin/web/app.js", ROOT / "src/admin/web/vhosts.js"): for old in (ROOT / "src/admin/web/app.js", ROOT / "src/admin/web/vhosts.js"):
assert not old.exists(), f"oversized browser monolith returned: {old.relative_to(ROOT)}" assert not old.exists(), f"oversized browser monolith returned: {old.relative_to(ROOT)}"
for required in ( for required in (
"src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/app/core.js", "src/admin/web/app/session-resume.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js",
"src/admin/web/vhosts/core.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js",
"docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go",
"docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/certificate.go", "docker-agent/server.go",
+4 -4
View File
@@ -13,7 +13,7 @@ const has = (haystack, needle) => assert.ok(haystack.includes(needle), `Missing
// The two mini charts share the exact H1/H2/H3 palette as their SVG lines. // The two mini charts share the exact H1/H2/H3 palette as their SVG lines.
for (const version of ['h1', 'h2', 'h3']) { for (const version of ['h1', 'h2', 'h3']) {
has(css, `.stat-chart .stat-chart-legend .legend-${version} i { background: var(--traffic-${version}); }`); assert.match(css, new RegExp(`\\.stat-chart\\s+\\.stat-chart-legend\\s+\\.legend-${version}\\s+i\\s*\\{\\s*background:\\s*var\\(--traffic-${version}\\);`));
assert.match(css, new RegExp(`\\.proto-${version}-line\\s*\\{[^}]*stroke:\\s*var\\(--traffic-${version}\\);`)); assert.match(css, new RegExp(`\\.proto-${version}-line\\s*\\{[^}]*stroke:\\s*var\\(--traffic-${version}\\);`));
has(html, `id="http-protocol-${version}"`); has(html, `id="http-protocol-${version}"`);
has(html, `id="ws-protocol-${version}"`); has(html, `id="ws-protocol-${version}"`);
@@ -43,7 +43,7 @@ const code = js.slice(js.indexOf(' const runtime = latestStatus?.runtime || {
const elements = new Map(); const elements = new Map();
const dollar = id => { if (!elements.has(id)) elements.set(id, { textContent: '' }); return elements.get(id); }; const dollar = id => { if (!elements.has(id)) elements.set(id, { textContent: '' }); return elements.get(id); };
const status = { const status = {
version: '0.31.2', uptime_seconds: 4201, version: '0.31.3', uptime_seconds: 4201,
runtime: { runtime: {
platform: 'Linux', operating_system: 'Debian GNU/Linux 13', kernel_version: '6.12.0', architecture: 'x86_64', platform: 'Linux', operating_system: 'Debian GNU/Linux 13', kernel_version: '6.12.0', architecture: 'x86_64',
statistics_source: 'systemstat+sysinfo', data_directory: '/var/lib/rustproxy', database_path: '/var/lib/rustproxy/config.sqlite3', statistics_source: 'systemstat+sysinfo', data_directory: '/var/lib/rustproxy', database_path: '/var/lib/rustproxy/config.sqlite3',
@@ -63,7 +63,7 @@ const context = {
vm.runInNewContext(code, context); vm.runInNewContext(code, context);
assert.equal(dollar('runtime-operating-system').textContent, 'Debian GNU/Linux 13'); assert.equal(dollar('runtime-operating-system').textContent, 'Debian GNU/Linux 13');
assert.equal(dollar('runtime-kernel-version').textContent, '6.12.0'); assert.equal(dollar('runtime-kernel-version').textContent, '6.12.0');
assert.equal(dollar('runtime-version').textContent, '0.31.2'); assert.equal(dollar('runtime-version').textContent, '0.31.3');
assert.equal(dollar('runtime-logical-cpus').textContent, '8'); assert.equal(dollar('runtime-logical-cpus').textContent, '8');
assert.equal(dollar('runtime-cpu-usage').textContent, '17.2%'); assert.equal(dollar('runtime-cpu-usage').textContent, '17.2%');
assert.equal(dollar('runtime-python').textContent, 'Available · 3.13.5'); assert.equal(dollar('runtime-python').textContent, 'Available · 3.13.5');
@@ -124,4 +124,4 @@ for (const key of ['python_version','pip_version','certbot_version']) {
assert.ok(runtimeSchema.properties.certificate_runtime.properties[key]); assert.ok(runtimeSchema.properties.certificate_runtime.properties[key]);
assert.equal(runtimeSchema.properties.certificate_runtime.properties[key].nullable, true); assert.equal(runtimeSchema.properties.certificate_runtime.properties[key].nullable, true);
} }
console.log('PASS: 0.31.2 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.'); console.log('PASS: 0.31.3 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
@@ -75,7 +75,7 @@ check(prepared.includes('response = intercept_route_origin_error(response, route
const marker = fs.readFileSync('src/proxy/error_fallback.rs','utf8'); const marker = fs.readFileSync('src/proxy/error_fallback.rs','utf8');
check(marker.includes('response.extensions_mut().insert(crate::pages::OriginProvidedResponse)'), 'Origin responses must be marked for the outer observer'); check(marker.includes('response.extensions_mut().insert(crate::pages::OriginProvidedResponse)'), 'Origin responses must be marked for the outer observer');
const observer = fs.readFileSync('src/proxy/upstream_helpers.rs','utf8'); const observer = fs.readFileSync('src/proxy/upstream_helpers.rs','utf8');
check(observer.includes('response.extensions().get::<crate::pages::OriginProvidedResponse>().is_none()'), 'Outer response observer must not unconditionally replace origin 404/5xx'); check(/fn should_replace_error_page\(/.test(observer) && /get::<crate::pages::OriginProvidedResponse>\(\)\s*\.is_none\(\)/.test(observer) && /if should_replace_error_page\(&response, minimal_welcome\)/.test(observer), 'Outer response observer must not unconditionally replace origin 404/5xx');
const pages = fs.readFileSync('src/pages.rs','utf8'); const pages = fs.readFileSync('src/pages.rs','utf8');
check(pages.includes('body_only.status = None;'), 'Application error interception must preserve the upstream HTTP code even when page overrides status'); check(pages.includes('body_only.status = None;'), 'Application error interception must preserve the upstream HTTP code even when page overrides status');
const interceptPosition = prepared.indexOf('response = intercept_route_origin_error(response, route, &snapshot);'); const interceptPosition = prepared.indexOf('response = intercept_route_origin_error(response, route, &snapshot);');
+1 -1
View File
@@ -1,6 +1,6 @@
from pathlib import Path from pathlib import Path
APP_PARTS = ("core.js", "refresh.js", "preferences.js", "config-history.js", "runtime.js", "settings.js") APP_PARTS = ("core.js", "session-resume.js", "refresh.js", "preferences.js", "config-history.js", "runtime.js", "settings.js")
PANEL_PARTS = ( PANEL_PARTS = (
"management.js", "drafts.js", "integrations.js", "vhosts.js", "management.js", "drafts.js", "integrations.js", "vhosts.js",
"web-server-profiles.js", "web-server.js", "docker.js", "streams.js", "web-server-profiles.js", "web-server.js", "docker.js", "streams.js",
+1 -1
View File
@@ -54,7 +54,7 @@ def run_load(
port = parsed.port or (443 if parsed.scheme == "https" else 80) port = parsed.port or (443 if parsed.scheme == "https" else 80)
tls = ssl._create_unverified_context() if insecure else ssl.create_default_context() tls = ssl._create_unverified_context() if insecure else ssl.create_default_context()
connection_type = http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection connection_type = http.client.HTTPSConnection if parsed.scheme == "https" else http.client.HTTPConnection
headers = {"user-agent": "RustProxy-Memory-Stress/0.31.2", "accept": "*/*"} headers = {"user-agent": "RustProxy-Memory-Stress/0.31.3", "accept": "*/*"}
if host_header: if host_header:
headers["host"] = host_header headers["host"] = host_header