This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 23:13:27 +02:00
1 parent e6604e41fc
commit 1dea7f55c8
49 files changed
+4499 -4025

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.25.3.
compatibility. Current source version: 0.25.4.
## Delivery
+6
View File
@@ -4,6 +4,12 @@
> test-only changes and packaging implementation notes were moved to
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.25.4
- Add restore from retained configuration history. An older retained document is validated and saved as a new revision instead of rewinding or overwriting history.
- Protect restore with the current-revision compare-and-swap, administrator session and CSRF controls, and record the source/new revision in administration audit.
- Keep SQLite schema 8 and the 512-revision retention limit; no database migration is required from 0.25.3.
## 0.25.3
- Add vhost and Custom-location HTTP method allow/block policies, with location-level reusable ACL profiles, inline IP ACLs and country access rules enforced before origin work.
Generated
+1 -1
View File
@@ -1688,7 +1688,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.25.3"
version = "0.25.4"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rust-proxy"
version = "0.25.3"
version = "0.25.4"
edition = "2021"
rust-version = "1.88"
description = "RustProxy - independent reverse proxy with local administration"
+11 -11
View File
@@ -1,9 +1,9 @@
# RustProxy 0.25.3
# RustProxy 0.25.4
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.
**0.25.3 release source.**
**0.25.4 release source.**
## Layer-4 streams
@@ -11,14 +11,13 @@ 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.
## What changed in 0.25.3
## What changed in 0.25.4
- Add per-vhost HTTP method policy with explicit allow-list or block-list modes. Method denials return `405 Method Not Allowed`; allow-list mode also emits `Allow`.
- Extend Custom locations with the same method controls plus reusable ACL profile, inline IP ACL and country allow/block policy. Host and location policies compose additively and are enforced before origin connection.
- Generate a 64-character SHA-256 request ID for each exchange, forward it to the origin as `X-Request-ID`, reuse it in diagnostics and persist it in access logging.
- Retain up to 512 manual-configuration revisions in SQLite schema 8 and expose an authenticated revision-history modal with month/day navigation and per-revision change preview.
- Add authenticated restore from retained manual-configuration history. Selecting an older revision validates it through the normal configuration pipeline and publishes it as a new revision; history is never rewound or overwritten.
- Protect restore with the same revision compare-and-swap, administrator session and CSRF controls as ordinary configuration writes, and record `config.restore` with source/new revision metadata in the administration audit.
- Keep SQLite schema 8 and the existing 512-revision retention limit; upgrading from 0.25.3 requires no data migration.
See [host access](docs/VHOST_ACCESS.md), [Custom locations](docs/CUSTOM_LOCATIONS.md), [documentation index](docs/README.md) and the [0.25.3 release audit](docs/V0.25.3_AUDIT.md). Open follow-up work is tracked separately in [TODO](docs/TODO.md).
The 0.25.3 method-policy, location ACL/country, SHA-256 request-ID and revision-history features remain unchanged. See [host access](docs/VHOST_ACCESS.md), [Custom locations](docs/CUSTOM_LOCATIONS.md), [documentation index](docs/README.md) and the [0.25.4 release audit](docs/V0.25.4_AUDIT.md). Open follow-up work is tracked separately in [TODO](docs/TODO.md).
## Documentation
@@ -26,7 +25,8 @@ See [host access](docs/VHOST_ACCESS.md), [Custom locations](docs/CUSTOM_LOCATION
- [docs/README.md](docs/README.md) - canonical documentation index.
- [docs/WEB_SERVER.md](docs/WEB_SERVER.md) - Web Server configuration, HTTP behavior and filesystem security.
- [docs/PHP_FPM.md](docs/PHP_FPM.md) - implemented FastCGI/PHP-FPM integration, safety model and runtime requirements.
- [docs/V0.25.3_AUDIT.md](docs/V0.25.3_AUDIT.md) - 0.25.3 method-policy/location-ACL/request-ID/config-history release audit and compiler gate.
- [docs/V0.25.4_AUDIT.md](docs/V0.25.4_AUDIT.md) - 0.25.4 configuration-history restore release audit and compiler gate.
- [docs/V0.25.3_AUDIT.md](docs/V0.25.3_AUDIT.md) - historical method-policy/location-ACL/request-ID/config-history release audit.
- [docs/V0.25.2_AUDIT.md](docs/V0.25.2_AUDIT.md) - historical 0.25.2 live-backup/symlink/origin recovery audit.
- [docs/V0.25.0_AUDIT.md](docs/V0.25.0_AUDIT.md) - Web Server foundation specification audit.
- [docs/TEST_REPORT.md](docs/TEST_REPORT.md) - verification results and environment limitations.
@@ -93,7 +93,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked
```
The 0.25.3 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
The 0.25.4 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
updates only this package's own version entry. This packaging environment has no Cargo, so the
dependency graph was not re-resolved here. On the compiler-enabled deployment host, run the
locked check/test/release gates below and retain the verified lockfile. `dev.sh` can generate a
@@ -332,7 +332,7 @@ python3 scripts/release_smoke.py
# Runs those checks first, then the Rust suite when Cargo is available:
bash dev.sh test
# Source-only package, with no Git checkout required:
python3 make_zip.py /tmp/RustProxy-0.25.3.zip
python3 make_zip.py /tmp/RustProxy-0.25.4.zip
```
## New management and test-origin guide
+10
View File
@@ -1,5 +1,15 @@
# RustProxy roadmap
## 0.25.4 - Configuration history restore
Current direction: 0.25.3 -> 0.25.4
- [x] Restore an older retained configuration through the normal validation and hot-publish path.
- [x] Save restore as a new revision without deleting or rewinding existing history.
- [x] Require current-revision CAS plus administrator session/CSRF and audit restore source/new revision.
- [x] Add restore action and confirmation to the existing revision-history modal.
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
## 0.25.3 - Request policy, correlation and configuration history
Current direction: 0.25.2 -> 0.25.3
+48 -47
View File
@@ -1,19 +1,19 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
962e4f330d5434c8a41668f96ed998940037d307f9469dd7ce7caf313e12eb92 AGENTS.md
3407d56ce02f80420c4a0571f376df887f2a782b8f44be1674a14b9e58a8c14c CHANGELOG.md
8e75e382dcab138362e5c2ffe171143dfa6fcb5541911fdc488e494d69af2dc7 Cargo.lock
f7bce4570cbd1eeda68abbabd67666cc16663c041f2dd1cd00a10cf635738a38 Cargo.toml
2ebaea6883711a6e1ba51bd8dbffc0b42423c04b7117057ea257cc6bbe2d1df1 AGENTS.md
a3d46a56da8acdf9d93062b22dc1245a32e02515af0e0ba006513a8ef5a82edf CHANGELOG.md
8a98d49a48ff13ca071e6d772977b047e658a7f3fedb5fee39262a754ddbb5be Cargo.lock
d68edcf9603597063ab8554b1f66017406ba1e3231d991ae108a1247040ff123 Cargo.toml
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
0a310a02d96af8491d233b7607d431083e19985aed47ae7585a8b92911d65d6e README.md
11bf5810056176e6233ffdadbddb2fd7b7980dd481ca84ffa84ae645e4a55042 ROADMAP.md
86a633c40f7cfa4122ece46e977f1eda2383a8025b8d34d72e05837a2806d0c1 README.md
13d36f416d7b9aa0e34d5ab3265d5d32c108b05385de32d44a4f03e2688ef860 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
ba49ec0e01e64effa35fbcf55cbcf3c12700c722c07def88c31c41197d4d8001 debian/README.md
91d9886a98127e4bfc99b3d9aab1f76a560c3e98238d3f7a13288f1e2a5996f1 debian/build-package.sh
6ff0bef85dd8006eb604297ca287ee984286ee84dfad2a5e6f9dbfc29335220f debian/README.md
9e26eb93e7facb4c771021ebc8492224659c9316cc2adaec05eeba63cad95efa debian/build-package.sh
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
b2f825f14dab955389bf9da286b3e6a1b9ff3aba93487d555fad1b74399974e3 debian/changelog
2076f96789448281f5e7dd1701bd6c0f46b88e002c7f357946f45cae675e2860 debian/changelog
c90457b19b1b2f7998d3db59a656d215eb0fab23769197405960457b66d95cf7 debian/control
dff2ecb9c14623416981f444bc6df1c4e2bf55d330260f36a312d8d42245400f debian/rules
c537d54fd92a9b97c5f6e6ec63bc1276cda324513e2946b341b2193f63b62e6a debian/rustproxy.environment
@@ -24,7 +24,7 @@ cb0db03a2c1b06fa04a11f7690755a0ca90b17f19ed8fdb228810f505c48c78a debian/rustpro
e8e18df40bcd69d6aa404282679186d9b8256ac63a5b4e80d7f081e8a0095a2e debian/source/format
385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md
7b7fdf383bbaea4f37b5c120e98a24e9a075fba56ae7838a31a5152c7edcde02 deploy/rustproxy.service
5780b0c64b531ff9628a411791e90f76312b75e7689c5d361d0ddc8e491551d7 dev.sh
847962eae2ed508e3f53a8f4610a4137db132e6d2980c93f062b4f61fbb4bcfd dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -33,14 +33,14 @@ bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/d
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go
8c4bead51262d0152dcc3327f3fdd771c4792e2940f9b47b9d62e1c34abbfe9f docker-agent/model.go
1d59f9a52597c3b1fcf7e861703b333e1cf23d07581fea9bb6cfff4e44ea6d82 docker-agent/model.go
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
802a30b793a6ae6a5046489612a23659f33d25e41d0794551381db202daac904 docs/ACME_PROVIDERS.md
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
69ee2212f6bddbb064e0cbdf4f1df2046314bbf33e5b17c92b64dcd41ec67d37 docs/API.md
3ff2b6b7a087ff19363ee1e9fde4f4b378f25d6c278dcf09e7104db414e27664 docs/API_ACCESS.md
e8d585a496dce2c4fe03698e0203c2ff52a24406975a55978c4316450c974dac docs/API.md
36dcb2f163b865afeb55338c28285ea2dd15a75c062d1c5ecf35b97e06f6ea18 docs/API_ACCESS.md
0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md
cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md
c25f3f6763c35dacf718b4b87c5665c2030b1fb3584e8c20940fc3ca7a2d26a3 docs/CACHE_DIAGNOSTICS.md
@@ -50,18 +50,18 @@ db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
9a882f07945b207d032936feb843f29467e4654f8255fa0104d988a54987f982 docs/ERROR_LOGGING.md
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
805f4d2a0fdec6b18cdfd78f4dce784735426d11e0643d835b35752f6aee4d68 docs/INSTALL.md
c802ed6eae5b9835a01b852ee5df64eac26737e82eed47d680d78286f93b19e9 docs/INSTALL.md
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
2942a5a4310a5a21a75b844daf9e82be406e008a60b103cc0e4f16884df93701 docs/LIVE_LOGS.md
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
42e12cd39522e6869dde0db0b348366e412254e19f551579dd5cb2d44d2dfad2 docs/MIGRATION.md
4ee1c176ee014327a66b703939d0875c7417fb5c79c4b722e65cd60ff27c4469 docs/MIGRATION.md
5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md
3da047768f32064c5ef0ad7a4c73c4b14930fe7317c54f5650f426af9f484593 docs/NPM_MIGRATION.md
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md
44e8b1940af190417574866911800ee5385ddce59c1f13c76dff35f02c134af8 docs/PROMETHEUS.md
0176b36e9ac2f2800f0d2776d2c66a2479b6cb931f9a3003218712774dd9cea5 docs/README.md
7a02c823e5eebe9db65f91d98c8091eb2dc1cdb3c6412c55ad7042f28a790bf2 docs/README.md
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md
@@ -69,9 +69,9 @@ ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.m
4bf7b3ea0bfe6c6b0ae0b7884b48b10ba29ff3a71611ec96957a3327d3c6ec88 docs/SECRET_STORE_BACKUP.md
a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
d2c72fe56885488245cd3278416a35451eb7c954eecdfad5cdec88e8e305e3e9 docs/SETTINGS.md
eb7b00e01f8d143722e25bba36a17311cccfe3a7427feb8c017a3a0c2f890a85 docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
8a24d14dfbeb8bcc76b3c876298298bb8d57ff2ca201854453bfed9b752979ea docs/TEST_REPORT.md
67c5965cb0732c671dd5be4cee246e48349114292c32fad3a8851c805c1d8e0d docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
b02957b708174c9def830ced3c70c943a47af7a33467ef858255d04ab92c4af0 docs/TRANSPORT.md
@@ -97,6 +97,7 @@ d0e609febbcd1cfdb8ea0b00b4fa6f081eac01e2d43a81e38a53ae25783152cc docs/V0.24.8_A
32c4053d48e8f24eb6c3395a4bbbc18f1c9790601166c73fb83173a14142789e docs/V0.25.1_AUDIT.md
41793fe2d0fa4a2bd88448f5b3870649f08eeaadd4cd420820647d332e7fdb5a docs/V0.25.2_AUDIT.md
9dbb66e408bcd4d32267724e557fa9f7f44ef9b70db4ab170c61a7a839b7b157 docs/V0.25.3_AUDIT.md
583d548b4a5db9f35a2bbf8ec00ec22397764c5850a8e883a3d12680da517df7 docs/V0.25.4_AUDIT.md
49a0ddd3071a480b331c108b718d497af9c2ec9844b3bfab5c01fbfab64c560d docs/VHOSTS.md
ace2c0dfe9154cbad760fd759efe8f120eb55dd8aa6f10f99933b7556d341f8f docs/VHOST_ACCESS.md
f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md
@@ -112,13 +113,13 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
17e7971fa9d9b141019fba3135d87cb1aa0cbc55d56b0718f879f880b615da26 examples/vhosts-balanced.json
e06905b6363103b1bcee1a09b9e02292033f114d88f3edbea22ddab53e9e5071 examples/vhosts-private.json
529b47c6d4ea44d2a25d1cf9561cb5ee1e05f50a568c6dba5889741bcf936434 grafana/README.md
63d0d880440f7c57da2960964d5c01eb087cbbfbed066e2b937ceea12e55fc9a grafana/rustproxy-overview.json
abf25ef3430fae2bb89c0ae3479b35ba7a6ba973a15c663b311c56c21127e26b grafana/rustproxy-overview.json
30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
1b967bc2a6682232427d83b3fa1d5a365610a38356b8e87efc89c1362e634a49 langs/en_US.json
a28ef2ce1c4e6e736367d31fd47cc89c53babbfb2e491268ef83d7283c11c076 langs/pl_pl.json
98cc369bd8051ee0f3980a2a290ff61bf8959b73ebaf94eee8cbe54260a5094b make_zip.py
f2724c4c684f7b16e59e022be48d337104b95d9c7bb037bdaf00f8f70faed812 langs/en_US.json
f61e0a008511879fa28f07286d3e5f0bceb46710e7f61ed4404b1c287884518e langs/pl_pl.json
b66fc0f10b1b438e4f4e1c14684f05df58220be0613ef08f490650c0da17add6 make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
@@ -137,11 +138,11 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
7e524285bfa2e3dbde0391257660635b039f23b0b3807e9510df663f757a5ad8 scripts/admin_audit.py
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
33b2754a8876a02c60ec067722ad6efa69475e4aee6f1b24eeb6bbbd9b1ccda2 scripts/check_source.py
b2312413cddfe608e76d25cbcf1efd495906af430a8abc32a706b483c9c120d7 scripts/check_source.py
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
f0c72c5911ca674da57356fc3f1dbbb083e6824b778a97e055610b2307d9e93d scripts/deploy_core.py
cf14d73bd73f5b12ec84e8e63428a421fc00beedefdc9837efb3304eef9cc55d scripts/deploy_core.py
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
5c8bb1be175dc1ab9ffd6a4eca87c1711c462167d0dfd08d8c502b31cf623c43 scripts/docs_check.py
02122574c1b88fd76779354f8552862ca619ec243a6ed9ec53573160f831b914 scripts/docs_check.py
6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py
2f846042b98a9148be944fb74a2b023b871ff884693a36847837228613a17679 scripts/memory_contracts.py
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py
@@ -151,8 +152,8 @@ d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a scripts/mock_a
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh
2bee5982de87c92d6420042e967661a9d7c795fde7fa3d72dfb5f98f6684940b scripts/preflight.py
ebec656486c9a685162c46694e2474ecde1f3c2c9864fe88c1a312331de8ec5e scripts/release_smoke.py
88ed7bc34a9d0a8479112295c5c15e8aaca5b4b495c95e896e05a4cfd74bb4d2 scripts/preflight.py
49971b07f0153d8c5afa761418ed22b08d25f174e7b4cc38e3e3d77d4eb21ef4 scripts/release_smoke.py
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example
3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore
@@ -164,11 +165,11 @@ ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_s
1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py
295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py
a65010f828194885ffc7f0f733937957f28e6049cd978bec1380a2b8bfbd0113 scripts/source_module_contracts.py
b097e41c3b55f2b96ad63f5e00849838cd8a1464ce52de6fc38d2633991a92de scripts/swagger_assets.py
e535270272a2f8c8e624a349be9e40a5af431edc49b1291ad7d1be5627fad1f5 scripts/swagger_assets.py
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
344fe38b8fbfa2dc8c425522d2f5c054566f46d9d145ec0686aac5280b6b0164 scripts/upgrade.py
094d4d6680d13418e4ecfc1c4e577642f23ee81247dc1b159b4953a90e644dae scripts/upgrade.py
43db35e7264b0345592172f2d1f6a6e4919fc4cbde2a9ff787acd7fff1d031d6 scripts/web_assets.py
a650d153a5a8f38d1ee53053bde1f0271205f628cddeda74e63f7fa6f885be63 scripts/web_server_contracts.py
ddeb7ca056e890269ff4ccd07aa4f9fdc41368ed2d63610e707d2bdeaded9979 scripts/web_server_contracts.py
b8ce3e8fd08865015ce312fae9c0457612af18d6ef4513a6281b9e3212f8d458 scripts/web_service_ui_test.mjs
d8e2b51c39252dbb4305512fa9b1ef508ec610968aed316b10514fd160298762 src/acl.rs
e8ffed9a9f50a467062d0758aec6aefdc8a39615282b2eef73603d08bc33c00a src/acl/tests.rs
@@ -180,28 +181,28 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi
7acbaf34e7b953b753ab074893e47b5024f0cd756dc4c7a1074067dd936c2e8e src/acme/tests.rs
8a07a8c9d845e55ffa47332d4379e8ffdc2cee09b22e6c6396edb8e768b24540 src/admin/access.rs
21e4ae7e72b0c86ba674bc820aabdd201cd62126fdd09ba22cc4b420e38ad300 src/admin/certificates.rs
63a54085ebae3e731f5f0789661d783367c605455704549d725b001a418cc1b7 src/admin/documentation.rs
94b4e595d0056e0bce6e5d5fcda3a0a89a06d41fbf2ebfaa289eb7b81c34e0aa src/admin/documentation.rs
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
cc5c84d8476e2ba11c3a4b1996ca616eb402bb0aea734434ac6e185e1041736e src/admin/logging_api.rs
468b6062cb806ef489bc107f69d4725dbf9c08d7e18d15b4482ae0f309c15d2f src/admin/mod.rs
28635ff531d1ca575f04b6be06563cc28b94c0d23701dced43e437060d152b25 src/admin/openapi.json
6de3410a03ad4b7b3722e603ff9f6b101072a02c12e0216c77c07406fa8802a2 src/admin/openapi.json
04ac147a823a1b5510977185fc10b4f5e3fddaa76c00ee73ab5209782dd4d859 src/admin/operations.rs
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
8029f4b426d5b40189aa743fbdf79442ec4471360986ac99a09f2ab34bdcd437 src/admin/router.rs
f77cdb17512797f3ff216bc423db647a9e0d2854b7cf775c6ffb853d5c3b2adb src/admin/router.rs
68685c80f57d8e2b0e1d835368893ddc71a8bb3519933fde99e74f6e64a0a6da src/admin/session.rs
e60217a8ae735d07901545cdb6b2a976d9abcc2ba0a998d297e2019c8f376462 src/admin/status.rs
b882846a576ab254f84f52c4120b3b19d31b2fabb1745aef1feb2b5ccf6bb92a src/admin/status.rs
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
9bed1f93a8ef7bea4478b26bc825aacc8825ca70cf26c993fe78ac784aab2526 src/admin/tests.rs
5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs
b8866db46f8be4f3f5bcceb5d84d32fe482f65d534b65cc75b4b3e33dba576c0 src/admin/tests/part_02.rs
b31d594499d7f8164c14b66dd77971b9985cefd99bd4d3686f9ea26e11e5a59c src/admin/tests/part_03.rs
854a17780d447ab28c77bcecd58fbba8a4a63805a6c305ba9fbe82f5b432ea77 src/admin/tests/part_04.rs
829d0ef87b6d0c4d2a8a9e1e299c27d2d84f83a73ea222df45b0384abc2e2da8 src/admin/tests/part_05.rs
02046813070ae7311612592fefec5432582b74b9a9b897c06ee46f363e7fa434 src/admin/tests/part_04.rs
5f0062abc42a3b81c79088dc0824ab4c81a22c6ea765e77d2527d07d13bffd58 src/admin/tests/part_05.rs
23c55f23deeec844a9abc2289440c9e0583aaeb72416fc5f3632a43a864c0efd src/admin/web/acme.js
bdedac825f9176a7c7220304d015c767a6b98f57b02ca7828ff0ee0b21b8ed25 src/admin/web/app/config-history.js
78e7d54d6536cd600dcbe66f0fc06ec109e121dfee1e57f3ca6e61cc6ae04a6a src/admin/web/app/config-history.js
f97188d3c9dcd60b19ff1a18f1e3eb5c942c996fbefe805fdc2ca07fa9785139 src/admin/web/app/core.js
19abc332a1009bcb087fe4ebb58d458739b27480ab50715b03d07fd408302942 src/admin/web/app/runtime.js
122d4b9d95a9ae42a6c9ef0c79ad586d27a56ec7ae77fd1cce2ad5996f9dab09 src/admin/web/app/settings.js
0d0a74e32dc8187962284d80e57df28472f4d51ef66c944f39b7ede2255417b9 src/admin/web/app/settings.js
d0be71555de13f732fd6ee44438a0110eb08c91e78bbc1aa36a1a84182780210 src/admin/web/cache-panel.js
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
3b84a7a08f9f68e2c78fed4523b594c31a251addc5480a69c5f6949908230539 src/admin/web/docker.js
@@ -216,7 +217,7 @@ f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js
ba71109f9ae01fa572cec8e6b964788eedaf9b8c954cd225230e7333ddbc9f96 src/admin/web/index.html
9db0fb42b9ac129c1017f3d273f6397b99924f087705568f5b48cbbe36d62474 src/admin/web/index.html
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json
a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html
@@ -225,7 +226,7 @@ e41619e5cb0ba01a3d287748f28eb38bbe7641f9bedade252d286c18e619c400 src/admin/web/
22318f39f7bb1ae98e65737d3c2ad9e63065b8e94ad59bed560c0e80d3a46949 src/admin/web/redirects.js
811a6b6d7f7c0d0a268113849be021176098097cd33279acb9a9ffc40d2b934f src/admin/web/settings.js
1a30affd6a717095a1763998a5850a79ae0f86b7b694806f2aba2b880735dd1a src/admin/web/streams.js
697d31d26af0428d9fd50ddf70a9cf9084714e597c0818a4367a871b084dcbb5 src/admin/web/style.css
bb8b1e2ae25b90351388f42ac573abc7c7ee7f865ab087548c84f7d6a162a0f2 src/admin/web/style.css
47e98065281c04b4c185d53df4ed0cb12fabc716e52143e6ae3c3c25db62126a src/admin/web/traffic-panel.js
049be02e0f68c3f6702f3bf29be760c4e5bc1199393ed198107c0a264711300d src/admin/web/transform-panel.js
ac79a0f5471f110850bf5ceaaee9a3903bb92fb2edc10e969d8fda81f66571c0 src/admin/web/vhosts/core.js
@@ -307,7 +308,7 @@ a74c5879d66129f8c32677dec9594281c412b890d09605d380c0e3a61a56db23 src/config/rou
7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/config/tests.rs
be6852c11041bcea2c387b76ba3d66a8ac06f8425bd1c976075efe5cc802902d src/config/tests/part_01.rs
467b89435d0d115b227c8c8093d06c3443b5c4c063bc18e322e7d2edf635a02e src/config/tests/part_02.rs
6cda152af92f4cc4ef07406f56c1d90ef65ce4a9b103511d9daf882951cdd885 src/db.rs
fa4e955ad23c0b5b3fb25af9f144c5c483f116fb79568300543b6501575d9e18 src/db.rs
fe229630e81b10b25edc1319857db1ef406cf5acc4cce4b4cd90f855d1cf55de src/db/history.rs
49200d4e69ad929d69d249fc3849b4227170e47016599fb034e6858322588753 src/db/tests.rs
19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs
@@ -333,11 +334,11 @@ e77598ade4ad6fc27f384bd1ee42005cd745c31c2306ffe03a94be6fbf97eee2 src/logging/fo
8c1f4a05a538daeda2de5a35d073750c910aed808746275afe58cf5a9d2c392d src/logging/tests.rs
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
79e1d39d7be966c22faad99146e384407427f2b1cec917b73786c5808ecc12f1 src/main/admin_runtime.rs
9812e4067fab9965c981725315b217c80a395e5aecbf6439e79bc4da0c29722b src/main/bootstrap.rs
e67829ea786e19413cf6299a3023c64da993de3c5fc91cefbdf11cd524afd002 src/main/bootstrap.rs
192623ac6d9ef376648d3dc372e7dedc0a37afbaa3dacf737e2e372228de09e5 src/main/proxy_runtime.rs
f58e3b75ed39e85fb4f943b970ea5bb271e709bce0979fd96a25e416387a81d9 src/main/shutdown.rs
a8783a34c01a288822c71b2a6807cc8d7645e22af5766053fed5c429d4536407 src/main/shutdown/tests.rs
c0281857ff511d9a4042d15ed25948913d8b136fc5d8970df127b5947afa3a54 src/main/tasks.rs
71a093c8afef3e0f8dd0a79111e172b6bccd48a0ace5ea133ee978b8325f9117 src/main/tasks.rs
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
b1ed4042f2d6c1029523c3977b85e2ab6020e2da7d5079f98051cef5e96ca390 src/metrics/vhost.rs
@@ -383,7 +384,7 @@ d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
31dc5cdea00efd63594dc6becd5a9103594696145cbc8e7b7594fb407214ad9d src/runtime/docker.rs
c052b35673d905219a71130b44a5dc0027bf9b853bff0b6bdbed4be4064550ea src/runtime/tests.rs
03144b1b00fb45c5616ca9c9b0e57013e86c1f07f129830aaec98a73b274199f src/runtime/types.rs
bed516efee36731a2232614b50cb0b87decdd0eb5fa9ba4f68d14c8570c89470 src/runtime/updates.rs
8fac75382d2f10e1ce7bf22441d2718cfce779d414f6ac51b390636587091fe0 src/runtime/updates.rs
b4c17b304166592e63c81884076589fbaf725c3f57043f8924c46167b061ac5b src/secret_store.rs
4a5ab7602324408a436ababc32011c9c8d958987505a05040d9633243ed88ae1 src/secret_store/tests.rs
6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs
@@ -441,7 +442,7 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
9c1c8d6eedecb95eadbfb23c3caea562f6f7a995606ef988f25b58c2d03bcdd5 src/web_server/config/model.rs
6191f4d541476c4a3ae296e7dd28bdb9f8a123fc509b47619f6f44519c44d278 src/web_server/config/validation.rs
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
1af493d339623045869226af7ab927e5f4b8ee2f4760ffe28b2a65af8041312b src/web_server/fastcgi/protocol.rs
2f389254aad49f67939a85d34a9fee13719ab1c4c577c052921e9feeaa56d203 src/web_server/fastcgi/protocol.rs
134c4998f6a0aa27e121af1ae3355b7e335488b2ae867ac0a934c58b4eb77191 src/web_server/fastcgi/response.rs
8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs
0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs
@@ -451,14 +452,14 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
fbc170c464ac6ae96113bfb667184008461016845086a0dece17f4dc99f8354f src/web_server/response/file.rs
13fe8e7bc524d330742dd17b3a2b2d6538c4c2a393c6ac835dc0ef433834cb3a src/web_server/response/file.rs
9aa68251a5f549f4ce3b1bda88d4abb9efc629caf444989381b4117c9488e37b src/web_server/response/headers.rs
2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs
cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs
c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs
a62665d7d1d8305cc1a6bca9180b0584c8cd88ca4a6b862190c5c5953ccdbc52 src/web_server/tests/part_02.rs
4e6b46de6d13a92f35daef8f599acf4842671e5c15f3b859d3699a6eb67e03f7 src/web_server/tests/part_02.rs
e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
+1 -1
View File
@@ -1,6 +1,6 @@
# Debian packaging
This directory builds the `rustproxy` binary package for RustProxy 0.25.3.
This directory builds the `rustproxy` binary package for RustProxy 0.25.4.
The package installs:
- `/usr/bin/rust-proxy`
+1 -1
View File
@@ -21,6 +21,6 @@ cargo fetch --locked
dpkg-buildpackage -us -uc -b
mkdir -p dist
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.25.3_*.deb' -o -name 'rustproxy_0.25.3_*.changes' -o -name 'rustproxy_0.25.3_*.buildinfo' \) \
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.25.4_*.deb' -o -name 'rustproxy_0.25.4_*.changes' -o -name 'rustproxy_0.25.4_*.buildinfo' \) \
-exec cp -f {} dist/ \;
echo "Packages copied to $ROOT/dist"
+1 -1
View File
@@ -1,4 +1,4 @@
rustproxy (0.25.3) stable; urgency=medium
rustproxy (0.25.4) stable; urgency=medium
* Add vhost/location HTTP method policy and location-level ACL/country controls.
* Forward hashed request IDs to origins for end-to-end correlation.
+2 -2
View File
@@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi
case "$mode" in
help|-h|--help)
cat <<'HELP'
RustProxy 0.25.3
RustProxy 0.25.4
./dev.sh Build and start proxy + local panel
./dev.sh run [serve flags] Same, with extra server options
./dev.sh admin setup-token Generate/print a one-time setup capability before first login
@@ -139,7 +139,7 @@ if [[ "$mode" == "test" ]]; then
printf 'Checking all Rust targets before tests...\n' >&2
cargo check --locked --all-targets
fi
printf 'Building RustProxy 0.25.3...\n' >&2
printf 'Building RustProxy 0.25.4...\n' >&2
cargo build --locked
BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
case "$mode" in
+1 -1
View File
@@ -1,6 +1,6 @@
package main
const version = "0.25.3"
const version = "0.25.4"
type config struct {
Listen string
+2 -1
View File
@@ -66,6 +66,7 @@ framework extraction failures are not guaranteed to have that JSON shape.
| `GET /healthz` | Public process health/version; not a backend health probe |
| `GET /api/config` | `{revision, config}` |
| `GET /api/config/history` | Retained revision metadata; `?revision=N` returns that document plus the previous retained document |
| `POST /api/config/history/restore` | Input `{revision, expected_revision}`; validates the older retained document and publishes it as a new configuration revision |
| `PUT /api/config` | Input `{expected_revision, config}`; returns the applied envelope; retained for atomic multi-section changes |
| `PUT /api/config/{section}` | Input `{expected_revision, value}`; replaces one validated section and returns the applied envelope |
| `GET /api/backends` | Authenticated per-vhost backend observations and active/desired frontend plans |
@@ -87,7 +88,7 @@ and rechecks session validity. It does not queue an unlimited per-viewer event l
or guarantee replay. The browser falls back to polling. An active dashboard renews
idle activity, not the absolute session deadline.
Configuration history retains the newest 512 successful manual/configuration revisions in SQLite schema 8. `GET /api/config/history` is session-protected like the rest of the administration API; `limit` is bounded to 512 and `revision` selects one retained revision for diff/inspection.
Configuration history retains the newest 512 successful manual/configuration revisions in SQLite schema 8. `GET /api/config/history` is session-protected like the rest of the administration API; `limit` is bounded to 512 and `revision` selects one retained revision for diff/inspection. `POST /api/config/history/restore` also requires the normal CSRF/request-marker protections, rejects stale `expected_revision`, and never rewinds the revision counter: restoring revision 85 while current revision is 120 creates revision 121.
Configuration updates return 409 for a stale revision, 422 for validation failure
and 429 when the eight-operation update admission bound is reached. Persistence and
+1 -1
View File
@@ -1,4 +1,4 @@
# Administration API access - 0.25.3
# Administration API access - 0.25.4
## Sign-in boundary
+1 -1
View File
@@ -1,4 +1,4 @@
# Installing and updating RustProxy 0.25.3
# Installing and updating RustProxy 0.25.4
The scripts prepare a Linux/systemd installation. This source archive is **not a
verified production build**: Rust/Cargo, real CA operations, live systemd startup
+4
View File
@@ -1,5 +1,9 @@
# Deployment and configuration migration history
## 0.25.3 -> 0.25.4
There is no SQLite schema or saved-configuration migration. Schema 8 and the existing 512-row `configuration_history` retention remain unchanged. Version 0.25.4 adds an authenticated restore operation that validates a retained document and saves it as a new revision; existing history is not rewound or overwritten.
## 0.25.2 -> 0.25.3
SQLite schema 7 is upgraded automatically to schema 8. The migration adds `configuration_history`; on first open the current revision is seeded into history, and subsequent successful configuration saves retain the newest 512 revisions for the dashboard revision-history view. No manual SQL is required.
+3 -2
View File
@@ -8,7 +8,8 @@ This is the canonical index for the current project documentation. The authentic
- [ROADMAP](../ROADMAP.md) - planned and completed release scope.
- [CHANGELOG](../CHANGELOG.md) - user/operator-visible changes by release.
- [TODO](TODO.md) - open work only; completed work is not retained as TODO.
- [0.25.3 release audit](V0.25.3_AUDIT.md) - request-method policy, location ACL/country controls, hashed request IDs and configuration-history scope plus release gates.
- [0.25.4 release audit](V0.25.4_AUDIT.md) - retained configuration restore, CAS/audit behavior and release gates.
- [0.25.3 release audit](V0.25.3_AUDIT.md) - historical request-method policy, location ACL/country controls, hashed request IDs and configuration-history scope.
- [0.25.2 release audit](V0.25.2_AUDIT.md) - historical live backup, internal symlink and resumed-origin recovery scope.
- [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards.
- [0.25.0 specification audit](V0.25.0_AUDIT.md) - Web Server foundation point-by-point implementation status.
@@ -58,7 +59,7 @@ This is the canonical index for the current project documentation. The authentic
## Historical or version-labelled context
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone; they are not current-release documentation. The current audit is [V0.25.3_AUDIT.md](V0.25.3_AUDIT.md); [V0.25.2_AUDIT.md](V0.25.2_AUDIT.md) records the prior backup/session hardening milestone and [V0.25.1_AUDIT.md](V0.25.1_AUDIT.md) records the prior Traffic/module-split milestone and [V0.25.0_AUDIT.md](V0.25.0_AUDIT.md) remains the Web Server foundation audit.
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone; they are not current-release documentation. The current audit is [V0.25.4_AUDIT.md](V0.25.4_AUDIT.md); [V0.25.3_AUDIT.md](V0.25.3_AUDIT.md) records the prior request-policy/history milestone and [V0.25.2_AUDIT.md](V0.25.2_AUDIT.md) records the prior backup/session hardening milestone and [V0.25.1_AUDIT.md](V0.25.1_AUDIT.md) records the prior Traffic/module-split milestone and [V0.25.0_AUDIT.md](V0.25.0_AUDIT.md) remains the Web Server foundation audit.
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.
+1 -1
View File
@@ -35,7 +35,7 @@ This is a settings fragment, not a replacement for the existing routing/vhost do
A complete opt-in example is in `examples/cache-dashboard.json`.
The dashboard exposes the complete raw JSON editor under **Settings -> Manual config**.
The same card exposes **Revision history**, which reads the newest 512 retained configuration revisions from SQLite schema 8 and groups them by month/day. Selecting a revision shows timestamp, actor and a bounded path-level before/after diff against the previous retained revision.
The same card exposes **Revision history**, which reads the newest 512 retained configuration revisions from SQLite schema 8 and groups them by month/day. Selecting a revision shows timestamp, actor and a bounded path-level before/after diff against the previous retained revision. An older retained revision can be restored from the same dialog; RustProxy validates it through the normal configuration update path and saves it as a new revision, preserving the prior history.
The historical `/configuration` browser route is retained only as a compatibility alias and
opens that Settings block; it is no longer a primary-navigation item. Prefer the structured
section editors for routine changes so unrelated sections are not republished.
+6 -4
View File
@@ -1,8 +1,8 @@
# Verification report - RustProxy 0.25.3
# Verification report - RustProxy 0.25.4
## Scope
RustProxy 0.25.3 adds vhost/location HTTP method policy, location-scoped ACL-profile/IP/country controls, hashed end-to-end request IDs and retained manual-configuration history on top of 0.25.2. SQLite advances from schema 7 to schema 8 through an additive `configuration_history` migration; existing configuration remains compatible and new policy fields default to disabled/inherited behavior. The dashboard adds a calendar-style revision-history dialog without changing the existing Manual config editor workflow.
RustProxy 0.25.4 adds restore from the retained manual-configuration history introduced in 0.25.3. Selecting an older retained revision validates that historical document through the normal configuration update pipeline and saves it as the next revision instead of rewinding history. SQLite remains at schema 8 with the existing 512-revision retention limit, so upgrading from 0.25.3 requires no database migration.
## Source contracts reviewed
@@ -10,7 +10,9 @@ RustProxy 0.25.3 adds vhost/location HTTP method policy, location-scoped ACL-pro
- Custom locations can select an `acl_profile_id` and add inline IP ACL plus country access policy. Profile and inline rules are compiled with the route and checked before authentication/backend selection; a location cannot weaken a vhost denial.
- Every observed exchange receives a fresh 64-hex SHA-256 `request_id`, overwrites inbound `X-Request-ID` before forwarding, and reuses the same value for diagnostics/logging. Custom header rules continue to protect `X-Request-ID`.
- SQLite schema 8 stores revision/time/actor/document in bounded `configuration_history`; successful CAS writes record history transactionally and retain the newest 512 revisions. The session-protected `/api/config/history` API supports metadata listing and one-revision detail with the previous retained document.
- The Manual config history UI groups retained revisions by month/day, lists revisions for a selected day and renders a bounded recursive path-level before/after diff using text-only DOM operations.
- `POST /api/config/history/restore` accepts an older retained revision plus the currently observed `expected_revision`. It rejects stale or non-older targets, reloads the retained document and passes it through the same startup-lock, certificate-removal, compile, cache-invalidation, persistence and hot-publish path as a normal configuration update.
- Restore persistence uses the existing storage CAS and appends the restored document as the next monotonic revision. The same transaction records `config.restore` with `source_revision=<old> revision=<new>`; the selected historical row is never overwritten.
- The Manual config history UI groups retained revisions by month/day, lists revisions for a selected day and renders a bounded recursive path-level before/after diff using text-only DOM operations. An older selected revision exposes a confirmation-gated restore action; success refreshes the active envelope and history.
- `Config.web_services` is a separate persisted collection; vhost/location `source` defaults to Reverse Proxy for old documents and Web Server stores only `web_service_id`.
- Full configuration compilation validates Web Service IDs, absolute/existing document roots, index names, duplicate IDs and every active Web Service reference. A pure static vhost does not require a backend pool; mixed Web Server and Reverse Proxy locations do.
- Web Server dispatch occurs after normal frontend routing, ACL and Basic/Tinyauth authorization and before backend selection/DNS/cache/upstream work. Static responses continue through the existing custom error-page, response-header/security, compression and transport pipeline.
@@ -49,7 +51,7 @@ RustProxy 0.25.3 adds vhost/location HTTP method policy, location-scoped ACL-pro
## Implemented automated coverage
Rust unit tests in `src/web_server/tests.rs` cover GET/HEAD/index/slash redirect, MIME/length, validators, valid/invalid range handling, If-Range, escaped/repeated directory listings, per-directory index-before-listing fallback, generic 403/HEAD behavior, MIME overrides/default MIME, cache rules, source headers, `try_files`, rewrite redirect/internal behavior, `.htaccess` WordPress-style front-controller routing, nested DirectoryIndex/MIME/Header/Expires behavior, strict-mode rejection and PHP-source protection, hardening probe/method blocking and fingerprint-header stripping, precompressed representations/ranges, download headers, PHP FastCGI fixture execution/failure-source-disclosure behavior, request-body 413 enforcement, custom FastCGI/PHP_VALUE validation and reserved-parameter rejection, direct traversal rejection and Unix/Linux symlink policy. `src/traffic/tests.rs` includes bounded-table and effective-client-IP view coverage. Configuration tests cover pure static vhosts without a pool, mixed Web Server/Reverse Proxy routing, reuse of one Web Service by multiple vhosts, unknown service references and backward-compatible default source behavior. Vhost tests cover method-policy normalization/allow/block behavior, and database tests cover retained configuration revision documents, actor metadata and previous-revision lookup. ACL tests include encoded traversal/separator/malformed-encoding cases.
Rust unit tests in `src/web_server/tests.rs` cover GET/HEAD/index/slash redirect, MIME/length, validators, valid/invalid range handling, If-Range, escaped/repeated directory listings, per-directory index-before-listing fallback, generic 403/HEAD behavior, MIME overrides/default MIME, cache rules, source headers, `try_files`, rewrite redirect/internal behavior, `.htaccess` WordPress-style front-controller routing, nested DirectoryIndex/MIME/Header/Expires behavior, strict-mode rejection and PHP-source protection, hardening probe/method blocking and fingerprint-header stripping, precompressed representations/ranges, download headers, PHP FastCGI fixture execution/failure-source-disclosure behavior, request-body 413 enforcement, custom FastCGI/PHP_VALUE validation and reserved-parameter rejection, direct traversal rejection and Unix/Linux symlink policy. `src/traffic/tests.rs` includes bounded-table and effective-client-IP view coverage. Configuration tests cover pure static vhosts without a pool, mixed Web Server/Reverse Proxy routing, reuse of one Web Service by multiple vhosts, unknown service references and backward-compatible default source behavior. Vhost tests cover method-policy normalization/allow/block behavior, database tests cover retained configuration revision documents, actor metadata and previous-revision lookup, and admin tests cover restoring revision 1 as revision 3 while preserving the previous document and emitting `config.restore`. ACL tests include encoded traversal/separator/malformed-encoding cases.
Rust-independent release checks additionally validate UI/JSON/language/source contracts and reproducible packaging.
+27
View File
@@ -0,0 +1,27 @@
# RustProxy 0.25.4 release audit
RustProxy 0.25.4 adds restore from retained manual-configuration history on top of the schema-8 history introduced in 0.25.3. There is no SQLite schema change.
## Configuration history restore
| Requirement | Status | Evidence |
| --- | --- | --- |
| Restore an older retained revision | IMPLEMENTED | `POST /api/config/history/restore` loads the retained document and passes it through the normal configuration validation/update path. |
| Preserve monotonic revision history | IMPLEMENTED | Restore uses the current `expected_revision` and commits the restored document as the next revision; it never updates or deletes the selected historical row. |
| Reject stale restore actions | IMPLEMENTED | The endpoint compares `expected_revision` with the current published revision and returns conflict before restore when configuration changed. The storage write retains the existing CAS guard. |
| Keep restore private and CSRF-protected | IMPLEMENTED | The endpoint is outside the anonymous allowlist and is routed through the existing administrator session, Origin/request-marker and CSRF guard. |
| Audit restores distinctly | IMPLEMENTED | Restore persistence records `config.restore` with `source_revision=<old> revision=<new>` in the same SQLite transaction as the configuration write/history insert. |
| Reuse hot-publish behavior | IMPLEMENTED | Restore calls the same compile, listener-lock, certificate-removal, cache-invalidation and immutable snapshot publication path used by ordinary configuration updates. |
| Provide dashboard restore UX | IMPLEMENTED | The revision-history detail view exposes a restore action only for an older revision, shows an explicit confirmation, refreshes the current envelope after success and reloads history. |
## Compatibility
SQLite remains at schema version 8 and the history capacity remains 512 revisions. Upgrading from 0.25.3 requires no SQL migration. History retention still begins with the schema-8 migration introduced in 0.25.3.
A restore can fail current validation when an older document is no longer valid in the current runtime environment, for example because referenced certificate files, listener constraints or other external prerequisites changed. In that case the active configuration and revision remain unchanged.
## Verification gates
Rust-independent source, language, documentation, OpenAPI, browser JavaScript, security-contract and release-smoke checks are included in the release tree. Rust tests include an authenticated end-to-end restore case that applies two configurations, restores the first as revision 3 and checks the `config.restore` audit entry and retained previous document.
This packaging workspace does not provide the Rust/Cargo toolchain. Compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` therefore remain mandatory on a release host before production deployment.
+1 -1
View File
@@ -3672,7 +3672,7 @@
},
"timepicker": {},
"timezone": "browser",
"title": "RustProxy 0.25.3 - Operations",
"title": "RustProxy 0.25.4 - Operations",
"uid": "rustproxy-0170",
"version": 2,
"weekStart": ""
+1949 -1946
View File
File diff suppressed because it is too large. Load diff
+1949 -1946
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -88,7 +88,7 @@ def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.25.3.zip')
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.25.4.zip')
output = parser.parse_args().output.resolve()
paths = files()
checksum = ROOT / 'SHA256SUMS'
+19 -10
View File
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
preflight(root)
manifest = tomllib.loads((root / "Cargo.toml").read_text())
assert manifest["package"]["version"] == "0.25.3"
assert manifest["package"]["version"] == "0.25.4"
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
h3dep = manifest["dependencies"]["h3"]
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
lock_path = root / "Cargo.lock"
if lock_path.exists():
lock = lock_path.read_text()
assert 'name = "rust-proxy"\nversion = "0.25.3"' in lock
assert 'name = "rust-proxy"\nversion = "0.25.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"
for release_doc, marker in {
"README.md": "# RustProxy 0.25.3",
"CHANGELOG.md": "## 0.25.3",
"ROADMAP.md": "## 0.25.3",
"AGENTS.md": "Current source version: 0.25.3.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.25.3",
"README.md": "# RustProxy 0.25.4",
"CHANGELOG.md": "## 0.25.4",
"ROADMAP.md": "## 0.25.4",
"AGENTS.md": "Current source version: 0.25.4.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.25.4",
}.items():
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
@@ -224,7 +224,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
for marker in ['"version": "0.25.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.25.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
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
@@ -566,6 +566,15 @@ assert 'Ok(line) => {' in main_source and 'command_tx.send(line).is_err()' in ma
for marker in ['LIVE_STREAM_CAPACITY', 'broadcast::Sender<LogEntry>', 'pub fn subscribe']:
assert marker in logging_source, marker
assert 'SCHEMA_VERSION: i64 = 8' in db_source and 'certificate_renewals' in db_source and '0004_admin_preferences.sql' in db_source and '0005_certificate_api.sql' in db_source and '0006_certificate_api_credentials.sql' in db_source and '0007_secret_store.sql' in db_source and '0008_config_history.sql' in db_source
for marker in ['replace_as_restore', 'config.restore', 'source_revision']:
assert marker in db_source, marker
for marker in ['restore_config_history', '/api/config/history/restore', 'expected_revision']:
assert marker in admin, marker
for marker in ['pub async fn restore_as', 'replace_as_restore']:
assert marker in runtime_source, marker
for marker in ['restoreConfigRevision', 'config-history-restore', 'configHistoryRestoreConfirm']:
assert marker in app_js or marker in html, marker
assert '"/api/config/history/restore"' in openapi
assert 'load_renewal_observations' not in acme_source and 'renewals.json' not in acme_source
for marker in ['pub kind: &\'static str', 'pub log_directory: String', 'pub(crate) fn certbot_log_directory', 'kind: "issue"', '"renewal-dry-run"']:
assert marker in acme_source, marker
@@ -1159,7 +1168,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
assert 'location.pathname === "/logging"' in settings_js_0241
assert '"RuntimeInformation"' in openapi and '"version": "0.25.3"' in openapi
assert '"RuntimeInformation"' in openapi and '"version": "0.25.4"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs")
@@ -1204,5 +1213,5 @@ for schema_name in ["RedirectHost", "NotFoundHost"]:
node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"]
assert "maxItems" not in node, schema_name
print("PASS: 0.25.3 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/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-v8 migrations.")
print("PASS: 0.25.4 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/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-v8 migrations.")
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
+1 -1
View File
@@ -97,7 +97,7 @@ def digest(path: Path) -> str:
def fetch_https(url: str, max_bytes: int) -> bytes:
if urllib.parse.urlparse(url).scheme != 'https':
raise ValueError('GeoIP downloads require HTTPS.')
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.25.3'})
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.25.4'})
with urllib.request.urlopen(request, timeout=30) as response:
if urllib.parse.urlparse(response.geturl()).scheme != 'https':
raise ValueError('GeoIP download redirected away from HTTPS.')
+6 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Rust-independent documentation/Reader consistency checks for the 0.25.3 release."""
"""Rust-independent documentation/Reader consistency checks for the 0.25.4 release."""
from pathlib import Path
import re
from rust_source import rust_source
@@ -14,6 +14,7 @@ required = [
DOCS / "README.md",
DOCS / "WEB_SERVER.md",
DOCS / "TODO.md",
DOCS / "V0.25.4_AUDIT.md",
DOCS / "V0.25.3_AUDIT.md",
DOCS / "V0.25.2_AUDIT.md",
DOCS / "V0.25.1_AUDIT.md",
@@ -26,7 +27,7 @@ for path in required:
assert path.is_file(), f"missing required documentation: {path.relative_to(ROOT)}"
index = (DOCS / "README.md").read_text(encoding="utf-8")
for marker in ["Functional documentation", "Development and testing", "Technical documentation", "Historical or version-labelled context", "TODO.md", "V0.25.3_AUDIT.md", "V0.25.2_AUDIT.md", "V0.25.1_AUDIT.md", "V0.25.0_AUDIT.md", "CHANGELOG_TECHNICAL_ARCHIVE.md", "ROADMAP_HISTORY.md", "/docs"]:
for marker in ["Functional documentation", "Development and testing", "Technical documentation", "Historical or version-labelled context", "TODO.md", "V0.25.4_AUDIT.md", "V0.25.3_AUDIT.md", "V0.25.2_AUDIT.md", "V0.25.1_AUDIT.md", "V0.25.0_AUDIT.md", "CHANGELOG_TECHNICAL_ARCHIVE.md", "ROADMAP_HISTORY.md", "/docs"]:
assert marker in index, marker
for document in sorted(DOCS.glob("*.md")):
if document.name == "README.md" or (document.name.startswith("V") and document.name.endswith("_AUDIT.md")):
@@ -53,13 +54,13 @@ assert '"/docs/content.json"' in admin
readme = (ROOT / "README.md").read_text(encoding="utf-8")
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert readme.startswith("# RustProxy 0.25.3")
assert readme.startswith("# RustProxy 0.25.4")
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
assert roadmap.startswith("# RustProxy roadmap") and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
assert roadmap.startswith("# RustProxy roadmap") and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
assert changelog.startswith("# Changelog\n") and "## 0.25.3" in changelog[:800] and "## 0.25.2" in changelog[:1400]
assert changelog.startswith("# Changelog\n") and "## 0.25.4" in changelog[:1000] and "## 0.25.3" in changelog[:2200] and "## 0.25.2" in changelog[:3200]
assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog
for developer_only in ["Clean the remaining compiler warnings", "Update package/source/UI/OpenAPI/release identity", "source regressions and update release identity"]:
assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}"
+3 -3
View File
@@ -73,12 +73,12 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
flat = module.parent.with_suffix(".rs")
if flat.is_file():
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/proxy/websocket.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/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/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/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/pages.rs", "scripts/mock_server.py", "scripts/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.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/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", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "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/proxy/websocket.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/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/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/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/pages.rs", "scripts/mock_server.py", "scripts/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.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/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", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]:
if not (root / path).is_file():
raise ValueError(f"Incomplete RustProxy 0.25.3 source tree: missing {path}")
raise ValueError(f"Incomplete RustProxy 0.25.4 source tree: missing {path}")
text = (root / "src/lib.rs").read_text(encoding="utf-8")
if '#[path = "auth/mod.rs"]' not in text:
raise ValueError("src/lib.rs is not the 0.25.3 entry point; extract the complete package, not individual files.")
raise ValueError("src/lib.rs is not the 0.25.4 entry point; extract the complete package, not individual files.")
return messages
+12 -5
View File
@@ -123,7 +123,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert backup.is_dir()
assert (backup / "src/obsolete.rs").is_file()
assert not (target / "src/obsolete.rs").exists()
assert 'version = "0.25.3"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert 'version = "0.25.4"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
assert (target / "langs/site.json").is_file()
@@ -134,7 +134,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert (target / "docker-agent/certificate.go").is_file()
assert (target / "docker-agent/server.go").is_file()
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
assert manifest["to_version"] == "0.25.3"
assert manifest["to_version"] == "0.25.4"
assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists()
assert not (backup / "legacy-data").exists()
@@ -545,7 +545,7 @@ def source_contract_smoke() -> None:
assert marker in admin, marker
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
assert '"/api/status"' not in anonymous_block
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.25.3"' in openapi_0241
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.25.4"' in openapi_0241
assert 'id="panel-logging"' not in html
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
assert marker in html, marker
@@ -921,6 +921,10 @@ def source_contract_smoke() -> None:
db_source = rust_source(ROOT, "src/db.rs")
acme_source = rust_source(ROOT, "src/acme.rs")
assert "SCHEMA_VERSION: i64 = 8" in db_source and "certificate_renewals" in db_source and "0004_admin_preferences.sql" in db_source and "0005_certificate_api.sql" in db_source and "0006_certificate_api_credentials.sql" in db_source and "0007_secret_store.sql" in db_source and "0008_config_history.sql" in db_source
runtime_source = rust_source(ROOT, "src/runtime.rs")
assert 'replace_as_restore' in db_source and 'config.restore' in db_source and 'source_revision' in db_source
assert 'restore_config_history' in admin and '/api/config/history/restore' in admin
assert 'pub async fn restore_as' in runtime_source and 'replace_as_restore' in runtime_source
assert "load_renewal_observations" not in acme_source and "renewals.json" not in acme_source
for marker in ["pub kind: &'static str", "pub log_directory: String", "pub(crate) fn certbot_log_directory", 'kind: "issue"', '"renewal-dry-run"']:
assert marker in acme_source, marker
@@ -944,6 +948,8 @@ def source_contract_smoke() -> None:
metrics_source = rust_source(ROOT, 'src/metrics.rs')
style_source = (ROOT / 'src/admin/web/style.css').read_text(encoding='utf-8')
app_source = read_app_js(ROOT)
assert 'restoreConfigRevision' in app_source and 'configHistoryRestoreConfirm' in app_source
assert 'id="config-history-restore"' in html
vhosts_ui = read_vhosts_js(ROOT)
for marker in ['pub enum AppliedEncoding', 'AppliedEncoding::Gzip', 'AppliedEncoding::Brotli', 'AppliedEncoding::Zstd', 'AppliedEncoding::Deflate']:
assert marker in compression_source, marker
@@ -989,7 +995,8 @@ def source_contract_smoke() -> None:
assert "removed_obsolete_runtime_state" in upgrade_source and "legacy-data" not in upgrade_source and "certs_dir.is_symlink()" in upgrade_source
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")
for marker in ['"version": "0.25.3"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert '"/api/config/history/restore"' in openapi
for marker in ['"version": "0.25.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 '"software_update"' in openapi and '"update_available"' in openapi
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
@@ -1164,7 +1171,7 @@ def main() -> None:
source_contract_smoke()
container_contract_smoke()
debian_grafana_contract_smoke()
print("PASS: 0.25.3 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
print("PASS: 0.25.4 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
if __name__ == "__main__":
+1 -1
View File
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
"""TLS verification uses Python's default trust store; never executes a shell."""
started = time.monotonic()
opener = build_opener(HttpsRedirects())
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.25.3"})
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.25.4"})
with opener.open(request, timeout=30) as response, destination.open("wb") as output:
length = response.headers.get("Content-Length")
if length and int(length) > MAX_ARCHIVE:
+3 -3
View File
@@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
for name in MANAGED:
if (target / name).is_symlink():
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
backup = target / ".upgrade-backup" / ("before-0.25.3-" + uuid.uuid4().hex[:12])
backup = target / ".upgrade-backup" / ("before-0.25.4-" + uuid.uuid4().hex[:12])
backup.mkdir(parents=True, mode=0o700)
installed: list[str] = []
moved: list[str] = []
@@ -84,7 +84,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
remove_legacy = legacy.exists()
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.25.3", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.25.4", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
if remove_legacy:
legacy.unlink()
except BaseException:
@@ -115,7 +115,7 @@ def main() -> None:
backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir)
except (OSError, ValueError) as error:
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
print(f"Installed RustProxy 0.25.3. Previous managed source files: {backup}")
print(f"Installed RustProxy 0.25.4. Previous managed source files: {backup}")
print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.")
print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh")
+2 -2
View File
@@ -96,7 +96,7 @@ need(web,
'HeaderValue::from_static("no-store")',
"builtin_error_response(StatusCode::FORBIDDEN, &method)",
"empty_index_can_go_directly_to_directory_listing",
"RustProxy <b>0.25.3</b>",
"RustProxy <b>0.25.4</b>",
"traversal_and_symlink_escape_are_not_served",
"try_files_rewrites_cache_rules_headers_and_downloads_work",
"precompressed_sidecars_keep_mime_validators_and_range_semantics",
@@ -178,7 +178,7 @@ assert "Source" in openapi.get("components", {}).get("schemas", {})
assert "client_ips" in openapi["components"]["schemas"]["TrafficCounters"]["properties"]
assert (ROOT / "docs/WEB_SERVER.md").is_file()
assert (ROOT / "docs/PHP_FPM.md").is_file()
assert (ROOT / "docs/V0.25.3_AUDIT.md").is_file()
assert (ROOT / "docs/V0.25.4_AUDIT.md").is_file()
assert (ROOT / "docs/V0.25.2_AUDIT.md").is_file()
assert (ROOT / "docs/V0.25.0_AUDIT.md").is_file()
assert (ROOT / "docs/TODO.md").is_file()
+1
View File
@@ -72,6 +72,7 @@ static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
("docs/V0.24.8_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.8_AUDIT.md"))),
("docs/V0.25.0_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.0_AUDIT.md"))),
("docs/V0.25.1_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.1_AUDIT.md"))),
("docs/V0.25.4_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.4_AUDIT.md"))),
("docs/V0.25.3_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.3_AUDIT.md"))),
("docs/V0.25.2_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.2_AUDIT.md"))),
("docs/V0.25.0_SECURITY_PREPROD_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.0_SECURITY_PREPROD_AUDIT.md"))),
+171 -2
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.25.3",
"version": "0.25.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."
},
"servers": [
@@ -5046,7 +5046,7 @@
}
}
},
"description": "Sets an HttpOnly SameSite=Strict session cookie. Browser sessions use the configured 7–30 day absolute lifetime. Use the panel sign-in page. No bearer token or URL token is accepted.",
"description": "Sets an HttpOnly SameSite=Strict session cookie. Browser sessions use the configured 7\u201330 day absolute lifetime. Use the panel sign-in page. No bearer token or URL token is accepted.",
"security": [],
"parameters": [
{
@@ -9179,6 +9179,175 @@
}
}
}
},
"/api/config/history/restore": {
"post": {
"summary": "Restore a retained configuration revision as a new revision",
"description": "Loads one retained configuration document, validates it through the normal full configuration update path, and saves it as a new revision. expected_revision must match the current configuration revision; the selected source revision must be older. Existing history is preserved.",
"tags": [
"Configuration"
],
"operationId": "post_api_config_history_restore",
"parameters": [
{
"name": "Origin",
"in": "header",
"required": true,
"schema": {
"type": "string"
},
"description": "Exact http://HOST:ADMIN_PORT. Set automatically by same-origin browser POST."
},
{
"name": "X-RustProxy-Request",
"in": "header",
"required": true,
"schema": {
"type": "string",
"enum": [
"1"
]
}
}
],
"security": [
{
"AdminSession": [],
"CsrfToken": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"revision",
"expected_revision"
],
"properties": {
"revision": {
"type": "integer",
"format": "int64",
"minimum": 0,
"description": "Older retained revision to restore."
},
"expected_revision": {
"type": "integer",
"format": "int64",
"minimum": 0,
"description": "Current revision observed by the caller."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Configuration restored and published as a new revision",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Envelope"
}
}
}
},
"400": {
"description": "Invalid restore request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"401": {
"description": "Authentication required or revoked",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Host, origin or CSRF validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Requested revision is not retained",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Configuration changed; reload before restoring",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"413": {
"description": "Request exceeds the endpoint body limit",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"422": {
"description": "Restored configuration failed current validation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"429": {
"description": "Bounded worker/session/rate limit",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"503": {
"description": "Control-plane storage/worker unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
}
}
+4
View File
@@ -339,6 +339,10 @@ pub fn router(state: Arc<App>) -> Router {
.route("/api/cache/cold/cleanup/{id}", get(cache_cold_cleanup_job))
.route("/api/config", get(configuration).merge(put(update)))
.route("/api/config/history", get(config_history))
.route(
"/api/config/history/restore",
post(restore_config_history).layer(DefaultBodyLimit::max(4096)),
)
.route("/api/config/{section}", put(update_section))
.route("/api/logs", get(logs))
.route("/api/live-logs", get(live_logs_stream))
+43
View File
@@ -342,6 +342,13 @@ struct ConfigHistoryQuery {
limit: Option<usize>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct ConfigHistoryRestore {
revision: u64,
expected_revision: u64,
}
async fn config_history(
State(state): State<Arc<App>>,
Query(query): Query<ConfigHistoryQuery>,
@@ -363,6 +370,42 @@ async fn config_history(
.map_err(auth::internal)?;
Ok(Json(json!({"revisions": revisions})))
}
async fn restore_config_history(
State(state): State<Arc<App>>,
Extension(session): Extension<Session>,
Json(input): Json<ConfigHistoryRestore>,
) -> Result<Json<crate::config::Envelope>, ApiError> {
let current_revision = state.config.load().envelope.revision;
if current_revision != input.expected_revision {
return Err(ApiError(
StatusCode::CONFLICT,
"configuration changed; reload before restoring".into(),
));
}
if input.revision >= current_revision {
return Err(ApiError(
StatusCode::BAD_REQUEST,
"only an older configuration revision can be restored".into(),
));
}
let detail = state
.store
.configuration_revision(input.revision)
.await
.map_err(auth::internal)?
.ok_or_else(|| ApiError(StatusCode::NOT_FOUND, "configuration revision not found".into()))?;
state
.restore_as(
Update {
expected_revision: input.expected_revision,
config: detail.config,
},
session.username,
input.revision,
)
.await
.map(Json)
}
async fn update(
State(state): State<Arc<App>>,
Extension(session): Extension<Session>,
+1
View File
@@ -312,6 +312,7 @@
for (path, method) in [
("/api/config", "PUT"),
("/api/config/cache", "PUT"),
("/api/config/history/restore", "POST"),
("/api/acme/issue", "POST"),
("/api/acme/renew", "POST"),
("/api/docker/refresh", "POST"),
+82
View File
@@ -55,6 +55,7 @@
"cookie"
);
assert!(spec["paths"].get("/api/config").is_some());
assert!(spec["paths"].get("/api/config/history/restore").is_some());
let response = router
.clone()
.oneshot(
@@ -170,3 +171,84 @@
}
store.close().await;
}
#[tokio::test]
async fn retained_configuration_revision_can_be_restored_as_a_new_revision() {
let (router, store, _dir, secret, csrf) = fixture().await;
let cookie = format!("{}={secret}", auth::COOKIE_NAME);
let mut first = Config::default();
first.server_header.value = "first".into();
let response = router
.clone()
.oneshot(
request("/api/config")
.method("PUT")
.header("content-type", "application/json")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::from(
json!({"expected_revision":0,"config":first}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(json_body(response).await["revision"], 1);
let mut second = Config::default();
second.server_header.value = "second".into();
let response = router
.clone()
.oneshot(
request("/api/config")
.method("PUT")
.header("content-type", "application/json")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::from(
json!({"expected_revision":1,"config":second}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(json_body(response).await["revision"], 2);
let response = router
.clone()
.oneshot(
request("/api/config/history/restore")
.method("POST")
.header("content-type", "application/json")
.header("cookie", &cookie)
.header("origin", "http://127.0.0.1:9090")
.header("x-rustproxy-request", "1")
.header("x-csrf-token", &csrf)
.body(Body::from(
json!({"revision":1,"expected_revision":2}).to_string(),
))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let restored = json_body(response).await;
assert_eq!(restored["revision"], 3);
assert_eq!(restored["config"]["server_header"]["value"], "first");
let audit = store.recent_audit().await.unwrap();
assert_eq!(audit[0].actor, "owner");
assert_eq!(audit[0].action, "config.restore");
assert_eq!(audit[0].detail, "source_revision=1 revision=3");
let detail = store.configuration_revision(3).await.unwrap().unwrap();
assert_eq!(detail.config.server_header.value, "first");
assert_eq!(detail.previous_config.unwrap().server_header.value, "second");
store.close().await;
}
+32 -1
View File
@@ -1,4 +1,4 @@
let configHistoryEntries = [];
let configHistoryEntries = [], configHistorySelectedRevision = null;
function historyDate(value, options = {}) {
try { return new Intl.DateTimeFormat(locale(), options).format(new Date(Number(value) * 1000)); }
catch { return new Date(Number(value) * 1000).toLocaleString(); }
@@ -13,6 +13,12 @@ function historyValue(value) {
const text = JSON.stringify(value, null, 2);
return text.length > 6000 ? `${text.slice(0, 6000)}\n...` : text;
}
function syncConfigHistoryRestoreButton() {
const button = $("config-history-restore"), revision = Number(configHistorySelectedRevision);
if (!button) return;
button.hidden = configHistorySelectedRevision === null;
button.disabled = busy || !envelope || !Number.isSafeInteger(revision) || revision >= Number(envelope.revision);
}
function configDiff(before, after, path = "$", output = [], depth = 0) {
if (output.length >= 200) return output;
if (Object.is(before, after)) return output;
@@ -29,8 +35,10 @@ function configDiff(before, after, path = "$", output = [], depth = 0) {
function renderConfigRevisionDetail(detail) {
const entry = detail?.revision;
if (!entry) return;
configHistorySelectedRevision = Number(entry.revision);
$("config-history-detail-title").textContent = `${t("configHistoryRevision")} ${entry.revision}`;
$("config-history-detail-meta").textContent = `${historyDate(entry.time, { dateStyle: "medium", timeStyle: "short" })} · ${entry.actor}`;
syncConfigHistoryRestoreButton();
document.querySelectorAll(".config-history-revision").forEach(button => button.classList.toggle("active", Number(button.dataset.revision) === Number(entry.revision)));
const diff = configDiff(entry.previous_config, entry.config);
const box = $("config-history-diff"); box.replaceChildren();
@@ -95,7 +103,29 @@ function renderConfigHistory() {
}
if (!months.length) calendar.append(el("p", t("configHistoryEmpty"), "empty"));
}
async function restoreConfigRevision() {
const revision = Number(configHistorySelectedRevision);
if (!envelope || busy || !Number.isSafeInteger(revision) || revision >= Number(envelope.revision)) return;
const expectedRevision = Number(envelope.revision);
const message = t("configHistoryRestoreConfirm").replace("{revision}", revision).replace("{current}", expectedRevision);
if (!(await appConfirm(message, { danger: true, confirmLabel: t("configHistoryRestoreAction") }))) return;
const button = $("config-history-restore"), epoch = authEpoch;
busy = true; syncConfigHistoryRestoreButton();
try {
const value = await api("/api/config/history/restore", { method: "POST", body: JSON.stringify({ revision, expected_revision: expectedRevision }) });
if (!authenticated || epoch !== authEpoch) return;
envelope = value;
await refreshCertificateInventory();
if (!authenticated || epoch !== authEpoch) return;
renderConfig();
toast(t("configHistoryRestored").replace("{revision}", revision).replace("{new_revision}", value.revision));
await refresh(true);
if (authenticated && epoch === authEpoch) await loadConfigHistory();
} catch (error) { toast(error.message, true); if (error.status === 401) lock(); }
finally { busy = false; if (button?.isConnected) syncConfigHistoryRestoreButton(); }
}
async function loadConfigHistory() {
configHistorySelectedRevision = null; syncConfigHistoryRestoreButton();
$("config-history-summary").textContent = t("configHistoryLoading");
try {
const result = await api("/api/config/history?limit=512"); configHistoryEntries = result.revisions || []; renderConfigHistory();
@@ -105,3 +135,4 @@ async function loadConfigHistory() {
$("config-history-open").addEventListener("click", () => { const dialog = $("config-history-dialog"); if (!dialog.open) dialog.showModal(); void loadConfigHistory(); });
$("config-history-close").addEventListener("click", () => $("config-history-dialog").close());
$("config-history-reload").addEventListener("click", () => void loadConfigHistory());
$("config-history-restore").addEventListener("click", () => void restoreConfigRevision());
+1 -1
View File
@@ -6,7 +6,7 @@ function renderSettings() {
if (!settingsDirty.has("response-form")) {
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
$("expose-request-id").checked = !!diagnostics.request_id;
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.3";
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.4";
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
}
+3 -3
View File
@@ -1472,7 +1472,7 @@
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
</select>
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
id="server-value" maxlength="256" placeholder="RustProxy/v0.25.3"
id="server-value" maxlength="256" placeholder="RustProxy/v0.25.4"
spellcheck="false" />
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
<div class="switch-grid">
@@ -2218,7 +2218,7 @@
</section>
</div>
</section>
<footer class="page-footer"><span>RustProxy <b>0.25.3</b></span><span id="footer-status">API - /
<footer class="page-footer"><span>RustProxy <b>0.25.4</b></span><span id="footer-status">API - /
dropped: 0</span></footer>
</div>
</main>
@@ -2370,7 +2370,7 @@
<aside class="card config-history-detail">
<div class="config-history-detail-head">
<div><span class="small-label" data-i18n="configHistoryChanges">CHANGES</span><h3 id="config-history-detail-title">-</h3></div>
<span class="fine" id="config-history-detail-meta"></span>
<div class="config-history-detail-actions"><span class="fine" id="config-history-detail-meta"></span><button class="outline" data-i18n="configHistoryRestoreAction" id="config-history-restore" type="button" hidden disabled>Restore revision</button></div>
</div>
<div class="config-history-revisions" id="config-history-revisions"></div>
<div class="config-history-diff" id="config-history-diff"><p class="empty" data-i18n="configHistorySelectDay">Select a day.</p></div>
+14
View File
@@ -7734,6 +7734,18 @@ button.config-history-day.is-selected {
margin: 3px 0 0;
}
.config-history-detail-actions {
display: flex;
flex-direction: column;
gap: 7px;
align-items: flex-end;
text-align: right;
}
.config-history-detail-actions button {
white-space: nowrap;
}
.config-history-revisions {
display: flex;
flex-wrap: wrap;
@@ -7788,4 +7800,6 @@ button.config-history-day.is-selected {
.config-history-layout { grid-template-columns: 1fr; overflow: auto; }
.config-history-browser, .config-history-detail { overflow: visible; }
.config-history-change-values { grid-template-columns: 1fr; }
.config-history-detail-head { flex-direction: column; }
.config-history-detail-actions { align-items: flex-start; text-align: left; }
}
+32 -7
View File
@@ -181,6 +181,33 @@ impl Store {
}
/// Compare-and-swap and audit are one transaction. Publication is handled by App.
pub async fn replace_as(&self, expected: u64, config: &Config, actor: &str) -> Result<bool> {
self.replace_as_action(expected, config, actor, "config.apply", None)
.await
}
pub(crate) async fn replace_as_restore(
&self,
expected: u64,
config: &Config,
actor: &str,
source_revision: u64,
) -> Result<bool> {
self.replace_as_action(
expected,
config,
actor,
"config.restore",
Some(source_revision),
)
.await
}
async fn replace_as_action(
&self,
expected: u64,
config: &Config,
actor: &str,
audit_action: &str,
source_revision: Option<u64>,
) -> Result<bool> {
let expected: i64 = expected.try_into().context("revision overflow")?;
ensure!(expected < i64::MAX, "revision overflow");
let document = serde_json::to_string(config)?;
@@ -206,13 +233,11 @@ impl Store {
.bind(history::CONFIG_HISTORY_CAPACITY)
.execute(&mut *tx)
.await?;
audit_tx(
&mut tx,
actor,
"config.apply",
&format!("revision={revision}"),
)
.await?;
let audit_detail = source_revision.map_or_else(
|| format!("revision={revision}"),
|source| format!("source_revision={source} revision={revision}"),
);
audit_tx(&mut tx, actor, audit_action, &audit_detail).await?;
tx.commit().await?;
Ok(true)
}
+1 -1
View File
@@ -41,7 +41,7 @@ use tracing_subscriber::prelude::*;
#[command(
name = "rust-proxy",
version,
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.25.3"
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.25.4"
)]
struct Cli {
#[arg(
+1 -1
View File
@@ -226,7 +226,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
}
});
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
println!("RustProxy live logs 0.25.3 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
println!("RustProxy live logs 0.25.4 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
if interactive { print!("> "); io::stdout().flush()?; }
let mut filters = LiveLogFilters::default();
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
+34 -5
View File
@@ -20,6 +20,22 @@ impl App {
self: &Arc<Self>,
update: Update,
actor: String,
) -> Result<config::Envelope, ApiError> {
self.update_as_action(update, actor, None).await
}
pub async fn restore_as(
self: &Arc<Self>,
update: Update,
actor: String,
source_revision: u64,
) -> Result<config::Envelope, ApiError> {
self.update_as_action(update, actor, Some(source_revision)).await
}
async fn update_as_action(
self: &Arc<Self>,
update: Update,
actor: String,
source_revision: Option<u64>,
) -> Result<config::Envelope, ApiError> {
let slot = self.update_slots.clone().try_acquire_owned().map_err(|_| {
ApiError(
@@ -80,11 +96,24 @@ impl App {
"configuration changed; reload before saving".into(),
));
}
match this
.store
.replace_as(expected_revision, &prepared.envelope.config, &actor)
.await
{
let persisted = match source_revision {
Some(source) => {
this.store
.replace_as_restore(
expected_revision,
&prepared.envelope.config,
&actor,
source,
)
.await
}
None => {
this.store
.replace_as(expected_revision, &prepared.envelope.config, &actor)
.await
}
};
match persisted {
Ok(true) => {}
Ok(false) => {
return Err(ApiError(
+1 -1
View File
@@ -156,7 +156,7 @@ pub(super) fn fastcgi_params(
}
params.insert("HTTPS".into(), if context.tls { "on" } else { "off" }.into());
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.25.3".into());
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.25.4".into());
params.insert("REDIRECT_STATUS".into(), "200".into());
if let Some(value) = headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()) {
params.insert("CONTENT_TYPE".into(), value.into());
+1 -1
View File
@@ -180,7 +180,7 @@ pub(super) fn directory_listing(path: &str, entries: &[DirectoryEntry]) -> Vec<u
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
));
}
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.25.3</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.25.4</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
html.into_bytes()
}
+1 -1
View File
@@ -202,7 +202,7 @@ async fn empty_index_can_go_directly_to_directory_listing() {
let body = std::str::from_utf8(&body).unwrap();
assert!(body.contains("file.txt"));
assert!(body.contains("DIRECTORY LISTING"));
assert!(body.contains("RustProxy <b>0.25.3</b>"));
assert!(body.contains("RustProxy <b>0.25.4</b>"));
}
#[tokio::test]