v0.31.4
This commit is contained in:
1 parent
95c55da2ee
commit
d942c1a35d
42 files changed
+1265
-109
No files matched your search
@@ -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.3.
|
compatibility. Current source version: 0.31.4.
|
||||||
|
|
||||||
## Delivery
|
## Delivery
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
> 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.4
|
||||||
|
|
||||||
|
- Correct the administration panel's handling of intermittent `auth.invalid_origin` with optional explicit HTTPS `administration.public_origin` when behind a trusted reverse proxy. Allow a bounded, HTTPS-first multi-hop forwarded-proto chain without trusting arbitrary forwarding headers.
|
||||||
|
- Use a single public panel URL: automatically include its hostname in the panel's effective Host allowlist. Keep `allowed_hosts` as optional **additional** DNS entries in an advanced section and deduplicate legacy repeated values on edit/save without an automatic database migration.
|
||||||
|
- Preserve strict Host, forwarded-Host, Origin, Fetch Metadata, CSRF and trusted-proxy checks; keep public proxy routes and SQLite schema unchanged.
|
||||||
|
- Add configuration and HTTP guard simulations, Rust source regression tests, English/Polish form labels and operator documentation. Rust/Cargo build and live mobile acceptance are still outstanding.
|
||||||
|
|
||||||
## 0.31.3
|
## 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.
|
- 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.
|
||||||
|
|||||||
Generated
+1
-1
@@ -1881,7 +1881,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-proxy"
|
name = "rust-proxy"
|
||||||
version = "0.31.3"
|
version = "0.31.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rust-proxy"
|
name = "rust-proxy"
|
||||||
version = "0.31.3"
|
version = "0.31.4"
|
||||||
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"
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
# RustProxy 0.31.3
|
# RustProxy 0.31.4
|
||||||
|
|
||||||
**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.3 mobile-session recovery patch source (Cargo verification required).**
|
**0.31.4 administration Origin pinning and simplified one-field configuration (Cargo verification required).**
|
||||||
|
|
||||||
|
**Reissued source package:** this package supersedes the earlier 0.31.4 archive under the same version number. Update/version checks cannot distinguish the two by version alone; rebuild/redeploy explicitly and verify the package SHA-256.
|
||||||
|
|
||||||
## Layer-4 streams
|
## Layer-4 streams
|
||||||
|
|
||||||
@@ -13,7 +15,16 @@ 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.3
|
## What changed in 0.31.4
|
||||||
|
|
||||||
|
- Fix intermittent `auth.invalid_origin` on the **administration panel only** behind a trusted reverse proxy. The optional `administration.public_origin` pins the browser-facing HTTPS origin and allows a bounded `X-Forwarded-Proto: https, http` chain when the immediate proxy peer is trusted. Without a pin, ambiguous forwarded-proto values still return HTTP 403.
|
||||||
|
- Set **only** `administration.public_origin`, for example `https://panel.example.test`, in Settings → Administration. Its DNS hostname is automatically included in the panel Host allowlist; it does **not** need to be repeated in `allowed_hosts`.
|
||||||
|
- The advanced **Additional allowed DNS hostnames** section remains optional. Older configurations with a duplicate pinned hostname still load, and editing/saving omits that duplicate while preserving independent entries. Removing or changing the pinned URL never silently keeps the previous hostname implicitly allowed.
|
||||||
|
- Keep strict checks for foreign `Origin`, mismatched or duplicate `X-Forwarded-Host`, cross-site requests, CSRF and untrusted proxies. The narrow opt-in self-admin session-cookie path may use the derived hostname only under its existing local-listener-risk conditions. Public proxy traffic and SQLite schema 9 are unchanged.
|
||||||
|
- Restart RustProxy after modifying the setting. See [Administration HTTPS](docs/SETTINGS.md) and [0.31.4 implementation and tests](docs/IMPLEMENTATION_0.31.4.md).
|
||||||
|
- **Release gate remains open:** Rust/Cargo compilation, resolved dependency lockfile and real mobile/reverse-proxy acceptance are still required before production deployment.
|
||||||
|
|
||||||
|
## Previously 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
# RustProxy roadmap
|
# RustProxy roadmap
|
||||||
|
|
||||||
|
## 0.31.4 - Administration-only public HTTPS Origin with one-field configuration
|
||||||
|
|
||||||
|
- [x] Add explicit HTTPS `administration.public_origin` and restrict its use to trusted reverse-proxy peers for panel requests.
|
||||||
|
- [x] Derive the pinned DNS hostname for the effective Host allowlist without duplicating it in saved `allowed_hosts`.
|
||||||
|
- [x] Keep additional allowed DNS hostnames in an advanced panel field and remove redundant legacy duplicates on edit/save.
|
||||||
|
- [x] Preserve strict Origin, CSRF, trusted proxy, forwarded Host and HTTPS-first header checks and the opt-in self-admin session-cookie boundary.
|
||||||
|
- [x] Add panel settings UI, translations, API schema, Rust source regressions and Node simulations without changing public proxy forwarding.
|
||||||
|
- [ ] Resolve Cargo dependencies, compile and test Rust, and perform real mobile/reverse-proxy acceptance before production deployment.
|
||||||
|
|
||||||
## 0.31.3 - Mobile session recovery and Origin diagnostics
|
## 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] 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.
|
||||||
|
|||||||
+41
-35
@@ -1,18 +1,18 @@
|
|||||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||||
2326433366f5ee39f50cc0c8fd308a1aa4bf913dd29c60d36d6ad40596e2057e AGENTS.md
|
ce2dfe3ddaf72f4d9f47fbddde8cc3a6379c95797785bbafdbc9f505ab0c06d7 AGENTS.md
|
||||||
fe15afa19911918df2cb8f754d42b23e367056b2350c4421273e4ea15558e04c CHANGELOG.md
|
3868e2b5ac1e1e324e0647da39dd4ace965b102fdd06a26d720977cb7bcc4ba2 CHANGELOG.md
|
||||||
a527e1bdd3ad6217a4a0bb06f29d33b4f5718525b8de81d4170ec7233bee5155 Cargo.lock
|
140089a442d329eb0819f17bbb8e9215687ede43d77fb74068c1b4e79b074547 Cargo.lock
|
||||||
267b8d8ff64905e059ea6626a6ab48fb20f42cf5ae4af25ffc69b07535a9d42b Cargo.toml
|
d8e3b968089f11eac6098bb8f51598a9012de6ec2f84a0ef6899ac27cdf2a134 Cargo.toml
|
||||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||||
1ceec1cdf7e056bf26096eedcf95f0efb192b69c0f614ba13ecf2808e534ebf3 README.md
|
6c1cddc4650a1bd9e31b3598baffb0d55435a2d3a5e31653c111aacd4ed96070 README.md
|
||||||
4c8c8a4efa169b7c92d27715d15d467c95c705bd8b439302e6bbde26e56f1dfd ROADMAP.md
|
ea6203f84882ab99aa22b99dd26d0d7b8d7fd64639aa74230ee431b3edac25ae 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
|
||||||
5147ac3233914fe580b4c4d5ae1b74af2948019f34ff6629d21a7811b27a5b84 dev.sh
|
7e9d5cf9a0720ac00a87a177b41c44524ed6b04e819343ea16967945b3a413ae 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
|
||||||
@@ -27,8 +27,8 @@ e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/n
|
|||||||
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
||||||
875faa3969df9409d79f050a84b4183496f557edda3d34aaec19fe556cf8c471 docs/ACME_PROVIDERS.md
|
875faa3969df9409d79f050a84b4183496f557edda3d34aaec19fe556cf8c471 docs/ACME_PROVIDERS.md
|
||||||
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
|
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
|
||||||
8ded74f422426606238fa9876b49277a7bc56ddd96062a836cdd6ddbf78bd335 docs/API.md
|
a36880dc07e922a36a4c8be5fa889a03658bb0492a1e20ec00fbb130241c7719 docs/API.md
|
||||||
6ed12684b970fb7173fc9d1ad1da8ecce784eb38b29b7e6e631309fb25446807 docs/API_ACCESS.md
|
b0c6fae1d538f33fb468a00307cbb745a601d9fc341c0a4f859d60c7e8d48865 docs/API_ACCESS.md
|
||||||
4c5288c9ed427cfb919bd023ed9a321dcf6d64bc4cad3e1768393356c5d835ba docs/ARCHITECTURE.md
|
4c5288c9ed427cfb919bd023ed9a321dcf6d64bc4cad3e1768393356c5d835ba docs/ARCHITECTURE.md
|
||||||
e9e2327684a4ae2d1ca4a0b3fb67d0ff83b5a150d38d4de4cfad2c5d3102eab6 docs/CACHE.md
|
e9e2327684a4ae2d1ca4a0b3fb67d0ff83b5a150d38d4de4cfad2c5d3102eab6 docs/CACHE.md
|
||||||
9113f3abbfe81bafc4855d8680df8aa2a381ecc358cc3e1a0f78b6b756421117 docs/CACHE_DIAGNOSTICS.md
|
9113f3abbfe81bafc4855d8680df8aa2a381ecc358cc3e1a0f78b6b756421117 docs/CACHE_DIAGNOSTICS.md
|
||||||
@@ -42,6 +42,7 @@ befa50579e3475b22589f26380d85b3345b80e4fa947d49649a3b91a8dd8b8c2 docs/CUSTOM_LO
|
|||||||
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
|
991ff3c09ca1e2bcf73b8fa2975405b955dd640c3f1e1246e75038c043c17183 docs/IMPLEMENTATION_0.31.3.md
|
||||||
|
db2087608a1225c56e42c416d949b777fb15b1ed8b69a1e71fc2a6af6750a1e5 docs/IMPLEMENTATION_0.31.4.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
|
||||||
@@ -55,7 +56,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
|
||||||
e87c81d2eb2e90d5b1ea9e40abb81a65ca81593eb5cc3e39717b818c9a75b287 docs/README.md
|
1abcf8b9abcff02cf40c0f3e5597af1e1bf3e6d48c2f7f4278a53c6ebad51f5b 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
|
||||||
@@ -64,9 +65,9 @@ a2bb837cdd3559d58158880c1f832badf64035376a3623050bb4e40c2bca104c docs/ROADMAP_H
|
|||||||
5dbdf45fddba198a28f511dbbd2f12bf7d2b88c41c3cbedbc20dfea3858779d3 docs/SECURITY.md
|
5dbdf45fddba198a28f511dbbd2f12bf7d2b88c41c3cbedbc20dfea3858779d3 docs/SECURITY.md
|
||||||
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
||||||
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
|
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
|
||||||
2cd9dadc49a5474e4ab06d5c01fa38c27cab436578cb9c9f327a37b3406d2e13 docs/SETTINGS.md
|
970025a6d1a37452b8bead6c9d2b3d4743ea602251dc446d575531d76c11ae83 docs/SETTINGS.md
|
||||||
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
|
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
|
||||||
1de7cf885e596ccfab31a434d597c3a8d9113d206305810ea628a5ee6d96f986 docs/TEST_REPORT.md
|
d7cd61b534a2d38511d212cd9ee906b16b28d99d4a7ba5ed6ef7c1ab3611d2b8 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
|
||||||
@@ -138,8 +139,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
|
||||||
19c17d16960539f9811cd551bbcbba25863711b064618069a58d48de3cc9fade langs/en_US.json
|
61337803fabb183629fa4dc9ff038ee7ee41956c8304de3d0122d425d0b1f53a langs/en_US.json
|
||||||
fcdb2db09c7a105893fc13462da4d0341a873e305bcc3fdcd0bd5e10b9a110ba langs/pl_pl.json
|
949dc843e348ada1b1208407daa13463a0e7e16548411376382e18635578dcbb 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
|
||||||
@@ -165,7 +166,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
|
||||||
3d8cfbc8177ef82f28dfba3fd555955dac4a29003267f93572ea26a98fb8255e scripts/resolve_dependencies_0312.sh
|
abf438c3cb5a9d0dcc1351a541d3d399f3e963fc793ceb1e4708beaf3f7523a9 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
|
||||||
@@ -180,17 +181,17 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi
|
|||||||
87f64198770ae026c03a242fa542db2590ca734b02e756f1e5c28d66236f753a src/acme/runtime_env.rs
|
87f64198770ae026c03a242fa542db2590ca734b02e756f1e5c28d66236f753a src/acme/runtime_env.rs
|
||||||
f8075a5ef9ae5651d72a33ac0f0fd5b44ad0ae30f6e2d2318412177e0ed837f5 src/acme/runtime_versions.rs
|
f8075a5ef9ae5651d72a33ac0f0fd5b44ad0ae30f6e2d2318412177e0ed837f5 src/acme/runtime_versions.rs
|
||||||
e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests.rs
|
e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests.rs
|
||||||
bc9eb32f0d2785bac122d5eff44742cd24306c503daa065564383d46416b2211 src/admin/access.rs
|
b6f38f3fd75d6685bbc882ed1077a38bf495cfcf01c397f69ed9b98be68b446f src/admin/access.rs
|
||||||
019f4aa33e3596bc9c11ef8cf5703bcfffe46c15288c0eba83c8dd9e38055fb2 src/admin/certificates.rs
|
019f4aa33e3596bc9c11ef8cf5703bcfffe46c15288c0eba83c8dd9e38055fb2 src/admin/certificates.rs
|
||||||
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
|
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
|
||||||
69095640bb0dca081317c7776cd73e83fab11ddd04fe903acea8f8c1b4899210 src/admin/documentation/documents.rs
|
efa4a357d51d9b3c575e64593b55c8a30afbab5cd96a44cc39786207228757b6 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
|
||||||
a857bcbdef2a673be2f01c5817ca6d75dd5627b7e3b80777c394aa2edd933cd8 src/admin/openapi.json
|
3028fd407ec2cbda7742746241113739136a27a5b81993acb2d6f99187086246 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
|
||||||
@@ -200,12 +201,12 @@ d9e90166d2199152585a5208feedfc1dd1395dc32ddbd2a094ae4a1aee45e115 src/admin/pref
|
|||||||
c9f952bec99dfe1d5a60998a38bd177f82f54de567230413e0c322ee561eba7c src/admin/public/tests.rs
|
c9f952bec99dfe1d5a60998a38bd177f82f54de567230413e0c322ee561eba7c src/admin/public/tests.rs
|
||||||
6125419b099cf4d330767bc07424d52a2b400f273d31084c058c43a72a6427b7 src/admin/refresh_preferences.rs
|
6125419b099cf4d330767bc07424d52a2b400f273d31084c058c43a72a6427b7 src/admin/refresh_preferences.rs
|
||||||
765c14e52bdc9e542d6ff651c250c24d33b77b7e46f1b5f19acf96cc2c99bdc8 src/admin/router.rs
|
765c14e52bdc9e542d6ff651c250c24d33b77b7e46f1b5f19acf96cc2c99bdc8 src/admin/router.rs
|
||||||
daf0f7393f2350e569a5e8dda52dbf3e3bbefcb989f07f723dca540ae0fe487e src/admin/session.rs
|
a40adfbaa908da3ea726d7aa3d45394a70a6a55b6fc0063270ec4d2a2f2026a3 src/admin/session.rs
|
||||||
6fa2e586feb335ea9ce2c4dc523e422e4b2aa52c823ba8f8851f9e0f37cd2fef src/admin/status.rs
|
e7355ef15e4377cd71e47d054c607079dd8e6711f1d86824f716b8003210a046 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
|
684307b74155ac7833acfb5e9e8eaccc92575d49b760928c30fc22e805a42a4c src/admin/tests.rs
|
||||||
18aa617a0bf60a3d8d2ad7dd059a369e7549ef46683d8f912e07f8fb091c26d1 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
|
||||||
@@ -216,6 +217,7 @@ cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail
|
|||||||
54ed2130e85239ffa4576417dd68fbb9349e396210b75f5da862897137d7e876 src/admin/tests/part_08.rs
|
54ed2130e85239ffa4576417dd68fbb9349e396210b75f5da862897137d7e876 src/admin/tests/part_08.rs
|
||||||
225a29e2e560e42e06e19c3d41c0196c4a42e1e84caa7c83446cab62e3ac30dc src/admin/tests/part_09.rs
|
225a29e2e560e42e06e19c3d41c0196c4a42e1e84caa7c83446cab62e3ac30dc src/admin/tests/part_09.rs
|
||||||
bffa6611f3776e97d0065b510b63f083aabf7b3429fcc5cb26b1883fdb7c47dd src/admin/tests/part_10.rs
|
bffa6611f3776e97d0065b510b63f083aabf7b3429fcc5cb26b1883fdb7c47dd src/admin/tests/part_10.rs
|
||||||
|
578f93cc5771b5858813945e5608b15c572bd3d7fd15bae291d292e2994ece25 src/admin/tests/part_11.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
|
||||||
9527dcafb9c14e420cb841a6984d373ba2136d07c45350e9368dd7a88e89af22 src/admin/web/app/core.js
|
9527dcafb9c14e420cb841a6984d373ba2136d07c45350e9368dd7a88e89af22 src/admin/web/app/core.js
|
||||||
@@ -240,14 +242,14 @@ 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
|
||||||
8490011e8164e6fab9601ff75a82eef3cc7972d1fdb8e8c4f73df5796ae72d37 src/admin/web/index.html
|
7994b44abf769243ff3a21d248d3eb79f26736c7730bd94978c29be08b3f07ef 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
|
||||||
21ddae9743ca545c89f6df0db40810dd680812e58f2b2b6875ed64fd8fb97ac6 src/admin/web/management.js
|
21ddae9743ca545c89f6df0db40810dd680812e58f2b2b6875ed64fd8fb97ac6 src/admin/web/management.js
|
||||||
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
|
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
|
||||||
df6efd27b1ebe1fafeb0dc6061b69be9aa46f71451f0fcdd00e2f7ef8692d3cb src/admin/web/settings.js
|
8fcad0d1a161ac6f45d773d1dc3925db2d32b34bdbe57595924f84eacb4f9136 src/admin/web/settings.js
|
||||||
bb67e77af5514478485cfb69f339d63d7eb0cbadd093fa054814a416260ccc1d src/admin/web/streams.js
|
bb67e77af5514478485cfb69f339d63d7eb0cbadd093fa054814a416260ccc1d src/admin/web/streams.js
|
||||||
c4f051f54293156803b48c04d972e9d5a290209826138ce15356d19adf569979 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
|
||||||
@@ -396,7 +398,7 @@ e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus
|
|||||||
59095a04707c2e4714358f4462dcf21c41952474b9b815c42bbd14e96c573846 src/prometheus/web_php.rs
|
59095a04707c2e4714358f4462dcf21c41952474b9b815c42bbd14e96c573846 src/prometheus/web_php.rs
|
||||||
b5856fd38a957b2ca9c7e3606d84d13d5cfcf3a45b487b15ae1d2dc248cbda57 src/proxy/body.rs
|
b5856fd38a957b2ca9c7e3606d84d13d5cfcf3a45b487b15ae1d2dc248cbda57 src/proxy/body.rs
|
||||||
16b9d4db3b20b2ab14c47ac14fcd18d3d8ca77c3ad9a1f37866ebfa60f379862 src/proxy/body/tests.rs
|
16b9d4db3b20b2ab14c47ac14fcd18d3d8ca77c3ad9a1f37866ebfa60f379862 src/proxy/body/tests.rs
|
||||||
f0cb26ed34c35bacc74b914457acac8a611390de14cf46e2e6d3bcc848e914e8 src/proxy/cache_helpers.rs
|
d3126b44f0a32d60421bb5afe8e83adf4a253faf7dc77f500bce8fe5d78b9d74 src/proxy/cache_helpers.rs
|
||||||
aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs
|
aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs
|
||||||
771cc1a38927f96e593b144dbab66cd64a5dc0ed42d54b1615c359845b2b7223 src/proxy/error_fallback.rs
|
771cc1a38927f96e593b144dbab66cd64a5dc0ed42d54b1615c359845b2b7223 src/proxy/error_fallback.rs
|
||||||
bf2e6bbd64fb45fceed54af0bff1c1e00da22b79c64dd8f5c23d2af0dbab0b7d src/proxy/forward/cache_lookup.rs
|
bf2e6bbd64fb45fceed54af0bff1c1e00da22b79c64dd8f5c23d2af0dbab0b7d src/proxy/forward/cache_lookup.rs
|
||||||
@@ -426,7 +428,7 @@ d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.r
|
|||||||
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
|
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
|
||||||
d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
|
d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
|
||||||
4e0b25c2b86eefddd76c64b39dc1c1cd6d243addc32cdea99e6c3ac84ed879a6 src/runtime/certificates.rs
|
4e0b25c2b86eefddd76c64b39dc1c1cd6d243addc32cdea99e6c3ac84ed879a6 src/runtime/certificates.rs
|
||||||
2752a1122f22cab7af9ccdd23cd0aa6d99a73f1d82614814217a3875927d7eef src/runtime/constructors.rs
|
8d2443df8dab1ce6f11878e991b4928046b7362c417c5703f6d1f3a977646cbe src/runtime/constructors.rs
|
||||||
31dc5cdea00efd63594dc6becd5a9103594696145cbc8e7b7594fb407214ad9d src/runtime/docker.rs
|
31dc5cdea00efd63594dc6becd5a9103594696145cbc8e7b7594fb407214ad9d src/runtime/docker.rs
|
||||||
e90df3fff8a25465425b099ae358d19085809e56d948ba47e01e53f8b93d5dd0 src/runtime/tests.rs
|
e90df3fff8a25465425b099ae358d19085809e56d948ba47e01e53f8b93d5dd0 src/runtime/tests.rs
|
||||||
0046525317ffbd5d3b34845656b0f58d9d31f38b3d8acd71e90d94bd7cdfb8ab src/runtime/types.rs
|
0046525317ffbd5d3b34845656b0f58d9d31f38b3d8acd71e90d94bd7cdfb8ab src/runtime/types.rs
|
||||||
@@ -441,8 +443,9 @@ f2f6196632b2fdb2444189624f961edaf5bdfff57f700b0207b57e2491c1daa5 src/server/htt
|
|||||||
71899a9b1897fa22ab9e8eb6a670054a6d382bad78714006b9b218d07ba99902 src/server/serve.rs
|
71899a9b1897fa22ab9e8eb6a670054a6d382bad78714006b9b218d07ba99902 src/server/serve.rs
|
||||||
e9e478cf45b88994eaffd115f51764b6b07852ee3c941cafdabb20584d3dabc0 src/server/serve/tests.rs
|
e9e478cf45b88994eaffd115f51764b6b07852ee3c941cafdabb20584d3dabc0 src/server/serve/tests.rs
|
||||||
048f2c7a8ef698a172ba7ed844558f4118b367b2cf340748c5c28da7723d8e85 src/server/tls_runtime.rs
|
048f2c7a8ef698a172ba7ed844558f4118b367b2cf340748c5c28da7723d8e85 src/server/tls_runtime.rs
|
||||||
623e1b24c17f2149c712bc9fe38ca23c503e88891f5eef1b5ceef6cbf8cf9093 src/settings.rs
|
73ec453ac2850f6963f5bc33401cb8c92aa26d5123f15d68e6633860d91f265e src/settings.rs
|
||||||
3b3893d899b8e82969588f30c1e0aeff250449e4c94627ff5ad94d29f227ceca src/settings/tests.rs
|
240ecdd70e8662ff3b84c21830dc0cbbbacadd755d56146e54214bc2887f068c src/settings/public_origin.rs
|
||||||
|
1f1121c3562c8f8f3fdee2113be2afffb8849be2d303e4c4409f87478b6e3307 src/settings/tests.rs
|
||||||
566daf9c0e1a30da2dc18e51090541a85ced3320a0fc00ed1d1de3e2b4da5072 src/stream.rs
|
566daf9c0e1a30da2dc18e51090541a85ced3320a0fc00ed1d1de3e2b4da5072 src/stream.rs
|
||||||
46d9f63ce13a634e00f38d36ca3bdb217f2e37f04d75a4ef118e7dac9896a489 src/stream/model.rs
|
46d9f63ce13a634e00f38d36ca3bdb217f2e37f04d75a4ef118e7dac9896a489 src/stream/model.rs
|
||||||
9c59bdb856106c0a74d1f8c156b0dd7289a170322ea556bad19bb8de3bf13d69 src/stream/runtime.rs
|
9c59bdb856106c0a74d1f8c156b0dd7289a170322ea556bad19bb8de3bf13d69 src/stream/runtime.rs
|
||||||
@@ -522,28 +525,31 @@ eecf680f001f2110dde4eceaccf2f922859a16c946a644f8854e8de4d3771f7c tests/policies
|
|||||||
728999110a6d8cacb62bc0399bc333fa23a66526ee23f563a3c93bf00e5fad19 tests/transport_config.rs
|
728999110a6d8cacb62bc0399bc333fa23a66526ee23f563a3c93bf00e5fad19 tests/transport_config.rs
|
||||||
b04362ad082a32c9ccc4cf38c46f44d71347adecf7aba3224b3af3ea4872e3d0 tools/README.md
|
b04362ad082a32c9ccc4cf38c46f44d71347adecf7aba3224b3af3ea4872e3d0 tools/README.md
|
||||||
358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py
|
358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py
|
||||||
|
1a7ff9b28ade57b99d054c703496b51949dd1a8e7ed67bc5630f94a7df258cb5 tools/checks/admin_panel_config_0314_ui_test.mjs
|
||||||
|
e5c58fc9b7685ec911bfa86f6ca4f0cc97c19a90c0cff5b60ea9887f1afe9ced tools/checks/admin_public_origin_0314_config_sim.mjs
|
||||||
|
0060f8217640403c6af2d5091fdc9087d0fb2fc30cd545a3c58277a396d8121c tools/checks/admin_public_origin_0314_sim.mjs
|
||||||
fd082812411cff7afb71b2289a6f1a469b8d3c8a7cfdcdd9e567a2a4baf81be4 tools/checks/admin_surface_audit_0310.py
|
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
|
||||||
bebe46609381f2050d90c809e9b5ac7b564d5b26f733a926f20e5363a4140d34 tools/checks/check_source.py
|
54c73f3c0a63d0345bbe14a538503a9c63b6a9ff1925e7e51acbce38abd370e9 tools/checks/check_source.py
|
||||||
0b583ee59dcccc35b15ef4e211d7e6546a8aaa7b0286ceab03c0b0dc084c1e07 tools/checks/dashboard_boot_0311_ui_test.mjs
|
0b583ee59dcccc35b15ef4e211d7e6546a8aaa7b0286ceab03c0b0dc084c1e07 tools/checks/dashboard_boot_0311_ui_test.mjs
|
||||||
259d4b0985c82b631e151a07e8ef3b4c760019cb99bb2af458ce577497f7df58 tools/checks/dashboard_startup_0310_ui_test.mjs
|
259d4b0985c82b631e151a07e8ef3b4c760019cb99bb2af458ce577497f7df58 tools/checks/dashboard_startup_0310_ui_test.mjs
|
||||||
6adc94ee148508a2e46f073eb6f3b89c4057983dea12c976ae7332fb8423ce41 tools/checks/dependency_upgrade_0312.py
|
76884f3c44f18c36eff3a1968aa173d2d782fa8d31ed7cc63e4ff41853488672 tools/checks/dependency_upgrade_0312.py
|
||||||
dd7b7512f5a988c1fe6fdbdeb78c3b0d33a23b57335eb4f881296cb252d8692c tools/checks/docs_check.py
|
9add103198795111ccf1b3197cdd2586f54f606d152ff92be2ae09beb628d5aa 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
|
af98579ee942e774d88775c261ac351847c28bb4a5396dc67e458bd6b1ff7963 tools/checks/origin_resume_0313_ui_test.mjs
|
||||||
5b7901747d879e12a39ed82b6efc513bcdddd15d508b5828210d2c83398fb6a4 tools/checks/panel_cache_http_0310_ui_test.mjs
|
b432ea24933ba28ae819d900892f7ada268560e2816234820394084d6296b595 tools/checks/panel_cache_http_0310_ui_test.mjs
|
||||||
7770cc0b1b70255003d19b63210cc3e21ca9c8804cb56c2fe01eaf2e04ebb0fa tools/checks/preflight.py
|
300c114c05f66be214bc2e06187fc4d0063e3dc3cd55f44c43c2e43cbd3106f2 tools/checks/preflight.py
|
||||||
021a97bd6763b59f3f7eca15f51efdf88d0c863f1f81a2e42ba80a9a09ca3fec tools/checks/refresh_0310_ui_test.mjs
|
021a97bd6763b59f3f7eca15f51efdf88d0c863f1f81a2e42ba80a9a09ca3fec tools/checks/refresh_0310_ui_test.mjs
|
||||||
951d184c67cf3b006f89dc6588f460797fcad9c15460f6638abd8316f8777736 tools/checks/release_smoke.py
|
e24a81c06deceab7f4d3c3bf596712d3eeb70b884c731820637461fbc93aa975 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
|
||||||
0d211440ae77f16187a5d833156911c7793ed27ac1d77e79de362e47c1ab1abd tools/checks/source_module_contracts.py
|
0d211440ae77f16187a5d833156911c7793ed27ac1d77e79de362e47c1ab1abd tools/checks/source_module_contracts.py
|
||||||
51afd5f31eeea8f1658d239dd34637d38a0a7eb1fb9f58a212040505d3b39b0c tools/checks/system_info_0301_ui_test.mjs
|
b87c6c5b5d5335e95b3afc00f7c9035ddccc5ca1394a4425abcf898092da053a 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
|
||||||
161637cb5e2e613e3c71cf8489cc936fa84913a2a02649463f8893a28a674242 tools/checks/vhost_origin_errors_0297_ui_test.mjs
|
161637cb5e2e613e3c71cf8489cc936fa84913a2a02649463f8893a28a674242 tools/checks/vhost_origin_errors_0297_ui_test.mjs
|
||||||
@@ -552,7 +558,7 @@ adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/s
|
|||||||
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
|
||||||
cfa2dee224a11bf3948dbe528f5877a76b830b7efb01cd785007213cc245493d tools/dev/memory_load.py
|
ddee9083a349b2a56b9ebe5742b5c91b668f31143762bac7d939068fbd2ee7a6 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
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ if [[ "$mode" == "test" ]]; then
|
|||||||
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/origin_resume_0313_ui_test.mjs
|
||||||
|
node tools/checks/admin_public_origin_0314_sim.mjs
|
||||||
|
node tools/checks/admin_public_origin_0314_config_sim.mjs
|
||||||
|
node tools/checks/admin_panel_config_0314_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
|
||||||
|
|||||||
+3
-3
@@ -65,9 +65,9 @@ The `rustproxy_session` cookie is HttpOnly, SameSite=Strict and Path=/ with the
|
|||||||
administration adds the Secure attribute; behind a trusted HTTPS-terminating reverse proxy
|
administration adds the Secure attribute; behind a trusted HTTPS-terminating reverse proxy
|
||||||
the external forwarded scheme controls that flag. A protected SSH/VPN tunnel remains valid,
|
the external forwarded scheme controls that flag. A protected SSH/VPN tunnel remains valid,
|
||||||
while direct remote administration should use HTTPS plus firewall/private-network policy.
|
while direct remote administration should use HTTPS plus firewall/private-network policy.
|
||||||
Direct IP/localhost Host values must use the actual administration port. A DNS name explicitly
|
Direct IP/localhost Host values must use the actual administration port. A DNS name explicitly listed in `administration.allowed_hosts`, or automatically
|
||||||
listed in `administration.allowed_hosts` represents the browser-facing authority and may use
|
derived from the validated `administration.public_origin`, represents the
|
||||||
the trusted reverse proxy's external port. Foreign Origin/cross-site requests are rejected.
|
browser-facing authority and may use the trusted reverse proxy's external port. Foreign Origin/cross-site requests are rejected.
|
||||||
There is no permissive CORS mode.
|
There is no permissive CORS mode.
|
||||||
Auth request bodies are limited to 4096 bytes.
|
Auth request bodies are limited to 4096 bytes.
|
||||||
|
|
||||||
|
|||||||
+5
-4
@@ -9,16 +9,17 @@ new bind address and allow explicit DNS hostnames. A persisted non-loopback bind
|
|||||||
accepted when HTTPS is enabled. The legacy startup flags remain only for deliberately
|
accepted when HTTPS is enabled. The legacy startup flags remain only for deliberately
|
||||||
plain remote HTTP. Public vhost `/docs` paths are not captured by the admin listener.
|
plain remote HTTP. Public vhost `/docs` paths are not captured by the admin listener.
|
||||||
|
|
||||||
When the dashboard is published through a trusted reverse proxy, an explicit
|
When the dashboard is published through a trusted reverse proxy, the hostname of
|
||||||
`administration.allowed_hosts` DNS entry is matched against the external authority rather
|
the optional `administration.public_origin` is automatically allowed for panel
|
||||||
than the private admin socket port. `X-Forwarded-Host` is trusted only from loopback or a
|
Host validation. Any extra `administration.allowed_hosts` DNS entry is also
|
||||||
|
matched against the external authority rather than the private admin socket port. `X-Forwarded-Host` is trusted only from loopback or a
|
||||||
configured trusted proxy; the external `X-Forwarded-Proto` additionally requires the
|
configured trusted proxy; the external `X-Forwarded-Proto` additionally requires the
|
||||||
forwarded-proto trust setting except for loopback. Origin/CSRF checks and Secure cookies use
|
forwarded-proto trust setting except for loopback. Origin/CSRF checks and Secure cookies use
|
||||||
that external origin. Direct IP/localhost access still requires the real administration port.
|
that external origin. Direct IP/localhost access still requires the real administration port.
|
||||||
|
|
||||||
### Publishing the panel through RustProxy itself
|
### Publishing the panel through RustProxy itself
|
||||||
|
|
||||||
0.16.0 supports the explicitly opted-in same-process topology where a public HTTPS vhost proxies to the local administration listener. The reserved `rustproxy_session` cookie is passed through only when all of these conditions are true: the browser-facing request is TLS, its DNS hostname is explicitly present in `administration.allowed_hosts`, the selected backend directly addresses the configured administration socket by IP, and that backend has `accept_local_listener_risk=true`. The control-plane route is forced out of shared cache.
|
0.16.0 supports the explicitly opted-in same-process topology where a public HTTPS vhost proxies to the local administration listener. The reserved `rustproxy_session` cookie is passed through only when all of these conditions are true: the browser-facing request is TLS, its DNS hostname either matches the pinned `administration.public_origin` or is explicitly listed in `administration.allowed_hosts`, the selected backend directly addresses the configured administration socket by IP, and that backend has `accept_local_listener_risk=true`. The control-plane route is forced out of shared cache.
|
||||||
|
|
||||||
This is a narrow exception to the normal isolation rule. For every ordinary backend RustProxy still removes `rustproxy_session` before forwarding requests and strips an upstream attempt to set that cookie in responses. DNS backend names are not accepted for the special self-admin cookie path. This prevents a normal application backend from reading or replacing the administration session.
|
This is a narrow exception to the normal isolation rule. For every ordinary backend RustProxy still removes `rustproxy_session` before forwarding requests and strips an upstream attempt to set that cookie in responses. DNS backend names are not accepted for the special self-admin cookie path. This prevents a normal application backend from reading or replacing the administration session.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# RustProxy 0.31.4 - Administration-panel Origin pinning and simplified HTTPS settings
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Only the **administration panel/control-plane authentication** and its settings,
|
||||||
|
OpenAPI, translations and regression tests change. No public vhost/proxy routing,
|
||||||
|
backend forwarding, stream, session-store or database schema behavior is altered.
|
||||||
|
Credential-based certificate API automation remains on its existing validation
|
||||||
|
path. The 0.31.3 BFCache, manual retry, SSE/polling and startup error protection
|
||||||
|
remains unchanged.
|
||||||
|
|
||||||
|
## Evidence and failure mode
|
||||||
|
|
||||||
|
0.31.3's `external_admin_scheme` rejects a trusted
|
||||||
|
`X-Forwarded-Proto: https, http` value with `auth.invalid_origin` and reason
|
||||||
|
`external_scheme` before checking a session, even for `GET /api/auth/session`
|
||||||
|
without an `Origin` header. This may appear intermittently if a multi-hop
|
||||||
|
reverse proxy sometimes appends rather than replaces the header. The previous
|
||||||
|
Node.js HTTP reconstruction reproduced this with 3/12 intentionally malformed
|
||||||
|
GETs; it does **not** prove the real user's reverse proxy behaves that way.
|
||||||
|
|
||||||
|
## Patch design
|
||||||
|
|
||||||
|
`administration.public_origin` is an **optional, explicit HTTPS origin**:
|
||||||
|
|
||||||
|
- Requires one canonical DNS hostname; that hostname is derived for the effective
|
||||||
|
panel Host allowlist without repeating it in `administration.allowed_hosts`.
|
||||||
|
The setting forbids credentials, userinfo, path, query, fragment, HTTP and ambiguous input.
|
||||||
|
- Is active only after restart and only when the immediate TCP peer is loopback
|
||||||
|
or is already in the configured trusted-proxy list. It does not implicitly
|
||||||
|
trust another proxy or the browser's `Host`/forwarding headers.
|
||||||
|
- Keeps checking the actual request `Host` against the administration listener
|
||||||
|
policy. If a forwarded Host exists, it must be a **single** authority and
|
||||||
|
match the pinned host and effective port. Malformed/userinfo/duplicate values
|
||||||
|
are rejected.
|
||||||
|
- For the pinned panel, accepts a bounded (maximum eight hops) protocol chain
|
||||||
|
only if the *first* hop is literally `https` and every hop is `http` or
|
||||||
|
`https`. This permits `https, http` but not `http`, `http, https`, `ftp`,
|
||||||
|
empty values, or arbitrarily long chains. Missing XFP is allowed only
|
||||||
|
because the explicit administrator pin defines the external HTTPS identity.
|
||||||
|
- Uses the configured origin for the browser `Origin` comparison, correct
|
||||||
|
`Secure` session cookie and the existing CSRF checks. Existing
|
||||||
|
`Sec-Fetch-Site: cross-site` blocking is unchanged.
|
||||||
|
- Does **not** alter the validation path when the pin is absent, the peer is
|
||||||
|
untrusted, or certificate-automation credentials are present.
|
||||||
|
|
||||||
|
On every rejection, the existing bounded, credential-redacted diagnostics
|
||||||
|
remain available. New `pinned_origin_mismatch` identifies a disagreement with
|
||||||
|
the explicit pin. Do not log cookies, CSRF tokens or Authorization headers.
|
||||||
|
|
||||||
|
## Simplified hostname configuration (included in this 0.31.4 reissue)
|
||||||
|
|
||||||
|
### Operator change
|
||||||
|
|
||||||
|
For the panel behind a trusted reverse proxy, **Settings -> Administration**
|
||||||
|
now needs only the **Public panel HTTPS origin** such as
|
||||||
|
`https://panel.example.test`. The DNS hostname comes from this validated
|
||||||
|
administrator setting; it need not (and should not) be repeated in
|
||||||
|
`administration.allowed_hosts`. Additional DNS hostnames remain in an optional
|
||||||
|
**Advanced** section. Only those extra names are stored in the allowlist.
|
||||||
|
|
||||||
|
Backend validation also canonicalizes any duplicate hostname in memory.
|
||||||
|
The settings page suppresses this value when loading an older configuration and omits it on the next save.
|
||||||
|
Other explicitly allowed hosts remain. Removing/changing the pin does not automatically retain the old
|
||||||
|
hostname as an extra allowlist entry. For legacy duplicated values, this
|
||||||
|
normalization occurs **only when the administrator saves**; there is no
|
||||||
|
silent SQLite mutation on startup.
|
||||||
|
|
||||||
|
### Configuration example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"administration": {
|
||||||
|
"public_origin": "https://panel.example.test",
|
||||||
|
"allowed_hosts": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This field still requires an HTTPS URL with one DNS hostname and an optional
|
||||||
|
port; userinfo, fragment, path, HTTP, IP literals and localhost remain invalid.
|
||||||
|
Changing either access-control field needs a RustProxy restart.
|
||||||
|
|
||||||
|
### Implementation and security
|
||||||
|
|
||||||
|
- `Administration::validate` normalizes the public URL independently of the
|
||||||
|
optional additional DNS list. `Administration::include_public_origin_host`
|
||||||
|
generates an **effective runtime/desired** Host allowlist once on startup /
|
||||||
|
status read, without adding to the persisted configuration or accepting a
|
||||||
|
wildcard. The originally saved allowlist is unchanged.
|
||||||
|
- `admin_host_allowed` and `configured_admin_public_authority` continue to
|
||||||
|
check the actual Host and the trusted last-hop proxy. Forwarded Host remains
|
||||||
|
single and identical to the pin. Only a bounded, first-hop HTTPS
|
||||||
|
X-Forwarded-Proto chain can be used with the explicit pin; no arbitrary
|
||||||
|
Origin or `Sec-Fetch-Site: cross-site` or missing CSRF is permitted.
|
||||||
|
- The explicitly opted-in self-admin vhost case may pass the reserved session
|
||||||
|
cookie when the incoming HTTPS host equals the pin; it still requires a
|
||||||
|
backend targeting the administration listener directly by IP and the prior
|
||||||
|
`accept_local_listener_risk` opt-in. No other proxy traffic changes.
|
||||||
|
- No new configuration key or SQLite migration. Older `allowed_hosts` lists
|
||||||
|
remain accepted, preserving backwards compatibility. Existing 0.31.3
|
||||||
|
mobile/BFCache recovery is untouched.
|
||||||
|
|
||||||
|
### Tests and acceptance gate
|
||||||
|
|
||||||
|
New Rust source tests also cover pin-only saved configuration, derived runtime
|
||||||
|
allowlist, additional legacy entries, changes/removal, invalid URL input,
|
||||||
|
same-origin session and `Secure` cookie, foreign Origin, untrusted peers,
|
||||||
|
forwarded-host spoofing and CSRF. Node.js runs a focused real settings-helper
|
||||||
|
test and a local HTTP simulation of the guard; these **do not** execute the
|
||||||
|
Rust server. Exact results are recorded in [TEST_REPORT](TEST_REPORT.md).
|
||||||
|
|
||||||
|
Production acceptance remains OPEN until Rust 1.99+ builds/tests pass with a
|
||||||
|
resolved Cargo.lock; a trusted reverse proxy and real mobile Chrome/Safari
|
||||||
|
are checked for repeated BFCache resumes, session expiry and negative
|
||||||
|
Origin/Host/CSRF cases. Never weaken the trusted-proxy list or disable Origin
|
||||||
|
validation to avoid `auth.invalid_origin`.
|
||||||
|
|
||||||
|
## Test coverage
|
||||||
|
|
||||||
|
Rust source tests in `src/settings/tests.rs` and `src/admin/tests/part_11.rs`
|
||||||
|
cover config parsing/normalization, persisted legacy defaults, restart gating,
|
||||||
|
trusted/foreign hosts, unknown/duplicate headers, cross-site metadata, HTTP
|
||||||
|
first hops, genuine same-origin session GET with multi-hop metadata, HTTPS
|
||||||
|
`Secure` session cookie, CSRF enforcement and opt-out behavior.
|
||||||
|
|
||||||
|
`tools/checks/admin_public_origin_0314_sim.mjs` and
|
||||||
|
`tools/checks/admin_public_origin_0314_config_sim.mjs` perform additional HTTP
|
||||||
|
guard *simulations*, not compiled RustProxy execution.
|
||||||
|
`tools/checks/admin_panel_config_0314_ui_test.mjs` checks the settings UI and form behavior. Record its result in
|
||||||
|
`docs/TEST_REPORT.md` only after it is run.
|
||||||
|
|
||||||
|
## Required production acceptance
|
||||||
|
|
||||||
|
1. `bash scripts/resolve_dependencies_0312.sh` on a Rust 1.99+ builder with
|
||||||
|
network/local audited dependencies; inspect regenerated lockfile.
|
||||||
|
2. `./dev.sh check && ./dev.sh test && ./dev.sh release` and Go tests.
|
||||||
|
3. Set an exact HTTPS panel origin (no duplicate DNS allowlist entry), restart, and verify
|
||||||
|
with a trusted real edge proxy. Keep RustProxy administration listening on
|
||||||
|
an isolated/private socket. Verify the first trusted hop sanitizes headers.
|
||||||
|
4. On iOS Safari and Android Chrome, perform 20+ visibility/BFCache resumes,
|
||||||
|
full refreshes, sessions renewed/expired, and a deliberate temporary 403.
|
||||||
|
Check HTTP 200 for valid `/api/auth/session` and `Secure` cookie; no reload
|
||||||
|
loops, blank screens, request floods, or leaked SSE/polling tasks.
|
||||||
|
5. Inject cross-origin, duplicate/foreign forwarded Host, downgraded protocol,
|
||||||
|
missing CSRF and untrusted-source requests; they must still fail. Confirm
|
||||||
|
certificate API automation and public vhosts behave identically to 0.31.3.
|
||||||
|
|
||||||
|
The original production cause cannot be declared fixed without representative
|
||||||
|
mobile/reverse-proxy request logs. This artifact is not compiler-verified unless
|
||||||
|
the release-host gate above passes.
|
||||||
+2
-1
@@ -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.4 administration public Origin and settings](IMPLEMENTATION_0.31.4.md) - opt-in HTTPS origin, one-field DNS allowlist, additional hosts in advanced settings, legacy deduplication, CSRF and remaining deployment checks.
|
||||||
- [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.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.
|
||||||
@@ -82,7 +83,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.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.
|
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.4.md](IMPLEMENTATION_0.31.4.md); [IMPLEMENTATION_0.31.3.md](IMPLEMENTATION_0.31.3.md) is retained as the previous mobile recovery milestone; [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.
|
||||||
|
|
||||||
|
|||||||
@@ -114,12 +114,78 @@ accepted only when `administration.tls` is true. Plain remote HTTP remains avail
|
|||||||
"tls": true,
|
"tls": true,
|
||||||
"certificate_id": "admin-example",
|
"certificate_id": "admin-example",
|
||||||
"allowed_hosts": ["admin.example.com"],
|
"allowed_hosts": ["admin.example.com"],
|
||||||
|
"public_origin": null,
|
||||||
"session_days": 30,
|
"session_days": 30,
|
||||||
"panel_compression": {"zstd": true, "br": true, "gzip": true}
|
"panel_compression": {"zstd": true, "br": true, "gzip": true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Fixed public origin for a reverse-proxied panel (0.31.4)
|
||||||
|
|
||||||
|
For a **panel** reached through a trusted reverse proxy, set the optional
|
||||||
|
`administration.public_origin` in **Settings → Administration** to the exact
|
||||||
|
browser-facing HTTPS origin (for example `https://panel.example.test`).
|
||||||
|
**Do not duplicate that hostname in `administration.allowed_hosts`.** Its DNS
|
||||||
|
hostname is implicitly included in the runtime panel Host allowlist. **Restart
|
||||||
|
RustProxy**; this is a restart-bound access-control setting. Omit it (`null`) for
|
||||||
|
direct administration or when forwarded headers are canonical. This setting
|
||||||
|
is not a wildcard, does not accept HTTP or a URL path, and requires a DNS
|
||||||
|
hostname rather than an IP address. SQLite schema 9 remains unchanged.
|
||||||
|
|
||||||
|
The advanced **Additional allowed DNS hostnames** field controls independent,
|
||||||
|
extra hostnames only. Older configurations containing both values are
|
||||||
|
accepted; on editing/saving the redundant duplicate is omitted from the
|
||||||
|
persisted list. If `public_origin` changes or is cleared, its old hostname is
|
||||||
|
no longer implicitly allowed (unless explicitly retained as an additional host).
|
||||||
|
|
||||||
|
Example for the default **private loopback** administration listener behind an
|
||||||
|
HTTPS-terminating proxy:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"administration": {
|
||||||
|
"allowed_hosts": [],
|
||||||
|
"public_origin": "https://panel.example.test"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The last hop must connect from loopback or an existing explicitly trusted proxy
|
||||||
|
CIDR. If it is not loopback, configure **only the actual proxy peer CIDR** under
|
||||||
|
Forwarding → Trusted proxies; be aware the general trusted-proxy setting also
|
||||||
|
applies to public traffic and must never include untrusted clients. The
|
||||||
|
administration listener should not be reachable directly from external clients.
|
||||||
|
Do **not** grant trust to public networks to work around Origin failures.
|
||||||
|
|
||||||
|
The pinned URL, not an element of `X-Forwarded-Proto`, defines the panel
|
||||||
|
scheme/authority. A trusted, bounded proxy chain `https, http` (HTTPS on the
|
||||||
|
browser side, HTTP on an internal hop) is permitted in this explicit mode.
|
||||||
|
An `http` first hop, malformed/too-long protocol chain, duplicate/conflicting
|
||||||
|
`X-Forwarded-Host`, different `Origin`, `Sec-Fetch-Site: cross-site`, and missing
|
||||||
|
CSRF protection for state-changing requests still result in HTTP 403. The
|
||||||
|
public proxy/vhost data plane is **not changed**, except that an already opted-in
|
||||||
|
self-admin vhost can recognize the pinned hostname for its reserved panel session
|
||||||
|
cookie; other upstreams continue to strip it. Credential-bearing certificate
|
||||||
|
automation retains its original TLS/forwarding checks.
|
||||||
|
|
||||||
|
Prefer fixing the proxy's forwarding headers rather than configuring the pin
|
||||||
|
when practical. For a TLS-only public entry, the closest trusted hop should
|
||||||
|
**replace**, not append, forwarded headers using known values, for example:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
proxy_set_header Host panel.example.test;
|
||||||
|
proxy_set_header X-Forwarded-Host panel.example.test;
|
||||||
|
proxy_set_header X-Forwarded-Proto https;
|
||||||
|
```
|
||||||
|
|
||||||
|
Only use that `https` setting on an entry that actually verifies external TLS;
|
||||||
|
never trust an arbitrary inbound `X-Forwarded-Proto`. If multiple intermediate
|
||||||
|
proxy hops must append protocol values, the pinned mode provides an explicit
|
||||||
|
server-side comparison target without accepting a client-supplied origin as
|
||||||
|
authoritative. Confirm deployment using a real mobile browser and the safe
|
||||||
|
`administration origin rejected` logs before production rollout.
|
||||||
|
|
||||||
**Administration → Panel content compression** (next to the admin listener/TLS settings)
|
**Administration → Panel content compression** (next to the admin listener/TLS settings)
|
||||||
controls Zstd, Brotli and gzip for the authenticated HTML document, JavaScript,
|
controls Zstd, Brotli and gzip for the authenticated HTML document, JavaScript,
|
||||||
CSS and language bundles. Each browser request is negotiated using
|
CSS and language bundles. Each browser request is negotiated using
|
||||||
|
|||||||
+27
-1
@@ -1,4 +1,30 @@
|
|||||||
# Verification report - RustProxy 0.31.3
|
# Verification report - RustProxy 0.31.4
|
||||||
|
|
||||||
|
## 0.31.4 reissue - origin protection and one-field administration public URL (2026-10-11)
|
||||||
|
|
||||||
|
- **Reissue verification (2026-10-11):** rebased the complete previously prepared simplified-config source onto release identifier `0.31.4` in the manifest, existing resolved-lock snapshot package entry, OpenAPI, source/version checks, documentation and deterministic ZIP metadata. Merged the panel Origin and one-field config release notes; no backend validation logic changes were introduced during this renumbering.
|
||||||
|
- **PASS — executed for this reissue:** `version_contracts.py`, `docs_check.py`, `langs_check.py`, `check_source.py`, `release_smoke.py`, `security_contracts.py`, `dependency_upgrade_0312.py`, `rust_module_contracts.py`, `system_info_no_subprocess_contracts.py`, `source_module_contracts.py`, `memory_contracts.py`, `web_server_contracts.py`, `admin_surface_audit_0310.py`, `security_dependency_contracts.py`; 15 standalone Node frontend/simulation scripts (including the updated original 53-case and expanded 61-case administration simulations and 9 UI scenarios); `go test ./...` for `docker-agent`; `bash -n dev.sh` and OpenAPI JSON parsing.
|
||||||
|
- **Not executed for this reissue:** full Rust compile/test/release (`cargo` and `rustc` absent), real mobile Safari/Chrome or live reverse-proxy verification. Passing Node simulations is **not** proof of compiled Rust behavior. The inherited Cargo.lock requires verification and/or re-resolution on the declared Rust 1.99+ toolchain.
|
||||||
|
|
||||||
|
- **Scope:** panel administration settings/config and narrow self-admin cookie opt-in only. `allowed_hosts` stores only additional DNS hostnames; the HTTPS pin contributes its hostname to the active/desired Host allowlist without persisting a duplicate.
|
||||||
|
- **Compatibility:** older configurations with a duplicate hostname remain valid; validation canonicalizes the in-memory config and the next save omits the duplicate without an automatic SQLite migration; changing/removing the pin does not silently retain a stale implicit hostname.
|
||||||
|
- **Security:** Host, single forwarded Host, trusted reverse proxy, first-hop HTTPS, CSRF and `Origin`/cross-site guard rules are unchanged. No SQLite schema migration, TLS listener or other vhost route changes.
|
||||||
|
- **PASS — source/release gates:** `version_contracts.py`, `rust_module_contracts.py`, `check_source.py`, `release_smoke.py`, `docs_check.py`, `langs_check.py`, `security_contracts.py`, `admin_surface_audit_0310.py`, `security_dependency_contracts.py`, and `dependency_upgrade_0312.py`; SQLite schema-9/upgrade smoke and the source/HTML/Reader/translation checks also passed.
|
||||||
|
- **PASS — focused simulated regressions:** `admin_public_origin_0314_config_sim.mjs`: **61/61 local Node HTTP guard-simulation assertions**, comprising 37 cases and 24 repeated session GETs. Invalid forwarded-proto on 3/12 unpinned GETs produced 403; with a pinned HTTPS public URL and **no duplicate DNS allowlist entry**, the same sequence produced 0/12. This simulation is not compiled RustProxy and cannot prove actual mobile/proxy headers.
|
||||||
|
- **PASS — real frontend helper/UI checks:** `admin_panel_config_0314_ui_test.mjs`: **9/9 settings-normalization scenarios** plus form/HTML/locale/OpenAPI consistency; `panel_cache_http_0310_ui_test.mjs` also executes the updated real administration form callback with a duplicated legacy hostname and verifies it is omitted on save.
|
||||||
|
- **PASS — other standalone checks:** all 14 bundled Node UI/simulation scripts, memory model (300,000 simulated PURGEs), 2,000 HTTP requests to a **Python mock peer** (not RustProxy), and `cd docker-agent && go test ./...`.
|
||||||
|
- **Harness and compiler limitations:** an initial `./dev.sh test` run exposed a legacy UI-test harness that lacked the new form helper; the harness was corrected and passed separately. A subsequent monolithic run exceeded the execution time limit after the memory/load-model stage, so no complete `./dev.sh test` success is claimed. `./dev.sh check` exited **127** because `cargo`/`rustc` are not installed. New Rust tests in `src/settings/tests.rs` and `src/admin/tests/part_11.rs` were **not compiled/executed**. The inherited Cargo.lock still needs resolution on Rust 1.99+; live mobile and real reverse-proxy acceptance remain OPEN.
|
||||||
|
|
||||||
|
### Earlier 0.31.4 validation baseline (2026-10-11)
|
||||||
|
|
||||||
|
- **User scope:** admin panel only; public vhosts, proxy forwarding, streams and database migrations are unchanged. The 0.31.3 BFCache/session resume and visible retry protections are retained.
|
||||||
|
- **Source correction:** optional `administration.public_origin` pins a browser-facing HTTPS panel origin to a configured DNS allowlist; it takes effect after restart and only with a trusted last-hop proxy. It accepts bounded `X-Forwarded-Proto: https, http` without relying on an ambiguous forwarded-proto element for the expected origin. Without the pin, 0.31.3's strict handling remains.
|
||||||
|
- **Security:** actual Host is validated, forwarded Host must be single/matching, the protocol's first hop must be HTTPS, and Origin, cross-site and CSRF checks remain. Untrusted peers and credential-bearing certificate automation retain their previous scheme validation. The earlier build required explicit `allowed_hosts`; the reissue now derives the pinned hostname without wildcard trust.
|
||||||
|
- **Node HTTP guard simulation (not compiled RustProxy): PASS, 53/53 assertions**, including 29 individual scenarios and 24 repeated session GETs: without the pin and with intentionally malformed XFP every fourth request, **3/12 GETs returned 403**; with an explicit pin and otherwise identical simulated headers, **0/12 returned 403**. `tools/checks/admin_public_origin_0314_sim.mjs` checks relevant code anchors and operates a local Node HTTP test peer. This does not confirm production headers or backend runtime behavior.
|
||||||
|
- **PASS (2026-10-11):** `./dev.sh test` passed every available Rust-independent stage: source-module boundaries, version/contracts, SQLite schema-9 migrations and upgrade, security source/dependency checks, 300,000 PURGE-model iterations, 2,000 real HTTP requests to a **Python test peer**, all bundled Node/browser UI regressions (including BFCache/resume), 0.31.4 HTTP simulation, documentation and both language packs. Exit **127** at the missing Rust/Cargo toolchain guard only. The simulated peer is **not** a running RustProxy binary. `./dev.sh check` likewise exits 127 due to missing Cargo.
|
||||||
|
- **PASS:** `(cd docker-agent && go test ./...)`, `node --check tools/checks/admin_public_origin_0314_sim.mjs`; source-package size/integrity checks are performed separately.
|
||||||
|
- **Rust regression tests added, NOT EXECUTED:** `src/settings/tests.rs` and `src/admin/tests/part_11.rs` cover pin validation, restart, strict opt-out, session Secure cookie, CSRF, foreign Origin, cross-site, host spoofing and untrusted peers.
|
||||||
|
- **Remaining release gate OPEN:** the sandbox has no Rust/Cargo/rustfmt and the inherited lockfile remains unresolved against 0.31.2+ dependencies. Before production use: resolve on Rust 1.99+, format, clippy, test/build, then capture actual mobile Safari/Chrome and reverse-proxy traffic and run the acceptance matrix in [IMPLEMENTATION_0.31.4.md](IMPLEMENTATION_0.31.4.md). No claim is made that the original field `auth.invalid_origin` was directly observed or eliminated.
|
||||||
|
|
||||||
## 0.31.3 - invalid-Origin resume and mobile panel recovery (2026-10-11)
|
## 0.31.3 - invalid-Origin resume and mobile panel recovery (2026-10-11)
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -893,7 +893,7 @@
|
|||||||
"administrationCertificate": "Certificate",
|
"administrationCertificate": "Certificate",
|
||||||
"administrationGeneratedCertificate": "Application-generated default certificate",
|
"administrationGeneratedCertificate": "Application-generated default certificate",
|
||||||
"administrationHelp": "HTTPS allows the administration listener to bind beyond loopback. Plain HTTP on the HTTPS port is redirected automatically. Listener and certificate changes take effect after restart.",
|
"administrationHelp": "HTTPS allows the administration listener to bind beyond loopback. Plain HTTP on the HTTPS port is redirected automatically. Listener and certificate changes take effect after restart.",
|
||||||
"administrationHosts": "Allowed DNS hostnames (optional strict allowlist)",
|
"administrationHosts": "Additional allowed DNS hostnames",
|
||||||
"administrationHttps": "Enable HTTPS for administration",
|
"administrationHttps": "Enable HTTPS for administration",
|
||||||
"administrationListen": "Listen address",
|
"administrationListen": "Listen address",
|
||||||
"cacheOverhead": "Overhead",
|
"cacheOverhead": "Overhead",
|
||||||
@@ -2332,6 +2332,10 @@
|
|||||||
"cacheHttpInvalidationCurlNote": "PURGE removes an exact URL; BAN invalidates a path prefix for 1 hour. No admin cookie is needed, but your IP must be allowed. A reverse proxy must forward these methods and trusted client IP information.",
|
"cacheHttpInvalidationCurlNote": "PURGE removes an exact URL; BAN invalidates a path prefix for 1 hour. No admin cookie is needed, but your IP must be allowed. A reverse proxy must forward these methods and trusted client IP information.",
|
||||||
"cacheHttpInvalidationTitle": "HTTP BAN / PURGE allowlist",
|
"cacheHttpInvalidationTitle": "HTTP BAN / PURGE allowlist",
|
||||||
"cacheHttpInvalidationAddresses": "Permitted client IP addresses / CIDRs (one per line)",
|
"cacheHttpInvalidationAddresses": "Permitted client IP addresses / CIDRs (one per line)",
|
||||||
"cacheHttpInvalidationHelp": "Empty = disabled. PURGE /path?query removes exact matches; BAN /path prevents use of cached URLs with that prefix (default TTL 1 hour, optional X-RustProxy-Ban-Ttl). BAN / requires X-RustProxy-Confirm-Wide: 1. Only the matched Host and vhost are affected. Behind a reverse proxy, configure trusted proxies and sanitize forwarding headers."
|
"cacheHttpInvalidationHelp": "Empty = disabled. PURGE /path?query removes exact matches; BAN /path prevents use of cached URLs with that prefix (default TTL 1 hour, optional X-RustProxy-Ban-Ttl). BAN / requires X-RustProxy-Confirm-Wide: 1. Only the matched Host and vhost are affected. Behind a reverse proxy, configure trusted proxies and sanitize forwarding headers.",
|
||||||
|
"administrationPublicOrigin": "Public panel HTTPS origin (optional)",
|
||||||
|
"administrationHostsAdvanced": "Advanced: additional allowed DNS hostnames",
|
||||||
|
"administrationHostsHelp": "Enter only additional hostnames. The public panel URL is included automatically. Without that URL, this remains an independent strict allowlist.",
|
||||||
|
"administrationPublicOriginHelp": "Behind a trusted reverse proxy, enter the external HTTPS panel URL once. Its hostname is automatically allowed. Restart required; public proxy routing is unchanged."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+6
-2
@@ -893,7 +893,7 @@
|
|||||||
"administrationCertificate": "Certyfikat",
|
"administrationCertificate": "Certyfikat",
|
||||||
"administrationGeneratedCertificate": "Domyślny certyfikat wygenerowany przez aplikację",
|
"administrationGeneratedCertificate": "Domyślny certyfikat wygenerowany przez aplikację",
|
||||||
"administrationHelp": "HTTPS pozwala wystawić panel administracyjny poza loopback. Zwykły HTTP na porcie HTTPS jest automatycznie przekierowywany. Zmiany listenera i certyfikatu działają po restarcie.",
|
"administrationHelp": "HTTPS pozwala wystawić panel administracyjny poza loopback. Zwykły HTTP na porcie HTTPS jest automatycznie przekierowywany. Zmiany listenera i certyfikatu działają po restarcie.",
|
||||||
"administrationHosts": "Dozwolone hosty DNS (opcjonalna ścisła lista)",
|
"administrationHosts": "Dodatkowe dozwolone hosty DNS",
|
||||||
"administrationHttps": "Włącz HTTPS dla panelu administracyjnego",
|
"administrationHttps": "Włącz HTTPS dla panelu administracyjnego",
|
||||||
"administrationListen": "Adres nasłuchu",
|
"administrationListen": "Adres nasłuchu",
|
||||||
"cacheOverhead": "Narzut",
|
"cacheOverhead": "Narzut",
|
||||||
@@ -2332,6 +2332,10 @@
|
|||||||
"cacheHttpInvalidationCurlNote": "PURGE usuwa dokładny URL; BAN unieważnia prefiks ścieżki na 1 godzinę. Nie potrzeba ciasteczka administratora, lecz IP musi być na liście. Reverse proxy musi przekazywać te metody oraz poprawnie ustalone IP klienta.",
|
"cacheHttpInvalidationCurlNote": "PURGE usuwa dokładny URL; BAN unieważnia prefiks ścieżki na 1 godzinę. Nie potrzeba ciasteczka administratora, lecz IP musi być na liście. Reverse proxy musi przekazywać te metody oraz poprawnie ustalone IP klienta.",
|
||||||
"cacheHttpInvalidationTitle": "Lista dozwolonych adresów HTTP BAN / PURGE",
|
"cacheHttpInvalidationTitle": "Lista dozwolonych adresów HTTP BAN / PURGE",
|
||||||
"cacheHttpInvalidationAddresses": "Dozwolone adresy IP klientów / CIDR (jeden na wiersz)",
|
"cacheHttpInvalidationAddresses": "Dozwolone adresy IP klientów / CIDR (jeden na wiersz)",
|
||||||
"cacheHttpInvalidationHelp": "Pusta lista = wyłączone. PURGE /ścieżka?zapytanie usuwa dokładne trafienia; BAN /ścieżka blokuje buforowane adresy URL z tym prefiksem (domyślnie 1 godzina, opcjonalnie X-RustProxy-Ban-Ttl). BAN / wymaga X-RustProxy-Confirm-Wide: 1. Operacje dotyczą wyłącznie dopasowanego hosta i vhosta. Za reverse proxy skonfiguruj zaufane proxy i usuwanie fałszywych nagłówków przekazywania."
|
"cacheHttpInvalidationHelp": "Pusta lista = wyłączone. PURGE /ścieżka?zapytanie usuwa dokładne trafienia; BAN /ścieżka blokuje buforowane adresy URL z tym prefiksem (domyślnie 1 godzina, opcjonalnie X-RustProxy-Ban-Ttl). BAN / wymaga X-RustProxy-Confirm-Wide: 1. Operacje dotyczą wyłącznie dopasowanego hosta i vhosta. Za reverse proxy skonfiguruj zaufane proxy i usuwanie fałszywych nagłówków przekazywania.",
|
||||||
|
"administrationPublicOrigin": "Publiczny adres HTTPS panelu (opcjonalnie)",
|
||||||
|
"administrationHostsAdvanced": "Zaawansowane: dodatkowe dozwolone hosty DNS",
|
||||||
|
"administrationHostsHelp": "Wpisz wyłącznie dodatkowe domeny. Domena z publicznego adresu panelu jest dozwolona automatycznie. Bez tego adresu lista pozostaje niezależnym ścisłym ograniczeniem.",
|
||||||
|
"administrationPublicOriginHelp": "Za zaufanym reverse proxy wpisz tylko publiczny adres HTTPS panelu. Jego domena zostanie dozwolona automatycznie. Wymagany restart; ruch proxy bez zmian."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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.3' ]]; then
|
if [[ "$(python3 scripts/version.py)" != '0.31.4' ]]; then
|
||||||
printf 'Expected RustProxy 0.31.3 sources.\n' >&2
|
printf 'Expected RustProxy 0.31.4 sources.\n' >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
printf 'Toolchain: '; rustc --version
|
printf 'Toolchain: '; rustc --version
|
||||||
|
|||||||
@@ -229,6 +229,72 @@ fn external_admin_authority(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In pinned-origin mode accept only bounded, consistent proxy-hop metadata:
|
||||||
|
// the first hop must report browser HTTPS, later internal hops may use HTTP.
|
||||||
|
// We still do NOT pick a trusted scheme from these values; the explicit pin
|
||||||
|
// defines the panel origin. A clear public HTTP or unknown value fails closed.
|
||||||
|
fn pinned_admin_proto_compatible(headers: &HeaderMap) -> bool {
|
||||||
|
let mut hops = 0;
|
||||||
|
for value in headers.get_all("x-forwarded-proto").iter() {
|
||||||
|
let Ok(value) = value.to_str() else { return false; };
|
||||||
|
for part in value.split(',') {
|
||||||
|
hops += 1;
|
||||||
|
if hops > 8 { return false; }
|
||||||
|
let scheme = part.trim();
|
||||||
|
if (hops == 1 && scheme != "https")
|
||||||
|
|| !matches!(scheme, "http" | "https")
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A configured panel URL is authoritative only for an explicitly trusted
|
||||||
|
/// direct reverse-proxy peer. In this mode X-Forwarded-Proto is deliberately
|
||||||
|
/// ignored: conflicting hop values such as "https, http" are not evidence of
|
||||||
|
/// the browser scheme. The public URL comes from administrator configuration,
|
||||||
|
/// not from a user-controlled header.
|
||||||
|
fn configured_admin_public_authority(
|
||||||
|
state: &App,
|
||||||
|
headers: &HeaderMap,
|
||||||
|
peer: Option<PeerAddr>,
|
||||||
|
raw_host: &str,
|
||||||
|
transport: Transport,
|
||||||
|
) -> Result<Option<String>, ()> {
|
||||||
|
let Some(origin) = state.active_administration.public_origin.as_deref() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if !trusted_admin_proxy(state, peer) {
|
||||||
|
// Never apply the pin to arbitrary direct/untrusted connections.
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
if !admin_host_allowed(
|
||||||
|
raw_host,
|
||||||
|
state.policy.admin_addr,
|
||||||
|
transport.tls,
|
||||||
|
&state.active_administration.allowed_hosts,
|
||||||
|
) {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let authority = origin.strip_prefix("https://").ok_or(())?;
|
||||||
|
if !pinned_admin_proto_compatible(headers) {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
if headers.contains_key("x-forwarded-host") {
|
||||||
|
let forwarded = single_header(headers, "x-forwarded-host").ok_or(())?;
|
||||||
|
if forwarded.contains('@') {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
let candidate = forwarded.parse::<http::uri::Authority>().map_err(|_| ())?;
|
||||||
|
if !authority_matches_origin(&candidate, authority, "https") {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Some(authority.to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
fn external_admin_scheme(
|
fn external_admin_scheme(
|
||||||
state: &App,
|
state: &App,
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
|
|||||||
@@ -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.4.md", "technical"),
|
||||||
reader_doc!("docs/IMPLEMENTATION_0.31.3.md", "technical"),
|
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"),
|
||||||
|
|||||||
@@ -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.3",
|
"version": "0.31.4",
|
||||||
"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": [
|
||||||
@@ -764,7 +764,13 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
},
|
||||||
|
"description": "Optional additional panel DNS hostnames. The hostname of public_origin is implicitly allowed and must not be duplicated."
|
||||||
|
},
|
||||||
|
"public_origin": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"description": "Optional administrator-pinned browser HTTPS origin behind a trusted reverse proxy. Its DNS hostname is automatically allowed without adding it to allowed_hosts. Restart required. Does not affect public proxy traffic."
|
||||||
},
|
},
|
||||||
"session_days": {
|
"session_days": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
|
|||||||
+29
-7
@@ -117,17 +117,39 @@ async fn guard(State(state): State<Arc<App>>, mut request: Request, next: Next)
|
|||||||
let headers = request.headers();
|
let headers = request.headers();
|
||||||
let trusted_host = trusted_admin_proxy(&state, peer);
|
let trusted_host = trusted_admin_proxy(&state, peer);
|
||||||
let trusted_proto = trusted_admin_forwarded_proto(&state, peer);
|
let trusted_proto = trusted_admin_forwarded_proto(&state, peer);
|
||||||
let scheme = match external_admin_scheme(&state, headers, peer, transport) {
|
// Pinned browser origin is panel-only. Certificate automation using API
|
||||||
Ok(value) => value,
|
// credentials keeps the original forwarding/TLS authorization semantics.
|
||||||
Err(()) => {
|
let automation_credentials = headers.contains_key(CERTIFICATE_API_KEY_HEADER)
|
||||||
return invalid_admin_origin(&request, "external_scheme", None, None, trusted_host, trusted_proto);
|
|| (headers.contains_key(AUTHORIZATION)
|
||||||
}
|
&& certificate_api_operation(request.method(), request.uri().path()).is_some());
|
||||||
|
let pinned_authority_result = if automation_credentials {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
configured_admin_public_authority(&state, headers, peer, &raw_host, transport)
|
||||||
};
|
};
|
||||||
let external_authority =
|
let pinned_authority = match pinned_authority_result {
|
||||||
match external_admin_authority(&state, headers, peer, &raw_host, scheme == "https") {
|
Ok(value) => value,
|
||||||
|
Err(()) => return invalid_admin_origin(&request, "pinned_origin_mismatch", Some("https"),
|
||||||
|
state.active_administration.public_origin.as_deref().and_then(|value| value.strip_prefix("https://")),
|
||||||
|
trusted_host, trusted_proto),
|
||||||
|
};
|
||||||
|
let (scheme, external_authority) = if let Some(authority) = pinned_authority {
|
||||||
|
// HTTPS is a trusted, administrator-pinned identity here. Do not select
|
||||||
|
// any list element of an ambiguous forwarded-proto chain.
|
||||||
|
("https", authority)
|
||||||
|
} else {
|
||||||
|
let scheme = match external_admin_scheme(&state, headers, peer, transport) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(()) => {
|
||||||
|
return invalid_admin_origin(&request, "external_scheme", None, None, trusted_host, trusted_proto);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let authority = match external_admin_authority(&state, headers, peer, &raw_host, scheme == "https") {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(()) => return secure(reject(StatusCode::FORBIDDEN, "auth.invalid_host")),
|
Err(()) => return secure(reject(StatusCode::FORBIDDEN, "auth.invalid_host")),
|
||||||
};
|
};
|
||||||
|
(scheme, authority)
|
||||||
|
};
|
||||||
if headers.get_all(ORIGIN).iter().count() > 1
|
if headers.get_all(ORIGIN).iter().count() > 1
|
||||||
|| headers.get(ORIGIN).is_some_and(|h| {
|
|| headers.get(ORIGIN).is_some_and(|h| {
|
||||||
h.to_str()
|
h.to_str()
|
||||||
|
|||||||
+3
-2
@@ -123,10 +123,11 @@ fn status_payload(state: &App) -> Value {
|
|||||||
desired_admin
|
desired_admin
|
||||||
.allowed_hosts
|
.allowed_hosts
|
||||||
.extend(certificate.hosts.clone());
|
.extend(certificate.hosts.clone());
|
||||||
desired_admin.allowed_hosts.sort();
|
|
||||||
desired_admin.allowed_hosts.dedup();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
desired_admin.include_public_origin_host();
|
||||||
|
desired_admin.allowed_hosts.sort();
|
||||||
|
desired_admin.allowed_hosts.dedup();
|
||||||
let desired_admin_certificate_files = desired_admin
|
let desired_admin_certificate_files = desired_admin
|
||||||
.certificate_id
|
.certificate_id
|
||||||
.as_deref()
|
.as_deref()
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ mod tests {
|
|||||||
include!("tests/part_08.rs");
|
include!("tests/part_08.rs");
|
||||||
include!("tests/part_09.rs");
|
include!("tests/part_09.rs");
|
||||||
include!("tests/part_10.rs");
|
include!("tests/part_10.rs");
|
||||||
|
include!("tests/part_11.rs");
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
#[test]
|
||||||
|
fn pinned_admin_proxy_proto_is_bounded_and_requires_external_https() {
|
||||||
|
let mut headers = HeaderMap::new();
|
||||||
|
assert!(pinned_admin_proto_compatible(&headers));
|
||||||
|
headers.insert("x-forwarded-proto", HeaderValue::from_static("https"));
|
||||||
|
assert!(pinned_admin_proto_compatible(&headers));
|
||||||
|
headers.insert("x-forwarded-proto", HeaderValue::from_static("https, http"));
|
||||||
|
assert!(pinned_admin_proto_compatible(&headers));
|
||||||
|
headers.append("x-forwarded-proto", HeaderValue::from_static("http"));
|
||||||
|
assert!(pinned_admin_proto_compatible(&headers));
|
||||||
|
for bad in ["http", "http, https", "https, ftp", "https,", ",https", "HTTPS", "https, http, http, http, http, http, http, http, http"] {
|
||||||
|
headers.remove("x-forwarded-proto");
|
||||||
|
headers.insert("x-forwarded-proto", HeaderValue::from_str(bad).unwrap());
|
||||||
|
assert!(!pinned_admin_proto_compatible(&headers), "must reject {bad}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pinned_panel_origin_survives_multi_hop_proxy_without_relaxing_csrf() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.administration.public_origin = Some("https://panel.example.test".into());
|
||||||
|
let (router, store, _dir, secret, csrf) = fixture_with_config(config).await;
|
||||||
|
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||||
|
// Without an explicit origin pin, 0.31.3 rejected this session GET as
|
||||||
|
// external_scheme although the browser address remained correct.
|
||||||
|
let result = router.clone().oneshot(request("/api/auth/session")
|
||||||
|
.header("cookie", &cookie)
|
||||||
|
.header("x-forwarded-host", "panel.example.test")
|
||||||
|
.header("x-forwarded-proto", "https, http")
|
||||||
|
.header("sec-fetch-site", "same-origin")
|
||||||
|
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
|
||||||
|
.body(Body::empty()).unwrap()).await.unwrap();
|
||||||
|
assert_eq!(result.status(), StatusCode::OK);
|
||||||
|
let session_cookie = result.headers().get_all(SET_COOKIE).iter()
|
||||||
|
.filter_map(|v| v.to_str().ok()).find(|v| v.starts_with(auth::COOKIE_NAME)).unwrap();
|
||||||
|
assert!(session_cookie.contains("; Secure"));
|
||||||
|
|
||||||
|
// The incoming admin Host may itself be the pinned public DNS name.
|
||||||
|
// There is no separate allowed_hosts entry in the saved configuration.
|
||||||
|
let pinned_host = Request::builder().uri("/api/auth/session")
|
||||||
|
.header("host", "panel.example.test")
|
||||||
|
.header("cookie", &cookie)
|
||||||
|
.header("x-forwarded-host", "panel.example.test")
|
||||||
|
.header("x-forwarded-proto", "https, http")
|
||||||
|
.extension(PeerAddr("127.0.0.1:45003".parse().unwrap()))
|
||||||
|
.body(Body::empty()).unwrap();
|
||||||
|
assert_eq!(router.clone().oneshot(pinned_host).await.unwrap().status(), StatusCode::OK);
|
||||||
|
|
||||||
|
// Explicit Origin, CSRF token and app request marker remain mandatory
|
||||||
|
// for a state-changing request even when XFP contains multiple hops.
|
||||||
|
let unauthorized = router.clone().oneshot(request("/api/auth/logout")
|
||||||
|
.method("POST").header("cookie", &cookie)
|
||||||
|
.header("origin", "https://panel.example.test")
|
||||||
|
.header("x-forwarded-host", "panel.example.test")
|
||||||
|
.header("x-forwarded-proto", "https, http")
|
||||||
|
.extension(PeerAddr("127.0.0.1:45001".parse().unwrap()))
|
||||||
|
.body(Body::empty()).unwrap()).await.unwrap();
|
||||||
|
assert_eq!(unauthorized.status(), StatusCode::FORBIDDEN);
|
||||||
|
let authorized = router.oneshot(request("/api/auth/logout")
|
||||||
|
.method("POST").header("cookie", &cookie)
|
||||||
|
.header("origin", "https://panel.example.test")
|
||||||
|
.header("x-rustproxy-request", "1")
|
||||||
|
.header("x-csrf-token", csrf)
|
||||||
|
.header("x-forwarded-host", "panel.example.test")
|
||||||
|
.header("x-forwarded-proto", "https, http")
|
||||||
|
.extension(PeerAddr("127.0.0.1:45002".parse().unwrap()))
|
||||||
|
.body(Body::empty()).unwrap()).await.unwrap();
|
||||||
|
assert_eq!(authorized.status(), StatusCode::OK);
|
||||||
|
store.close().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn pinned_panel_origin_rejects_untrusted_proxy_host_and_origin() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.administration.public_origin = Some("https://panel.example.test".into());
|
||||||
|
let (router, store, _dir, secret, _) = fixture_with_config(config).await;
|
||||||
|
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
|
||||||
|
for (xfh, xfp, origin, fetch_site) in [
|
||||||
|
("evil.example.test", "https, http", "https://panel.example.test", "same-origin"),
|
||||||
|
("other@panel.example.test", "https, http", "https://panel.example.test", "same-origin"),
|
||||||
|
("panel.example.test", "http", "https://panel.example.test", "same-origin"),
|
||||||
|
("panel.example.test", "http, https", "https://panel.example.test", "same-origin"),
|
||||||
|
("panel.example.test", "https, http", "https://evil.example.test", "same-origin"),
|
||||||
|
("panel.example.test", "https, http", "null", "same-origin"),
|
||||||
|
("panel.example.test", "https, http", "https://panel.example.test", "cross-site"),
|
||||||
|
] {
|
||||||
|
let result = router.clone().oneshot(request("/api/auth/session")
|
||||||
|
.header("cookie", &cookie)
|
||||||
|
.header("x-forwarded-host", xfh)
|
||||||
|
.header("x-forwarded-proto", xfp)
|
||||||
|
.header("origin", origin)
|
||||||
|
.header("sec-fetch-site", fetch_site)
|
||||||
|
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
|
||||||
|
.body(Body::empty()).unwrap()).await.unwrap();
|
||||||
|
assert_eq!(result.status(), StatusCode::FORBIDDEN,
|
||||||
|
"must reject forwarded-host={xfh}, proto={xfp}, origin={origin}, fetch={fetch_site}");
|
||||||
|
}
|
||||||
|
// Credential-bearing certificate automation does not inherit the
|
||||||
|
// browser-origin override, even through the same trusted admin socket.
|
||||||
|
let automation = router.clone().oneshot(request("/api/certificates/deploy")
|
||||||
|
.method("POST")
|
||||||
|
.header(CERTIFICATE_API_KEY_HEADER, "svc-key")
|
||||||
|
.header("x-forwarded-host", "panel.example.test")
|
||||||
|
.header("x-forwarded-proto", "https, http")
|
||||||
|
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
|
||||||
|
.body(Body::empty()).unwrap()).await.unwrap();
|
||||||
|
assert_eq!(automation.status(), StatusCode::FORBIDDEN);
|
||||||
|
|
||||||
|
let untrusted = router.clone().oneshot(request("/api/auth/session")
|
||||||
|
.header("cookie", &cookie)
|
||||||
|
.header("x-forwarded-host", "panel.example.test")
|
||||||
|
.header("x-forwarded-proto", "https, http")
|
||||||
|
.header("origin", "https://panel.example.test")
|
||||||
|
.extension(PeerAddr("192.0.2.42:44000".parse().unwrap()))
|
||||||
|
.body(Body::empty()).unwrap()).await.unwrap();
|
||||||
|
assert_eq!(untrusted.status(), StatusCode::FORBIDDEN);
|
||||||
|
|
||||||
|
let mut duplicated = request("/api/auth/session")
|
||||||
|
.header("cookie", &cookie)
|
||||||
|
.header("x-forwarded-host", "panel.example.test")
|
||||||
|
.header("x-forwarded-proto", "https, http")
|
||||||
|
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
|
||||||
|
.body(Body::empty()).unwrap();
|
||||||
|
duplicated.headers_mut().append("x-forwarded-host", "panel.example.test".parse().unwrap());
|
||||||
|
assert_eq!(router.clone().oneshot(duplicated).await.unwrap().status(), StatusCode::FORBIDDEN);
|
||||||
|
store.close().await;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn without_public_origin_ambiguous_forwarded_proto_stays_forbidden() {
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.administration.allowed_hosts = vec!["panel.example.test".into()];
|
||||||
|
let (router, store, _dir, secret, _) = fixture_with_config(config).await;
|
||||||
|
let result = router.oneshot(request("/api/auth/session")
|
||||||
|
.header("cookie", format!("{}={secret}", auth::COOKIE_NAME))
|
||||||
|
.header("x-forwarded-host", "panel.example.test")
|
||||||
|
.header("x-forwarded-proto", "https, http")
|
||||||
|
.extension(PeerAddr("127.0.0.1:45000".parse().unwrap()))
|
||||||
|
.body(Body::empty()).unwrap()).await.unwrap();
|
||||||
|
assert_eq!(result.status(), StatusCode::FORBIDDEN);
|
||||||
|
store.close().await;
|
||||||
|
}
|
||||||
@@ -2734,9 +2734,17 @@
|
|||||||
data-i18n="administrationHttps">Enable HTTPS for administration</span></label>
|
data-i18n="administrationHttps">Enable HTTPS for administration</span></label>
|
||||||
<label><span data-i18n="administrationCertificate">Certificate</span><select
|
<label><span data-i18n="administrationCertificate">Certificate</span><select
|
||||||
id="admin-certificate"></select></label>
|
id="admin-certificate"></select></label>
|
||||||
<label><span data-i18n="administrationHosts">Allowed DNS hostnames</span><textarea
|
<label><span data-i18n="administrationPublicOrigin">Public panel HTTPS origin (optional)</span><input
|
||||||
id="admin-hosts" rows="4" spellcheck="false"
|
id="admin-public-origin" maxlength="255" placeholder="https://panel.example.com"
|
||||||
placeholder="admin.example.com"></textarea></label>
|
type="url" spellcheck="false" autocomplete="off" /></label>
|
||||||
|
<p class="fine" data-i18n="administrationPublicOriginHelp">When behind a trusted reverse proxy, enter the panel's external HTTPS address once. Its DNS name is automatically allowed. A restart is required.</p>
|
||||||
|
<details id="admin-extra-hosts-details">
|
||||||
|
<summary data-i18n="administrationHostsAdvanced">Advanced: additional allowed DNS hostnames</summary>
|
||||||
|
<label><span data-i18n="administrationHosts">Additional allowed DNS hostnames</span><textarea
|
||||||
|
id="admin-hosts" rows="4" spellcheck="false"
|
||||||
|
placeholder="other-admin.example.com"></textarea></label>
|
||||||
|
<p class="fine" data-i18n="administrationHostsHelp">Only extra hostnames belong here. The hostname from the public panel URL is automatically included; without a public URL this remains an independent strict allowlist.</p>
|
||||||
|
</details>
|
||||||
<label><span data-i18n="administrationSessionDays">Administrator session lifetime
|
<label><span data-i18n="administrationSessionDays">Administrator session lifetime
|
||||||
(days)</span><input id="admin-session-days" min="7" max="30" step="1"
|
(days)</span><input id="admin-session-days" min="7" max="30" step="1"
|
||||||
type="number" required="" /></label>
|
type="number" required="" /></label>
|
||||||
|
|||||||
@@ -8,6 +8,19 @@
|
|||||||
? "logging"
|
? "logging"
|
||||||
: (typeof location !== "undefined" && location.pathname === "/settings" ? new URLSearchParams(location.search).get("tab") : null);
|
: (typeof location !== "undefined" && location.pathname === "/settings" ? new URLSearchParams(location.search).get("tab") : null);
|
||||||
const tabAliases = { http2: "connections" };
|
const tabAliases = { http2: "connections" };
|
||||||
|
// Keep the public panel hostname implicit. Legacy configurations may have
|
||||||
|
// written it into allowed_hosts in 0.31.4; do not show/save it twice.
|
||||||
|
function pinnedAdminHostname(value) {
|
||||||
|
try {
|
||||||
|
const url = new URL(value);
|
||||||
|
return url.protocol === "https:" ? url.hostname.toLowerCase().replace(/\.$/, "") : "";
|
||||||
|
} catch { return ""; }
|
||||||
|
}
|
||||||
|
function additionalAdminHosts(hosts, origin) {
|
||||||
|
const pinned = pinnedAdminHostname(origin);
|
||||||
|
return hosts.map(host => host.trim()).filter(host =>
|
||||||
|
host && (!pinned || host.replace(/\.$/, "").toLowerCase() !== pinned));
|
||||||
|
}
|
||||||
const normalizedRequestedTab = tabAliases[requestedTab] || requestedTab;
|
const normalizedRequestedTab = tabAliases[requestedTab] || requestedTab;
|
||||||
let active = buttons.some(button => button.dataset.settingsTab === normalizedRequestedTab) ? normalizedRequestedTab : defaultTab;
|
let active = buttons.some(button => button.dataset.settingsTab === normalizedRequestedTab) ? normalizedRequestedTab : defaultTab;
|
||||||
function applySettingsSearch() {
|
function applySettingsSearch() {
|
||||||
@@ -212,7 +225,10 @@ curl --fail --show-error --request POST \
|
|||||||
$("admin-tls").checked = !!admin.tls;
|
$("admin-tls").checked = !!admin.tls;
|
||||||
const current = admin.certificate_id || "", generated = document.createElement("option"); generated.value = ""; generated.textContent = t("administrationGeneratedCertificate");
|
const current = admin.certificate_id || "", generated = document.createElement("option"); generated.value = ""; generated.textContent = t("administrationGeneratedCertificate");
|
||||||
$("admin-certificate").replaceChildren(generated, ...(envelope.config.tls?.certificates || []).map(cert => { const o = document.createElement("option"); o.value = cert.id; o.textContent = cert.id; return o; })); $("admin-certificate").value = current;
|
$("admin-certificate").replaceChildren(generated, ...(envelope.config.tls?.certificates || []).map(cert => { const o = document.createElement("option"); o.value = cert.id; o.textContent = cert.id; return o; })); $("admin-certificate").value = current;
|
||||||
$("admin-hosts").value = (admin.allowed_hosts || []).join("\n");
|
const extraHosts = additionalAdminHosts(admin.allowed_hosts || [], admin.public_origin);
|
||||||
|
$("admin-hosts").value = extraHosts.join("\n");
|
||||||
|
$("admin-extra-hosts-details").open = extraHosts.length > 0;
|
||||||
|
$("admin-public-origin").value = admin.public_origin || "";
|
||||||
$("admin-session-days").value = Number(admin.session_days || 30);
|
$("admin-session-days").value = Number(admin.session_days || 30);
|
||||||
for (const codec of ["zstd", "br", "gzip"]) $(`admin-compression-${codec}`).checked = admin.panel_compression?.[codec] !== false;
|
for (const codec of ["zstd", "br", "gzip"]) $(`admin-compression-${codec}`).checked = admin.panel_compression?.[codec] !== false;
|
||||||
}
|
}
|
||||||
@@ -300,7 +316,7 @@ curl --fail --show-error --request POST \
|
|||||||
}, "prometheus");
|
}, "prometheus");
|
||||||
bindSettingsForm("administration-form", next => {
|
bindSettingsForm("administration-form", next => {
|
||||||
const locked = !!latestStatus?.administration?.listen_locked;
|
const locked = !!latestStatus?.administration?.listen_locked;
|
||||||
next.administration = { listen: locked ? null : $("admin-listen").value.trim(), tls: $("admin-tls").checked, certificate_id: $("admin-tls").checked ? ($("admin-certificate").value || null) : null, allowed_hosts: $("admin-hosts").value.split(/[\n,]+/).map(v => v.trim()).filter(Boolean), session_days: Number($("admin-session-days").value), panel_compression: Object.fromEntries(["zstd", "br", "gzip"].map(codec => [codec, $(`admin-compression-${codec}`).checked])) };
|
next.administration = { listen: locked ? null : $("admin-listen").value.trim(), tls: $("admin-tls").checked, certificate_id: $("admin-tls").checked ? ($("admin-certificate").value || null) : null, allowed_hosts: additionalAdminHosts($("admin-hosts").value.split(/[\n,]+/), $("admin-public-origin").value.trim()), public_origin: $("admin-public-origin").value.trim() || null, session_days: Number($("admin-session-days").value), panel_compression: Object.fromEntries(["zstd", "br", "gzip"].map(codec => [codec, $(`admin-compression-${codec}`).checked])) };
|
||||||
}, "administration");
|
}, "administration");
|
||||||
bindSettingsForm("tls-policy-form", next => {
|
bindSettingsForm("tls-policy-form", next => {
|
||||||
if (!$("tls-policy-12").checked && !$("tls-policy-13").checked) throw fieldError($("tls-policy-12"), t("tlsPolicyVersionRequired"));
|
if (!$("tls-policy-12").checked && !$("tls-policy-13").checked) throw fieldError($("tls-policy-12"), t("tlsPolicyVersionRequired"));
|
||||||
|
|||||||
@@ -181,8 +181,9 @@ fn response_context(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Reserved administration-session cookies may cross the proxy boundary only
|
/// Reserved administration-session cookies may cross the proxy boundary only
|
||||||
/// for the explicit self-admin deployment: HTTPS, an explicitly allowed public
|
/// for the explicit self-admin deployment: HTTPS, a specifically allowed
|
||||||
/// administration hostname, a backend that directly targets the configured
|
/// administration hostname (extra DNS list or pinned public HTTPS origin),
|
||||||
|
/// a backend that directly targets the configured
|
||||||
/// administration listener, and the existing local-listener risk opt-in.
|
/// administration listener, and the existing local-listener risk opt-in.
|
||||||
fn admin_session_passthrough(
|
fn admin_session_passthrough(
|
||||||
snapshot: &crate::config::Snapshot,
|
snapshot: &crate::config::Snapshot,
|
||||||
@@ -196,9 +197,7 @@ fn admin_session_passthrough(
|
|||||||
.envelope
|
.envelope
|
||||||
.config
|
.config
|
||||||
.administration
|
.administration
|
||||||
.allowed_hosts
|
.explicitly_allowed_panel_host(host)
|
||||||
.iter()
|
|
||||||
.any(|allowed| allowed.eq_ignore_ascii_case(host))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_response_rules(
|
fn apply_response_rules(
|
||||||
|
|||||||
@@ -145,10 +145,11 @@ impl App {
|
|||||||
active_administration
|
active_administration
|
||||||
.allowed_hosts
|
.allowed_hosts
|
||||||
.extend(certificate.hosts.clone());
|
.extend(certificate.hosts.clone());
|
||||||
active_administration.allowed_hosts.sort();
|
|
||||||
active_administration.allowed_hosts.dedup();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
active_administration.include_public_origin_host();
|
||||||
|
active_administration.allowed_hosts.sort();
|
||||||
|
active_administration.allowed_hosts.dedup();
|
||||||
let logs = Logs::start(log_settings, shutdown.clone());
|
let logs = Logs::start(log_settings, shutdown.clone());
|
||||||
let traffic = crate::traffic::Analytics::start(shutdown.clone());
|
let traffic = crate::traffic::Analytics::start(shutdown.clone());
|
||||||
let app = Arc::new(Self {
|
let app = Arc::new(Self {
|
||||||
|
|||||||
+18
-2
@@ -1,5 +1,5 @@
|
|||||||
//! Persisted listener intent and response controls. Listener changes require restart.
|
//! Persisted listener intent and response controls. Listener changes require restart.
|
||||||
use anyhow::{ensure, Result};
|
use anyhow::{ensure, Context as _, Result};
|
||||||
use http::{HeaderMap, HeaderValue};
|
use http::{HeaderMap, HeaderValue};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{net::SocketAddr, time::Duration};
|
use std::{net::SocketAddr, time::Duration};
|
||||||
@@ -195,8 +195,13 @@ pub struct Administration {
|
|||||||
pub tls: bool,
|
pub tls: bool,
|
||||||
/// None uses the application-managed default certificate pair.
|
/// None uses the application-managed default certificate pair.
|
||||||
pub certificate_id: Option<String>,
|
pub certificate_id: Option<String>,
|
||||||
/// Optional strict DNS Host/Origin allowlist. Empty HTTPS lists accept valid DNS names.
|
/// Optional additional DNS Host allowlist. The hostname of public_origin is
|
||||||
|
/// included automatically at runtime, never duplicated in persisted config.
|
||||||
|
/// With no public_origin, an empty HTTPS list retains the legacy DNS policy.
|
||||||
pub allowed_hosts: Vec<String>,
|
pub allowed_hosts: Vec<String>,
|
||||||
|
/// Optional fixed browser-facing HTTPS origin for the panel behind a trusted
|
||||||
|
/// reverse proxy. It is NEVER inferred from forwarded headers.
|
||||||
|
pub public_origin: Option<String>,
|
||||||
/// Absolute browser-session lifetime. Existing sessions are bounded by the current value.
|
/// Absolute browser-session lifetime. Existing sessions are bounded by the current value.
|
||||||
pub session_days: u16,
|
pub session_days: u16,
|
||||||
pub panel_compression: PanelCompression,
|
pub panel_compression: PanelCompression,
|
||||||
@@ -208,6 +213,7 @@ impl Default for Administration {
|
|||||||
tls: false,
|
tls: false,
|
||||||
certificate_id: None,
|
certificate_id: None,
|
||||||
allowed_hosts: Vec::new(),
|
allowed_hosts: Vec::new(),
|
||||||
|
public_origin: None,
|
||||||
session_days: 30,
|
session_days: 30,
|
||||||
panel_compression: PanelCompression::default(),
|
panel_compression: PanelCompression::default(),
|
||||||
}
|
}
|
||||||
@@ -225,6 +231,7 @@ impl Administration {
|
|||||||
|| self.tls != active.tls
|
|| self.tls != active.tls
|
||||||
|| self.certificate_id != active.certificate_id
|
|| self.certificate_id != active.certificate_id
|
||||||
|| self.allowed_hosts != active.allowed_hosts
|
|| self.allowed_hosts != active.allowed_hosts
|
||||||
|
|| self.public_origin != active.public_origin
|
||||||
}
|
}
|
||||||
pub fn validate(&mut self, tls: &crate::tls::Settings) -> Result<()> {
|
pub fn validate(&mut self, tls: &crate::tls::Settings) -> Result<()> {
|
||||||
if let Some(addr) = self.listen {
|
if let Some(addr) = self.listen {
|
||||||
@@ -263,6 +270,14 @@ impl Administration {
|
|||||||
"duplicate administration hostname"
|
"duplicate administration hostname"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if let Some(public_origin) = self.public_origin.as_mut() {
|
||||||
|
*public_origin = normalize_public_admin_origin(public_origin)?;
|
||||||
|
}
|
||||||
|
// Accept old 0.31.4 saved configs, but never retain a second explicit
|
||||||
|
// copy of the pinned hostname in a validated or newly saved config.
|
||||||
|
if let Some(host) = self.public_origin_host().map(str::to_owned) {
|
||||||
|
self.allowed_hosts.retain(|allowed| !allowed.eq_ignore_ascii_case(&host));
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -467,5 +482,6 @@ impl ServerHeader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
include!("settings/public_origin.rs");
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
include!("settings/tests.rs");
|
include!("settings/tests.rs");
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// Normalize the browser-facing HTTPS origin independently of the optional
|
||||||
|
// additional DNS allowlist. It is never inferred from forwarding headers.
|
||||||
|
fn normalize_public_admin_origin(public_origin: &str) -> Result<String> {
|
||||||
|
ensure!(
|
||||||
|
public_origin.len() <= 255 && !public_origin.chars().any(|c| matches!(c, '@' | '#' | '?' | ',' | '\\')),
|
||||||
|
"administration public_origin must be a single HTTPS origin without userinfo, query or fragment"
|
||||||
|
);
|
||||||
|
let uri = public_origin.parse::<http::Uri>()
|
||||||
|
.context("invalid administration public_origin")?;
|
||||||
|
ensure!(
|
||||||
|
uri.scheme_str() == Some("https")
|
||||||
|
&& uri.query().is_none()
|
||||||
|
&& matches!(uri.path(), "" | "/"),
|
||||||
|
"administration public_origin must be HTTPS without a path or query"
|
||||||
|
);
|
||||||
|
let authority = uri.authority()
|
||||||
|
.context("administration public_origin requires an authority")?;
|
||||||
|
// This setting is for a named HTTPS origin. Use the existing direct
|
||||||
|
// listener/Host policy for IP addresses and localhost.
|
||||||
|
let host = crate::config::normalize_host(authority.host())?;
|
||||||
|
ensure!(
|
||||||
|
host.parse::<std::net::IpAddr>().is_err() && host != "localhost",
|
||||||
|
"administration public_origin must name a DNS hostname"
|
||||||
|
);
|
||||||
|
let port = if authority.as_str().contains(':') {
|
||||||
|
let port = authority.port_u16()
|
||||||
|
.context("invalid administration public_origin port")?;
|
||||||
|
ensure!(port > 0, "administration public_origin port must be nonzero");
|
||||||
|
format!(":{port}")
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
Ok(format!("https://{host}{port}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Administration {
|
||||||
|
/// The origin is validated and normalized before it is stored, so this
|
||||||
|
/// returns only its DNS hostname (never a forwarded or client-supplied host).
|
||||||
|
pub fn public_origin_host(&self) -> Option<&str> {
|
||||||
|
self.public_origin.as_deref()
|
||||||
|
.and_then(|url| url.strip_prefix("https://"))
|
||||||
|
.and_then(|authority| authority.split(':').next())
|
||||||
|
}
|
||||||
|
/// Used only by the explicitly opted-in panel session-cookie passthrough.
|
||||||
|
pub fn explicitly_allowed_panel_host(&self, host: &str) -> bool {
|
||||||
|
self.allowed_hosts.iter().any(|allowed| allowed.eq_ignore_ascii_case(host))
|
||||||
|
|| self.public_origin_host().is_some_and(|name| name.eq_ignore_ascii_case(host))
|
||||||
|
}
|
||||||
|
/// Build the access-control allowlist for the active/desired panel state.
|
||||||
|
/// Never mutate the saved configuration or accumulate stale hosts on edits.
|
||||||
|
pub fn include_public_origin_host(&mut self) {
|
||||||
|
if let Some(host) = self.public_origin_host().map(str::to_owned) {
|
||||||
|
if !self.allowed_hosts.iter().any(|allowed| allowed.eq_ignore_ascii_case(&host)) {
|
||||||
|
self.allowed_hosts.push(host);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -256,6 +256,67 @@ mod tests {
|
|||||||
admin.allowed_hosts = vec!["panel.example.com".into(), "PANEL.EXAMPLE.COM.".into()];
|
admin.allowed_hosts = vec!["panel.example.com".into(), "PANEL.EXAMPLE.COM.".into()];
|
||||||
assert!(admin.validate(&crate::tls::Settings::default()).is_err());
|
assert!(admin.validate(&crate::tls::Settings::default()).is_err());
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn administration_public_origin_is_https_explicit_and_restart_bound() {
|
||||||
|
let tls = crate::tls::Settings::default();
|
||||||
|
let mut admin = Administration {
|
||||||
|
public_origin: Some("https://Panel.Example.TEST:8443/".into()),
|
||||||
|
..Administration::default()
|
||||||
|
};
|
||||||
|
admin.validate(&tls).unwrap();
|
||||||
|
assert_eq!(admin.public_origin.as_deref(), Some("https://panel.example.test:8443"));
|
||||||
|
assert_eq!(admin.public_origin_host(), Some("panel.example.test"));
|
||||||
|
assert!(admin.allowed_hosts.is_empty(), "pin must not be stored twice");
|
||||||
|
assert!(admin.explicitly_allowed_panel_host("PANEL.EXAMPLE.TEST"));
|
||||||
|
assert!(!admin.explicitly_allowed_panel_host("other.example.test"));
|
||||||
|
let mut runtime = admin.clone();
|
||||||
|
runtime.include_public_origin_host();
|
||||||
|
runtime.include_public_origin_host();
|
||||||
|
assert_eq!(runtime.allowed_hosts, vec!["panel.example.test".to_owned()]);
|
||||||
|
assert!(admin.allowed_hosts.is_empty(), "saved config remains unchanged");
|
||||||
|
let active = Administration { public_origin: None, ..admin.clone() };
|
||||||
|
assert!(admin.restart_differs(&active));
|
||||||
|
let mut legacy: Administration = serde_json::from_str(r#"{"allowed_hosts":["panel.example.test"]}"#).unwrap();
|
||||||
|
legacy.validate(&tls).unwrap();
|
||||||
|
assert_eq!(legacy.public_origin, None);
|
||||||
|
assert!(legacy.explicitly_allowed_panel_host("panel.example.test"));
|
||||||
|
let mut old_pin: Administration = serde_json::from_str(
|
||||||
|
r#"{"public_origin":"https://panel.example.test","allowed_hosts":["PANEL.Example.TEST.","other.example.test"]}"#
|
||||||
|
).unwrap();
|
||||||
|
old_pin.validate(&tls).unwrap();
|
||||||
|
assert_eq!(old_pin.allowed_hosts, vec!["other.example.test".to_owned()]);
|
||||||
|
assert!(old_pin.explicitly_allowed_panel_host("panel.example.test"));
|
||||||
|
assert!(old_pin.explicitly_allowed_panel_host("other.example.test"));
|
||||||
|
// Additional hosts are independent: editing/removing the pinned origin
|
||||||
|
// must not leave its former hostname in the saved strict list.
|
||||||
|
let mut changed = admin.clone();
|
||||||
|
changed.public_origin = Some("https://other.example.test".into());
|
||||||
|
changed.validate(&tls).unwrap();
|
||||||
|
assert_eq!(changed.public_origin_host(), Some("other.example.test"));
|
||||||
|
assert!(!changed.explicitly_allowed_panel_host("panel.example.test"));
|
||||||
|
let mut opted_out = changed;
|
||||||
|
opted_out.public_origin = None;
|
||||||
|
opted_out.validate(&tls).unwrap();
|
||||||
|
assert!(opted_out.allowed_hosts.is_empty());
|
||||||
|
for invalid in [
|
||||||
|
"http://panel.example.test",
|
||||||
|
"https://*.example.test",
|
||||||
|
"https://localhost",
|
||||||
|
"https://127.0.0.1",
|
||||||
|
"https://panel.example.test/a",
|
||||||
|
"https://panel.example.test/?x=1",
|
||||||
|
"https://admin@panel.example.test",
|
||||||
|
"https://panel.example.test#frag",
|
||||||
|
"https://panel.example.test:0",
|
||||||
|
"https://panel.example.test:99999",
|
||||||
|
"https://panel.example.test,https://evil.example.test",
|
||||||
|
] {
|
||||||
|
let mut candidate = admin.clone();
|
||||||
|
candidate.public_origin = Some(invalid.into());
|
||||||
|
assert!(candidate.validate(&tls).is_err(), "must reject {invalid}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn administration_hostname_count_is_not_artificially_capped() {
|
fn administration_hostname_count_is_not_artificially_capped() {
|
||||||
let mut admin = Administration {
|
let mut admin = Administration {
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
// Test the real admin settings normalization helpers and visible form wiring;
|
||||||
|
// source-level UI test only, not a Rust or browser integration test.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
|
||||||
|
const js = fs.readFileSync('src/admin/web/settings.js','utf8');
|
||||||
|
const html = fs.readFileSync('src/admin/web/index.html','utf8');
|
||||||
|
const en = JSON.parse(fs.readFileSync('langs/en_US.json','utf8'));
|
||||||
|
const pl = JSON.parse(fs.readFileSync('langs/pl_pl.json','utf8'));
|
||||||
|
const api = JSON.parse(fs.readFileSync('src/admin/openapi.json','utf8'));
|
||||||
|
const begin = js.indexOf(' function pinnedAdminHostname(value) {');
|
||||||
|
const end = js.indexOf(' const normalizedRequestedTab', begin);
|
||||||
|
assert(begin >= 0 && end > begin, 'real admin form normalization helpers must exist');
|
||||||
|
const sandbox = vm.createContext({URL});
|
||||||
|
vm.runInContext(js.slice(begin,end),sandbox);
|
||||||
|
const normalize = (hosts, pin) => Array.from(vm.runInContext(
|
||||||
|
`additionalAdminHosts(${JSON.stringify(hosts)},${JSON.stringify(pin)})`,sandbox));
|
||||||
|
|
||||||
|
const cases = [
|
||||||
|
{name:'empty form',hosts:[],pin:null,expected:[]},
|
||||||
|
{name:'simple single origin',hosts:[],pin:'https://panel.example.test',expected:[]},
|
||||||
|
{name:'0.31.4 repeated host',hosts:['panel.example.test'],pin:'https://panel.example.test',expected:[]},
|
||||||
|
{name:'case and trailing dot',hosts:['PANEL.Example.Test.',' other.example.test '],pin:'https://panel.example.test:8443',expected:['other.example.test']},
|
||||||
|
{name:'multiple additional names',hosts:['foo.example.test','panel.example.test','bar.example.test'],pin:'https://panel.example.test',expected:['foo.example.test','bar.example.test']},
|
||||||
|
{name:'pin changed',hosts:['other.example.test'],pin:'https://new.example.test',expected:['other.example.test']},
|
||||||
|
{name:'no pin means independent allowlist',hosts:['panel.example.test'],pin:'',expected:['panel.example.test']},
|
||||||
|
{name:'invalid URL delegates to server validation',hosts:['other.example.test'],pin:'not a url',expected:['other.example.test']},
|
||||||
|
{name:'blank host lines removed',hosts:['',' panel.example.test ',' '],pin:null,expected:['panel.example.test']},
|
||||||
|
];
|
||||||
|
for (const item of cases) {
|
||||||
|
assert.deepEqual(normalize(item.hosts,item.pin),item.expected,item.name);
|
||||||
|
}
|
||||||
|
const admin = html.split('id="settings-page-administration"')[1].split('id="settings-page-system-info"')[0];
|
||||||
|
for (const id of ['admin-public-origin','admin-extra-hosts-details','admin-hosts'])
|
||||||
|
assert(admin.includes(`id="${id}"`),`missing ${id}`);
|
||||||
|
assert(admin.indexOf('id="admin-public-origin"') < admin.indexOf('id="admin-extra-hosts-details"'));
|
||||||
|
assert(admin.includes('<details id="admin-extra-hosts-details">'));
|
||||||
|
assert(js.includes('$("admin-extra-hosts-details").open = extraHosts.length > 0'));
|
||||||
|
assert(js.includes('additionalAdminHosts(admin.allowed_hosts || [], admin.public_origin)'));
|
||||||
|
assert(js.includes('allowed_hosts: additionalAdminHosts($("admin-hosts").value.split('));
|
||||||
|
assert(!js.includes('allowed_hosts: $("admin-hosts").value.split('));
|
||||||
|
for (const key of ['administrationHosts','administrationHostsAdvanced','administrationHostsHelp','administrationPublicOriginHelp']) {
|
||||||
|
assert.equal(typeof en.translations[key],'string'); assert.equal(typeof pl.translations[key],'string');
|
||||||
|
assert(en.translations[key].length > 15 && pl.translations[key].length > 15,key);
|
||||||
|
}
|
||||||
|
// Config contract remains compatible; the pin allows a host implicitly.
|
||||||
|
const spec = api.components?.schemas || api.definitions;
|
||||||
|
const specText=JSON.stringify(spec);
|
||||||
|
assert(specText.includes('implicitly allowed'));
|
||||||
|
assert(specText.includes('automatically allowed'));
|
||||||
|
console.log(`PASS: ${cases.length} real settings-helper scenarios, HTML/wiring, locale parity and OpenAPI descriptions`);
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
// Reconstruct ONLY the RustProxy 0.31.4 administration Origin guard decision.
|
||||||
|
// Real-source anchors guard against accidental model/source drift. This is NOT
|
||||||
|
// a Rust server, not a browser, and not an HTTP/2 or TLS integration test.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import http from 'node:http';
|
||||||
|
import {fileURLToPath} from 'node:url';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||||
|
const read = p => fs.readFileSync(path.join(root, p), 'utf8');
|
||||||
|
const source = {
|
||||||
|
settings: read('src/settings.rs') + read('src/settings/public_origin.rs'), access: read('src/admin/access.rs'),
|
||||||
|
session: read('src/admin/session.rs'), forwarding: read('src/forwarding.rs'),
|
||||||
|
};
|
||||||
|
for (const [part, tokens] of Object.entries({
|
||||||
|
settings: ['pub public_origin: Option<String>', 'fn normalize_public_admin_origin(public_origin: &str)', 'fn include_public_origin_host(&mut self)', 'fn explicitly_allowed_panel_host(&self, host: &str)', 'self.public_origin != active.public_origin'],
|
||||||
|
access: ['fn configured_admin_public_authority(', 'fn pinned_admin_proto_compatible(', 'hops > 8', 'scheme != "https"', 'trusted_admin_proxy(state, peer)'],
|
||||||
|
session: ['configured_admin_public_authority(&state, headers, peer, &raw_host, transport)', 'automation_credentials', 'pinned_origin_mismatch', '"origin_mismatch"', '"cross_site_fetch"'],
|
||||||
|
forwarding: ['self.trust_proto && self.trusted(ip)'],
|
||||||
|
})) for (const token of tokens) assert(source[part].includes(token), `Source contract missing: ${part}/${token}`);
|
||||||
|
|
||||||
|
const defaults = {host:'127.0.0.1:9090', 'x-forwarded-host':'panel.example.test',
|
||||||
|
'x-forwarded-proto':'https', 'sec-fetch-site':'same-origin'};
|
||||||
|
const context = {listenIp:'127.0.0.1', listenPort:9090, tls:false,
|
||||||
|
peerTrusted:true, trustProto:true, publicOrigin:null, allowedHosts:['panel.example.test'],
|
||||||
|
automation:false};
|
||||||
|
const normal = x => x.toLowerCase();
|
||||||
|
function authority(value) {
|
||||||
|
if (typeof value !== 'string' || !value || value.includes('@') || value.includes(',') || value.includes('/')) return null;
|
||||||
|
try {
|
||||||
|
const parsed = new URL(`https://${value}/`);
|
||||||
|
if (parsed.username || parsed.password || parsed.hash || parsed.search || parsed.pathname !== '/') return null;
|
||||||
|
const host = parsed.hostname.toLowerCase();
|
||||||
|
return {host, port:parsed.port ? Number(parsed.port) : null};
|
||||||
|
} catch {return null;}
|
||||||
|
}
|
||||||
|
function sameAuthority(a,b,scheme) {
|
||||||
|
const left=authority(a), right=authority(b);
|
||||||
|
if (!left || !right) return false;
|
||||||
|
const defaultPort = scheme === 'https' ? 443 : 80;
|
||||||
|
return left.host===right.host && (left.port ?? defaultPort)===(right.port ?? defaultPort);
|
||||||
|
}
|
||||||
|
function hostAllowed(value, tls, cfg) {
|
||||||
|
const actual=authority(value);
|
||||||
|
if (!actual) return false;
|
||||||
|
const port=actual.port ?? (tls ? 443 : 80);
|
||||||
|
// Effective runtime/desired allowlist also includes the public origin host;
|
||||||
|
// only extra names are persisted in allowed_hosts.
|
||||||
|
const pinnedHost=cfg.publicOrigin ? new URL(cfg.publicOrigin).hostname.toLowerCase() : null;
|
||||||
|
return (actual.host===cfg.listenIp && port===cfg.listenPort)
|
||||||
|
|| cfg.allowedHosts.includes(actual.host)
|
||||||
|
|| actual.host===pinnedHost;
|
||||||
|
}
|
||||||
|
const map = raw => {
|
||||||
|
const headers=new Map();
|
||||||
|
for (let i=0;i<raw.length;i+=2) {
|
||||||
|
const name=normal(raw[i]);
|
||||||
|
headers.set(name,[...(headers.get(name)||[]),raw[i+1]]);
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
const all = (headers,name) => headers.get(name)||[];
|
||||||
|
const single = (headers,name) => all(headers,name).length===1 ? all(headers,name)[0] : null;
|
||||||
|
function protoCompatible(headers) {
|
||||||
|
let hops=0;
|
||||||
|
for (const value of all(headers,'x-forwarded-proto')) {
|
||||||
|
for (const part of value.split(',')) {
|
||||||
|
hops++;
|
||||||
|
const scheme=part.trim();
|
||||||
|
if (hops>8 || (hops===1 && scheme!=='https') || !['http','https'].includes(scheme)) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function originMatches(value,scheme,external) {
|
||||||
|
if (typeof value!=='string' || value.includes('#') || value.includes('@')) return false;
|
||||||
|
try {
|
||||||
|
const uri=new URL(value);
|
||||||
|
if (uri.protocol!==`${scheme}:` || uri.pathname!=='/' || uri.search || uri.hash || uri.username || uri.password) return false;
|
||||||
|
return sameAuthority(uri.host, external, scheme);
|
||||||
|
} catch {return false;}
|
||||||
|
}
|
||||||
|
function guard(headers,cfg) {
|
||||||
|
const rawHost=single(headers,'host');
|
||||||
|
if (!rawHost) return {status:403,reason:'invalid_host'};
|
||||||
|
let scheme=cfg.tls ? 'https' : 'http', external=rawHost;
|
||||||
|
// 0.31.4 public pin only behind a trusted last hop and only for panel traffic.
|
||||||
|
if (cfg.publicOrigin && cfg.peerTrusted && !cfg.automation) {
|
||||||
|
let pinned;
|
||||||
|
try { pinned=new URL(cfg.publicOrigin); } catch {return {status:403,reason:'pinned_origin_mismatch'};}
|
||||||
|
if (pinned.protocol!=='https:' || !hostAllowed(rawHost,cfg.tls,cfg) || !protoCompatible(headers))
|
||||||
|
return {status:403,reason:'pinned_origin_mismatch'};
|
||||||
|
const xfh=all(headers,'x-forwarded-host');
|
||||||
|
if (xfh.length>1 || xfh.length===1 && !sameAuthority(xfh[0],pinned.host,'https'))
|
||||||
|
return {status:403,reason:'pinned_origin_mismatch'};
|
||||||
|
scheme='https'; external=pinned.host;
|
||||||
|
} else {
|
||||||
|
if (!cfg.tls && cfg.peerTrusted && cfg.trustProto && all(headers,'x-forwarded-proto').length) {
|
||||||
|
const proto=single(headers,'x-forwarded-proto');
|
||||||
|
if (proto!=='https' && proto!=='http') return {status:403,reason:'external_scheme'};
|
||||||
|
scheme=proto;
|
||||||
|
}
|
||||||
|
if (cfg.peerTrusted && all(headers,'x-forwarded-host').length)
|
||||||
|
external=single(headers,'x-forwarded-host');
|
||||||
|
if (!hostAllowed(external,scheme==='https',cfg))
|
||||||
|
return {status:403,reason:'invalid_host'};
|
||||||
|
}
|
||||||
|
const originValues=all(headers,'origin');
|
||||||
|
if (originValues.length>1 || originValues.length===1 && !originMatches(originValues[0],scheme,external))
|
||||||
|
return {status:403,reason:'origin_mismatch'};
|
||||||
|
if (all(headers,'sec-fetch-site').includes('cross-site'))
|
||||||
|
return {status:403,reason:'cross_site_fetch'};
|
||||||
|
return {status:200,reason:'pass',scheme};
|
||||||
|
}
|
||||||
|
const cases=[];
|
||||||
|
const add=(name,patch,opts,expected)=>cases.push({name,patch,opts,expected});
|
||||||
|
const PIN={publicOrigin:'https://panel.example.test',allowedHosts:[]};
|
||||||
|
add('legacy normal same-origin GET', {},{},'pass');
|
||||||
|
add('no pin and no DNS allowlist rejects proxied host', {},{allowedHosts:[]},'invalid_host');
|
||||||
|
add('legacy malformed multi-hop chain', {'x-forwarded-proto':'https, http'},{},'external_scheme');
|
||||||
|
add('legacy duplicated XFP lines', {'x-forwarded-proto':['https','http']},{},'external_scheme');
|
||||||
|
add('legacy explicit DNS host without pin', {'host':'other.example.test','x-forwarded-host':'other.example.test'},
|
||||||
|
{allowedHosts:['other.example.test']}, 'pass');
|
||||||
|
add('legacy extra host without pin still denies unknown', {'host':'evil.example.test','x-forwarded-host':'evil.example.test'},
|
||||||
|
{allowedHosts:['other.example.test']}, 'invalid_host');
|
||||||
|
add('pin: normal GET, persisted allowed_hosts empty', {},PIN,'pass');
|
||||||
|
add('pin: public Host implicitly allowed', {'host':'panel.example.test'},PIN,'pass');
|
||||||
|
add('pin: unknown Host still rejected', {'host':'unknown.example.test'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: old 0.31.4 explicit hostname remains compatible', {}, {...PIN,allowedHosts:['panel.example.test']},'pass');
|
||||||
|
add('pin: additional DNS host accepted only with matching forwarded public Host',
|
||||||
|
{'host':'other.example.test'}, {...PIN,allowedHosts:['other.example.test']},'pass');
|
||||||
|
add('pin: changing public origin does not allow previous hostname',
|
||||||
|
{'host':'panel.example.test','x-forwarded-host':'new.example.test'},
|
||||||
|
{...PIN,publicOrigin:'https://new.example.test'},'pinned_origin_mismatch');
|
||||||
|
add('pin: GET multi-hop https then http',{'x-forwarded-proto':'https, http'},PIN,'pass');
|
||||||
|
add('pin: duplicate XFP lines',{'x-forwarded-proto':['https','http']},PIN,'pass');
|
||||||
|
add('pin: 8-hop bounded chain',{'x-forwarded-proto':'https, http, http, http, http, http, http, http'},PIN,'pass');
|
||||||
|
add('pin: 9-hop rejected',{'x-forwarded-proto':'https, http, http, http, http, http, http, http, http'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: no XFP; public origin configured',{'x-forwarded-proto':null},PIN,'pass');
|
||||||
|
add('pin: no XFH; internal Host still validated',{'x-forwarded-host':null,'origin':'https://panel.example.test'},PIN,'pass');
|
||||||
|
add('pin: wrong XFH',{'x-forwarded-host':'attacker.example.test'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: different allowed Host but wrong configured URL',{'x-forwarded-host':'panel.example.test:8443'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: XFH userinfo rejected',{'x-forwarded-host':'attacker@panel.example.test'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: duplicate XFH lines',{'x-forwarded-host':['panel.example.test','panel.example.test']},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: first hop downgraded HTTP',{'x-forwarded-proto':'http, https'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: HTTP only',{'x-forwarded-proto':'http'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: unrecognized protocol',{'x-forwarded-proto':'ftp, http'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: empty protocol element',{'x-forwarded-proto':'https, '},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: invalid XFP capitalization',{'x-forwarded-proto':'HTTPS'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: correct explicit browser Origin',{'origin':'https://panel.example.test','x-forwarded-proto':'https, http'},PIN,'pass');
|
||||||
|
add('pin: correct explicit Origin with :443',{'origin':'https://panel.example.test:443'},PIN,'pass');
|
||||||
|
add('pin: attacker Origin rejected',{'origin':'https://attacker.example.test'},PIN,'origin_mismatch');
|
||||||
|
add('pin: Origin null rejected',{'origin':'null'},PIN,'origin_mismatch');
|
||||||
|
add('pin: conflicting Origin port rejected',{'origin':'https://panel.example.test:8443'},PIN,'origin_mismatch');
|
||||||
|
add('pin: cross-site API request rejected',{'sec-fetch-site':'cross-site'},PIN,'cross_site_fetch');
|
||||||
|
add('pin: untrusted peer must not apply pin',{'x-forwarded-proto':'https, http','origin':'https://panel.example.test'}, {...PIN,peerTrusted:false},'origin_mismatch');
|
||||||
|
add('pin: credential automation remains strict',{'x-forwarded-proto':'https, http'}, {...PIN,automation:true},'external_scheme');
|
||||||
|
add('pin: Host cannot spoof unallowlisted identity',{'host':'attacker.example.test'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: correct alt port 8443',{'x-forwarded-host':'panel.example.test:8443','origin':'https://panel.example.test:8443'}, {...PIN, publicOrigin:'https://panel.example.test:8443'},'pass');
|
||||||
|
|
||||||
|
function encodedHeaders(patch) {
|
||||||
|
const merged={...defaults,...patch};
|
||||||
|
const result={};
|
||||||
|
for (const [key,value] of Object.entries(merged)) if (value!==null) result[key]=value;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const pending=[];
|
||||||
|
const server=http.createServer((req,res)=>{
|
||||||
|
const item=pending.shift();
|
||||||
|
if (!item) return res.writeHead(500).end('no scenario');
|
||||||
|
const result=guard(map(req.rawHeaders),{...context,...item.opts});
|
||||||
|
res.writeHead(result.status, {'content-type':'application/json'}).end(JSON.stringify(result));
|
||||||
|
});
|
||||||
|
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
|
||||||
|
async function request(item) {
|
||||||
|
pending.push(item);
|
||||||
|
return await new Promise((resolve,reject)=>{
|
||||||
|
const req=http.request({hostname:'127.0.0.1',port:server.address().port,path:'/api/auth/session',
|
||||||
|
agent:false,headers:encodedHeaders(item.patch)},res=>{
|
||||||
|
let data='';res.setEncoding('utf8');res.on('data',part=>data+=part);
|
||||||
|
res.on('end',()=>resolve(JSON.parse(data)));
|
||||||
|
});
|
||||||
|
req.on('error',reject);req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (const item of cases) {
|
||||||
|
const result=await request(item);
|
||||||
|
assert.equal(result.reason,item.expected,item.name);
|
||||||
|
assert.equal(result.status,item.expected==='pass' ? 200:403,item.name);
|
||||||
|
}
|
||||||
|
const repeats=[];
|
||||||
|
for (const configured of [false,true]) {
|
||||||
|
for (let attempt=1;attempt<=12;attempt++) {
|
||||||
|
const malformed=attempt%4===0;
|
||||||
|
const result=await request({patch:{'x-forwarded-proto':malformed?'https, http':'https'},
|
||||||
|
opts:configured? PIN : {}});
|
||||||
|
assert.equal(result.status,configured?200:(malformed?403:200));
|
||||||
|
repeats.push({pin:configured,attempt,forwardedProto:malformed?'https, http':'https',status:result.status,reason:result.reason});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const summary={cases:cases.length,repeated:repeats.length,unconfigured403:repeats.filter(x=>!x.pin&&x.status===403).length,
|
||||||
|
pinned403:repeats.filter(x=>x.pin&&x.status===403).length};
|
||||||
|
const output={release:'0.31.4',classification:'Node.js HTTP simulation of admin guard, not compiled RustProxy',
|
||||||
|
assertions:cases.length+repeats.length,scenarios:cases.map(({name,expected})=>({name,expected})),repeats,summary};
|
||||||
|
if (process.argv[2]) fs.writeFileSync(process.argv[2],JSON.stringify(output,null,2)+'\n');
|
||||||
|
console.log(`PASS: ${output.assertions} HTTP-simulation assertions: ${summary.cases} cases + ${summary.repeated} resumes; `+
|
||||||
|
`403 with no pin ${summary.unconfigured403}/12, 403 with pin ${summary.pinned403}/12`);
|
||||||
|
} finally {server.close();}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
// Reconstruct ONLY the RustProxy 0.31.4 administration Origin guard decision.
|
||||||
|
// Real-source anchors guard against accidental model/source drift. This is NOT
|
||||||
|
// a Rust server, not a browser, and not an HTTP/2 or TLS integration test.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import http from 'node:http';
|
||||||
|
import {fileURLToPath} from 'node:url';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||||
|
const read = p => fs.readFileSync(path.join(root, p), 'utf8');
|
||||||
|
const source = {
|
||||||
|
settings: read('src/settings.rs') + read('src/settings/public_origin.rs'), access: read('src/admin/access.rs'),
|
||||||
|
session: read('src/admin/session.rs'), forwarding: read('src/forwarding.rs'),
|
||||||
|
};
|
||||||
|
for (const [part, tokens] of Object.entries({
|
||||||
|
settings: ['pub public_origin: Option<String>', 'fn include_public_origin_host(&mut self)', 'self.public_origin != active.public_origin'],
|
||||||
|
access: ['fn configured_admin_public_authority(', 'fn pinned_admin_proto_compatible(', 'hops > 8', 'scheme != "https"', 'trusted_admin_proxy(state, peer)'],
|
||||||
|
session: ['configured_admin_public_authority(&state, headers, peer, &raw_host, transport)', 'automation_credentials', 'pinned_origin_mismatch', '"origin_mismatch"', '"cross_site_fetch"'],
|
||||||
|
forwarding: ['self.trust_proto && self.trusted(ip)'],
|
||||||
|
})) for (const token of tokens) assert(source[part].includes(token), `Source contract missing: ${part}/${token}`);
|
||||||
|
|
||||||
|
const defaults = {host:'127.0.0.1:9090', 'x-forwarded-host':'panel.example.test',
|
||||||
|
'x-forwarded-proto':'https', 'sec-fetch-site':'same-origin'};
|
||||||
|
const context = {listenIp:'127.0.0.1', listenPort:9090, tls:false,
|
||||||
|
peerTrusted:true, trustProto:true, publicOrigin:null, allowedHosts:['panel.example.test'],
|
||||||
|
automation:false};
|
||||||
|
const normal = x => x.toLowerCase();
|
||||||
|
function authority(value) {
|
||||||
|
if (typeof value !== 'string' || !value || value.includes('@') || value.includes(',') || value.includes('/')) return null;
|
||||||
|
try {
|
||||||
|
const parsed = new URL(`https://${value}/`);
|
||||||
|
if (parsed.username || parsed.password || parsed.hash || parsed.search || parsed.pathname !== '/') return null;
|
||||||
|
const host = parsed.hostname.toLowerCase();
|
||||||
|
return {host, port:parsed.port ? Number(parsed.port) : null};
|
||||||
|
} catch {return null;}
|
||||||
|
}
|
||||||
|
function sameAuthority(a,b,scheme) {
|
||||||
|
const left=authority(a), right=authority(b);
|
||||||
|
if (!left || !right) return false;
|
||||||
|
const defaultPort = scheme === 'https' ? 443 : 80;
|
||||||
|
return left.host===right.host && (left.port ?? defaultPort)===(right.port ?? defaultPort);
|
||||||
|
}
|
||||||
|
function hostAllowed(value, tls, cfg) {
|
||||||
|
const actual=authority(value);
|
||||||
|
if (!actual) return false;
|
||||||
|
const port=actual.port ?? (tls ? 443 : 80);
|
||||||
|
const pinnedHost=cfg.publicOrigin ? new URL(cfg.publicOrigin).hostname.toLowerCase() : null;
|
||||||
|
// The validated public panel hostname is derived at runtime.
|
||||||
|
return (actual.host===cfg.listenIp && port===cfg.listenPort)
|
||||||
|
|| cfg.allowedHosts.includes(actual.host)
|
||||||
|
|| actual.host===pinnedHost;
|
||||||
|
}
|
||||||
|
const map = raw => {
|
||||||
|
const headers=new Map();
|
||||||
|
for (let i=0;i<raw.length;i+=2) {
|
||||||
|
const name=normal(raw[i]);
|
||||||
|
headers.set(name,[...(headers.get(name)||[]),raw[i+1]]);
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
const all = (headers,name) => headers.get(name)||[];
|
||||||
|
const single = (headers,name) => all(headers,name).length===1 ? all(headers,name)[0] : null;
|
||||||
|
function protoCompatible(headers) {
|
||||||
|
let hops=0;
|
||||||
|
for (const value of all(headers,'x-forwarded-proto')) {
|
||||||
|
for (const part of value.split(',')) {
|
||||||
|
hops++;
|
||||||
|
const scheme=part.trim();
|
||||||
|
if (hops>8 || (hops===1 && scheme!=='https') || !['http','https'].includes(scheme)) return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function originMatches(value,scheme,external) {
|
||||||
|
if (typeof value!=='string' || value.includes('#') || value.includes('@')) return false;
|
||||||
|
try {
|
||||||
|
const uri=new URL(value);
|
||||||
|
if (uri.protocol!==`${scheme}:` || uri.pathname!=='/' || uri.search || uri.hash || uri.username || uri.password) return false;
|
||||||
|
return sameAuthority(uri.host, external, scheme);
|
||||||
|
} catch {return false;}
|
||||||
|
}
|
||||||
|
function guard(headers,cfg) {
|
||||||
|
const rawHost=single(headers,'host');
|
||||||
|
if (!rawHost) return {status:403,reason:'invalid_host'};
|
||||||
|
let scheme=cfg.tls ? 'https' : 'http', external=rawHost;
|
||||||
|
// 0.31.4 public pin only behind a trusted last hop and only for panel traffic.
|
||||||
|
if (cfg.publicOrigin && cfg.peerTrusted && !cfg.automation) {
|
||||||
|
let pinned;
|
||||||
|
try { pinned=new URL(cfg.publicOrigin); } catch {return {status:403,reason:'pinned_origin_mismatch'};}
|
||||||
|
if (pinned.protocol!=='https:' || !hostAllowed(rawHost,cfg.tls,cfg) || !protoCompatible(headers))
|
||||||
|
return {status:403,reason:'pinned_origin_mismatch'};
|
||||||
|
const xfh=all(headers,'x-forwarded-host');
|
||||||
|
if (xfh.length>1 || xfh.length===1 && !sameAuthority(xfh[0],pinned.host,'https'))
|
||||||
|
return {status:403,reason:'pinned_origin_mismatch'};
|
||||||
|
scheme='https'; external=pinned.host;
|
||||||
|
} else {
|
||||||
|
if (!cfg.tls && cfg.peerTrusted && cfg.trustProto && all(headers,'x-forwarded-proto').length) {
|
||||||
|
const proto=single(headers,'x-forwarded-proto');
|
||||||
|
if (proto!=='https' && proto!=='http') return {status:403,reason:'external_scheme'};
|
||||||
|
scheme=proto;
|
||||||
|
}
|
||||||
|
if (cfg.peerTrusted && all(headers,'x-forwarded-host').length)
|
||||||
|
external=single(headers,'x-forwarded-host');
|
||||||
|
if (!hostAllowed(external,scheme==='https',cfg))
|
||||||
|
return {status:403,reason:'invalid_host'};
|
||||||
|
}
|
||||||
|
const originValues=all(headers,'origin');
|
||||||
|
if (originValues.length>1 || originValues.length===1 && !originMatches(originValues[0],scheme,external))
|
||||||
|
return {status:403,reason:'origin_mismatch'};
|
||||||
|
if (all(headers,'sec-fetch-site').includes('cross-site'))
|
||||||
|
return {status:403,reason:'cross_site_fetch'};
|
||||||
|
return {status:200,reason:'pass',scheme};
|
||||||
|
}
|
||||||
|
const cases=[];
|
||||||
|
const add=(name,patch,opts,expected)=>cases.push({name,patch,opts,expected});
|
||||||
|
const PIN={publicOrigin:'https://panel.example.test',allowedHosts:[]};
|
||||||
|
add('legacy normal same-origin GET', {},{},'pass');
|
||||||
|
add('legacy malformed multi-hop chain', {'x-forwarded-proto':'https, http'},{},'external_scheme');
|
||||||
|
add('legacy duplicated XFP lines', {'x-forwarded-proto':['https','http']},{},'external_scheme');
|
||||||
|
add('pin: normal GET', {},PIN,'pass');
|
||||||
|
add('pin: GET multi-hop https then http',{'x-forwarded-proto':'https, http'},PIN,'pass');
|
||||||
|
add('pin: duplicate XFP lines',{'x-forwarded-proto':['https','http']},PIN,'pass');
|
||||||
|
add('pin: 8-hop bounded chain',{'x-forwarded-proto':'https, http, http, http, http, http, http, http'},PIN,'pass');
|
||||||
|
add('pin: 9-hop rejected',{'x-forwarded-proto':'https, http, http, http, http, http, http, http, http'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: no XFP; public origin configured',{'x-forwarded-proto':null},PIN,'pass');
|
||||||
|
add('pin: no XFH; internal Host still validated',{'x-forwarded-host':null,'origin':'https://panel.example.test'},PIN,'pass');
|
||||||
|
add('pin: wrong XFH',{'x-forwarded-host':'attacker.example.test'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: different allowed Host but wrong configured URL',{'x-forwarded-host':'panel.example.test:8443'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: XFH userinfo rejected',{'x-forwarded-host':'attacker@panel.example.test'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: duplicate XFH lines',{'x-forwarded-host':['panel.example.test','panel.example.test']},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: first hop downgraded HTTP',{'x-forwarded-proto':'http, https'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: HTTP only',{'x-forwarded-proto':'http'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: unrecognized protocol',{'x-forwarded-proto':'ftp, http'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: empty protocol element',{'x-forwarded-proto':'https, '},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: invalid XFP capitalization',{'x-forwarded-proto':'HTTPS'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: correct explicit browser Origin',{'origin':'https://panel.example.test','x-forwarded-proto':'https, http'},PIN,'pass');
|
||||||
|
add('pin: correct explicit Origin with :443',{'origin':'https://panel.example.test:443'},PIN,'pass');
|
||||||
|
add('pin: attacker Origin rejected',{'origin':'https://attacker.example.test'},PIN,'origin_mismatch');
|
||||||
|
add('pin: Origin null rejected',{'origin':'null'},PIN,'origin_mismatch');
|
||||||
|
add('pin: conflicting Origin port rejected',{'origin':'https://panel.example.test:8443'},PIN,'origin_mismatch');
|
||||||
|
add('pin: cross-site API request rejected',{'sec-fetch-site':'cross-site'},PIN,'cross_site_fetch');
|
||||||
|
add('pin: untrusted peer must not apply pin',{'x-forwarded-proto':'https, http','origin':'https://panel.example.test'}, {...PIN,peerTrusted:false},'origin_mismatch');
|
||||||
|
add('pin: credential automation remains strict',{'x-forwarded-proto':'https, http'}, {...PIN,automation:true},'external_scheme');
|
||||||
|
add('pin: Host cannot spoof unallowlisted identity',{'host':'attacker.example.test'},PIN,'pinned_origin_mismatch');
|
||||||
|
add('pin: correct alt port 8443',{'x-forwarded-host':'panel.example.test:8443','origin':'https://panel.example.test:8443'}, {...PIN, publicOrigin:'https://panel.example.test:8443'},'pass');
|
||||||
|
|
||||||
|
function encodedHeaders(patch) {
|
||||||
|
const merged={...defaults,...patch};
|
||||||
|
const result={};
|
||||||
|
for (const [key,value] of Object.entries(merged)) if (value!==null) result[key]=value;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const pending=[];
|
||||||
|
const server=http.createServer((req,res)=>{
|
||||||
|
const item=pending.shift();
|
||||||
|
if (!item) return res.writeHead(500).end('no scenario');
|
||||||
|
const result=guard(map(req.rawHeaders),{...context,...item.opts});
|
||||||
|
res.writeHead(result.status, {'content-type':'application/json'}).end(JSON.stringify(result));
|
||||||
|
});
|
||||||
|
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
|
||||||
|
async function request(item) {
|
||||||
|
pending.push(item);
|
||||||
|
return await new Promise((resolve,reject)=>{
|
||||||
|
const req=http.request({hostname:'127.0.0.1',port:server.address().port,path:'/api/auth/session',
|
||||||
|
agent:false,headers:encodedHeaders(item.patch)},res=>{
|
||||||
|
let data='';res.setEncoding('utf8');res.on('data',part=>data+=part);
|
||||||
|
res.on('end',()=>resolve(JSON.parse(data)));
|
||||||
|
});
|
||||||
|
req.on('error',reject);req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
for (const item of cases) {
|
||||||
|
const result=await request(item);
|
||||||
|
assert.equal(result.reason,item.expected,item.name);
|
||||||
|
assert.equal(result.status,item.expected==='pass' ? 200:403,item.name);
|
||||||
|
}
|
||||||
|
const repeats=[];
|
||||||
|
for (const configured of [false,true]) {
|
||||||
|
for (let attempt=1;attempt<=12;attempt++) {
|
||||||
|
const malformed=attempt%4===0;
|
||||||
|
const result=await request({patch:{'x-forwarded-proto':malformed?'https, http':'https'},
|
||||||
|
opts:configured? PIN : {}});
|
||||||
|
assert.equal(result.status,configured?200:(malformed?403:200));
|
||||||
|
repeats.push({pin:configured,attempt,forwardedProto:malformed?'https, http':'https',status:result.status,reason:result.reason});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const summary={cases:cases.length,repeated:repeats.length,unconfigured403:repeats.filter(x=>!x.pin&&x.status===403).length,
|
||||||
|
pinned403:repeats.filter(x=>x.pin&&x.status===403).length};
|
||||||
|
const output={release:'0.31.4',classification:'Node.js HTTP simulation of admin guard, not compiled RustProxy',
|
||||||
|
assertions:cases.length+repeats.length,scenarios:cases.map(({name,expected})=>({name,expected})),repeats,summary};
|
||||||
|
if (process.argv[2]) fs.writeFileSync(process.argv[2],JSON.stringify(output,null,2)+'\n');
|
||||||
|
console.log(`PASS: ${output.assertions} HTTP-simulation assertions: ${summary.cases} cases + ${summary.repeated} resumes; `+
|
||||||
|
`403 with no pin ${summary.unconfigured403}/12, 403 with pin ${summary.pinned403}/12`);
|
||||||
|
} finally {server.close();}
|
||||||
@@ -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.3"
|
assert manifest["package"]["version"] == "0.31.4"
|
||||||
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.3"' in lock
|
assert 'name = "rust-proxy"\nversion = "0.31.4"' 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.3",
|
"README.md": "# RustProxy 0.31.4",
|
||||||
"CHANGELOG.md": "## 0.31.3",
|
"CHANGELOG.md": "## 0.31.4",
|
||||||
"ROADMAP.md": "## 0.31.3",
|
"ROADMAP.md": "## 0.31.4",
|
||||||
"AGENTS.md": "Current source version: 0.31.3.",
|
"AGENTS.md": "Current source version: 0.31.4.",
|
||||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.31.3",
|
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.31.4",
|
||||||
}.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.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']:
|
for marker in ['"version": "0.31.4"', '"/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
|
||||||
@@ -1394,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.3"' in openapi
|
assert '"RuntimeInformation"' in openapi and '"version": "0.31.4"' 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")
|
||||||
@@ -1633,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.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("PASS: 0.31.4 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.")
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Check the 0.31.3 dependency/source migration; optionally require a resolved lock.
|
"""Check the 0.31.4 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.3'
|
assert manifest['package']['version'] == '0.31.4'
|
||||||
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.3 source dependency APIs, manifest, TLS features, toolchain images and Swagger pin (static only)')
|
print('PASS: 0.31.4 source dependency APIs, manifest, TLS features, toolchain images and Swagger pin (static only)')
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Rust-independent documentation/Reader consistency checks for the 0.31.3 release."""
|
"""Rust-independent documentation/Reader consistency checks for the 0.31.4 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
|
||||||
@@ -29,6 +29,8 @@ required = [
|
|||||||
DOCS / "V0.25.2_AUDIT.md",
|
DOCS / "V0.25.2_AUDIT.md",
|
||||||
DOCS / "V0.25.1_AUDIT.md",
|
DOCS / "V0.25.1_AUDIT.md",
|
||||||
DOCS / "V0.25.0_AUDIT.md",
|
DOCS / "V0.25.0_AUDIT.md",
|
||||||
|
DOCS / "IMPLEMENTATION_0.31.4.md",
|
||||||
|
DOCS / "IMPLEMENTATION_0.31.3.md",
|
||||||
DOCS / "IMPLEMENTATION_0.31.1.md",
|
DOCS / "IMPLEMENTATION_0.31.1.md",
|
||||||
DOCS / "TEST_REPORT.md",
|
DOCS / "TEST_REPORT.md",
|
||||||
DOCS / "CHANGELOG_TECHNICAL_ARCHIVE.md",
|
DOCS / "CHANGELOG_TECHNICAL_ARCHIVE.md",
|
||||||
@@ -65,7 +67,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.3")
|
assert readme.startswith("# RustProxy 0.31.4")
|
||||||
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
|
||||||
|
|||||||
@@ -77,13 +77,22 @@ const bindSettingsForm = (id, handler) => handlers.set(id, handler);
|
|||||||
const adminStart = administration.indexOf('bindSettingsForm("administration-form", next => {');
|
const adminStart = administration.indexOf('bindSettingsForm("administration-form", next => {');
|
||||||
const adminEnd = administration.indexOf('bindSettingsForm("tls-policy-form",', adminStart);
|
const adminEnd = administration.indexOf('bindSettingsForm("tls-policy-form",', adminStart);
|
||||||
assert(adminStart > 0 && adminEnd > adminStart);
|
assert(adminStart > 0 && adminEnd > adminStart);
|
||||||
runInNewContext(administration.slice(adminStart, adminEnd), {
|
// Use the shipped helper in the extracted callback environment as well.
|
||||||
$: field, bindSettingsForm, latestStatus: { administration: { listen_locked: false } },
|
const hostHelperStart = administration.indexOf(' function pinnedAdminHostname(value) {');
|
||||||
|
const hostHelperEnd = administration.indexOf(' const normalizedRequestedTab', hostHelperStart);
|
||||||
|
assert(hostHelperStart > 0 && hostHelperEnd > hostHelperStart);
|
||||||
|
runInNewContext(administration.slice(hostHelperStart, hostHelperEnd) + administration.slice(adminStart, adminEnd), {
|
||||||
|
$: field, bindSettingsForm, latestStatus: { administration: { listen_locked: false } }, URL,
|
||||||
});
|
});
|
||||||
const adminNext = {};
|
const adminNext = {};
|
||||||
handlers.get('administration-form')(adminNext);
|
handlers.get('administration-form')(adminNext);
|
||||||
assert.deepEqual(JSON.parse(JSON.stringify(adminNext.administration.panel_compression)),
|
assert.deepEqual(JSON.parse(JSON.stringify(adminNext.administration.panel_compression)),
|
||||||
{ zstd: true, br: true, gzip: true });
|
{ zstd: true, br: true, gzip: true });
|
||||||
|
field('admin-public-origin').value = 'https://panel.example.test';
|
||||||
|
field('admin-hosts').value = 'panel.example.test\nother.example.test';
|
||||||
|
const pinnedNext = {};
|
||||||
|
handlers.get('administration-form')(pinnedNext);
|
||||||
|
assert.deepEqual(JSON.parse(JSON.stringify(pinnedNext.administration.allowed_hosts)), ['other.example.test']);
|
||||||
field('admin-compression-br').checked = false;
|
field('admin-compression-br').checked = false;
|
||||||
field('admin-compression-zstd').checked = false;
|
field('admin-compression-zstd').checked = false;
|
||||||
const offNext = {};
|
const offNext = {};
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
|||||||
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/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"]:
|
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.3 source tree: missing {path}")
|
raise ValueError(f"Incomplete RustProxy 0.31.4 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.3 entry point; extract the complete package, not individual files.")
|
raise ValueError("src/lib.rs is not the 0.31.4 entry point; extract the complete package, not individual files.")
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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.3"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
assert 'version = "0.31.4"' 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.3"
|
assert manifest["to_version"] == "0.31.4"
|
||||||
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()
|
||||||
@@ -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.3"' in openapi_0241
|
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.31.4"' 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.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"']:
|
for marker in ['"version": "0.31.4"', '"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.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.")
|
print("PASS: 0.31.4 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__":
|
||||||
|
|||||||
@@ -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.3', uptime_seconds: 4201,
|
version: '0.31.4', 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.3');
|
assert.equal(dollar('runtime-version').textContent, '0.31.4');
|
||||||
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.3 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
|
console.log('PASS: 0.31.4 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
|
||||||
@@ -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.3", "accept": "*/*"}
|
headers = {"user-agent": "RustProxy-Memory-Stress/0.31.4", "accept": "*/*"}
|
||||||
if host_header:
|
if host_header:
|
||||||
headers["host"] = host_header
|
headers["host"] = host_header
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user