This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 16:47:24 +02:00
1 parent 59ddabb72b
commit 174389e5b1
48 files changed
+676 -200

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.1.
compatibility. Current source version: 0.25.2.
## 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.2
- Allow password-encrypted full backups while RustProxy is running by using a consistent SQLite snapshot and a private staged copy of durable filesystem state; restore remains offline and atomic.
- Preserve validated internal relative symlinks in backup/restore, including Certbot `live/` links into `archive/`, while rejecting absolute, escaping and dangling links. Version-1 backup archives remain restorable.
- Recover a restored/resumed authenticated dashboard tab from a transient `auth.invalid_origin` response with one bounded same-origin reload; server-side Host, Origin, request-marker and CSRF validation remain unchanged.
## 0.25.1
- Reorganize **Traffic** into compact Overview, Clients, Requests and Delivery sub-tabs and visualize all bounded analytics dimensions with donut or horizontal bar charts instead of requiring a long page scroll.
Generated
+1 -1
View File
@@ -1688,7 +1688,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.25.1"
version = "0.25.2"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rust-proxy"
version = "0.25.1"
version = "0.25.2"
edition = "2021"
rust-version = "1.88"
description = "RustProxy - independent reverse proxy with local administration"
+12 -12
View File
@@ -1,9 +1,9 @@
# RustProxy 0.25.1
# RustProxy 0.25.2
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.1 release source.**
**0.25.2 release source.**
## Layer-4 streams
@@ -11,13 +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.1
## What changed in 0.25.2
- Reorganize **Traffic** into Overview, Clients, Requests and Delivery sub-tabs so per-vhost analytics stay compact without a long page scroll. Render all bounded traffic dimensions as charts: donuts for low-cardinality distributions and horizontal bars for User-Agent, client IP, path, route and byte-volume rankings.
- Keep the Rust source-tree 500-line ceiling as a release contract, isolate unit tests from implementation files and strengthen split-module regression checks for dangling attributes/imports. Split the Docker Agent and oversized dashboard source bundles into bounded source modules while preserving the existing browser asset URLs.
- Fix the first Web Server module split regressions around `htaccess`, `server_protocol`, `anyhow::Context` and dangling derive boundaries; compiler-enabled `cargo check` remains a release-host gate when the packaging workspace lacks Rust.
- Create password-encrypted full backups without stopping RustProxy. SQLite is captured with a consistent snapshot and durable files are copied into private staging before the encrypted archive is written; restore remains offline and atomic.
- Preserve safe internal relative symlinks across backup/restore, including Certbot `live/` -> `archive/` certificate links. Absolute, escaping and dangling symlinks are rejected instead of being followed.
- Recover restored/resumed authenticated dashboard tabs from a transient invalid-origin state with a bounded same-origin reload. Server-side Host, Origin, request-marker and CSRF checks remain strict.
See [Web Server](docs/WEB_SERVER.md) for configuration and security details, [documentation index](docs/README.md) for the current documentation set, and [0.25.1 release audit](docs/V0.25.1_AUDIT.md) for point-by-point release status. Open follow-up work is tracked separately in [TODO](docs/TODO.md).
See [Web Server](docs/WEB_SERVER.md) for configuration and security details, [documentation index](docs/README.md) for the current documentation set, and [0.25.2 release audit](docs/V0.25.2_AUDIT.md) for point-by-point release status. Open follow-up work is tracked separately in [TODO](docs/TODO.md).
## Documentation
@@ -25,7 +25,7 @@ See [Web Server](docs/WEB_SERVER.md) for configuration and security details, [do
- [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.1_AUDIT.md](docs/V0.25.1_AUDIT.md) - 0.25.1 Traffic/modularization release audit and compiler gate.
- [docs/V0.25.2_AUDIT.md](docs/V0.25.2_AUDIT.md) - 0.25.2 live-backup/symlink/origin recovery release audit and compiler gate.
- [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.
- [docs/TODO.md](docs/TODO.md) - unresolved follow-up only.
@@ -91,7 +91,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked
```
The 0.25.1 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
The 0.25.2 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
@@ -308,10 +308,10 @@ language JSON files survive the update; shipped files replace their old names,
whose previous bytes remain in the backup. Unknown custom source edits are not
merged. Do not overlay individual source files onto an old tree.
RustProxy 0.25.1 keeps the existing SQLite schema version 7. Web Services and vhost/location
RustProxy 0.25.2 keeps the existing SQLite schema version 7. Web Services and vhost/location
source selection are stored in the normal revisioned configuration document, so no separate
database migration is required; omitted source fields still default to Reverse Proxy. Once a
0.25.1 configuration containing the new fields is saved, an older binary may reject unknown
0.25.2 configuration containing the new fields is saved, an older binary may reject unknown
configuration fields. Roll back using the pre-upgrade data backup as well as the old source,
not just the binary.
@@ -329,7 +329,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.1.zip
python3 make_zip.py /tmp/RustProxy-0.25.2.zip
```
## New management and test-origin guide
+13
View File
@@ -1,5 +1,16 @@
# RustProxy roadmap
## 0.25.2 - Live backup and resumed-session hardening
Current direction: 0.25.1 -> 0.25.2
- [x] Allow encrypted full-backup creation while RustProxy owns the data-directory process lock.
- [x] Snapshot SQLite consistently and stage durable files before archive encryption.
- [x] Preserve only validated relative symlinks that resolve inside the staged data tree, including Certbot `live` -> `archive` links.
- [x] Keep restore offline, validated, staged and atomically activated.
- [x] Recover transient invalid-origin state in restored/resumed admin tabs with a one-shot same-origin reload, without weakening server Host/Origin/CSRF checks.
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
## 0.25.1 - Traffic UX and source modularization
Current direction: 0.25.0 -> 0.25.1
@@ -9,6 +20,7 @@ Current direction: 0.25.0 -> 0.25.1
- [x] Keep every Rust source file at or below 500 lines and keep unit tests outside implementation files.
- [x] Split oversized dashboard source bundles and the Docker Agent into bounded source modules while preserving behavior and URLs.
- [x] Guard the Web Server split against dangling attributes and missing `htaccess`, `server_protocol` and `anyhow::Context` imports.
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
## 0.25.0 - Web Server foundation
@@ -35,6 +47,7 @@ Current direction: 0.24.10 -> 0.25.0
- [x] Keep Web Server inside the existing routing, ACL/auth, custom-header/error-page/security and H1/H2/H3 response pipeline; do not create separate routing/error/cache subsystems.
- [x] Add unit/source-contract coverage, OpenAPI/UI language coverage and Web Server documentation.
- [x] Clean README/Docs/CHANGELOG/ROADMAP material, add a canonical docs index, authenticated Markdown Reader, 0.25 specification audit and open-work TODO.
- [ ] Run compiler-enabled `./dev.sh fmt`, `./dev.sh check`, `./dev.sh test` plus live H1/H2/H3/static-file acceptance on the release host.
## Web Server follow-up
+47 -45
View File
@@ -1,19 +1,19 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
b170ebacc706ae4fd4ccfd1e0e39ec09b32339a1d4c0c09e8c4a256a30b6088a AGENTS.md
c7267b5be9b10b758c56a7bb6201378bb07e92d945c3068ac7057011414eb61b CHANGELOG.md
2de8e335cb69d6e2375d9c40e5dfa92b9259d475bc91db9da1e8aea5178d1c0e Cargo.lock
37ebd727f68dc6552d83927466b41d020677acf8b61cb5c6a8606e6dc0d349a6 Cargo.toml
871884dcd2f86bd4ef429940a03da356836fd9d29af00008bbf4688d10cd79df AGENTS.md
fd5d1f9be9487eb58bfa55a2eb138e1ef675fe32c3bfd069d6d81e486fe5c592 CHANGELOG.md
547274c990951a99d2a40f2ad86363da4dc3110d0d6384c985a61152c90a8024 Cargo.lock
d918e60ed622b589f1ad116e78ecbdf24149d2a144025fbfde4a5b6513b9d181 Cargo.toml
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
2e96d60cb489209c0c24344738ee7b2e2228b0c56e5e179b02040df5cf52a0ce README.md
183292392aab167a5fc0172c0059530e8f098d39958f07bcd7b843c0d625b85f ROADMAP.md
a8f3a799da3ef63b9e95109c01e806b6e4ee9e0190ae0b3f046bf0b810f117ee README.md
3d1defbb40639dc707945297165eeab70795d25b6b0bdec1a333f9b59e06a16a ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
66f7c9aa12782d6e57b6a512c9cb10f623abd8cbb9ddd4582f19013e837964b5 debian/README.md
58e94c1c9fab848aca8ae2b79ab4ed071bcbc7841130e91c3a8e08114e3bf918 debian/build-package.sh
0543cfaad02787b04037321d101cff6052cad68275694386f5bbaa6f35e731d8 debian/README.md
4ca1bc64c9147f8f8705e81f7c3db3376ff84227ebac24c7f5516a0e24e5b203 debian/build-package.sh
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
941c7b6c16e59f037c60b26278584819cbf4a898ce11f164ca94d0cac4644df4 debian/changelog
60b631c2ec8299d1dc82f11505a9a541dd46c0bc3acd35895734193331497f91 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
565c83dde85cfb32af90950bd57ff0480c467bc97f65145806bb92ed8b1f4ced dev.sh
35a9e2248a62d9d06875a327c6925cdd3880a9e0ee7e9735b566c688ef5fbf8c 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
fb8c89f015e3a05edb16b84e0a76771a9ed9cb347d339ea41fefd788812f71a9 docker-agent/model.go
91b976d55742ae69bf31558edb5fabf9e6d79e63dfb2d1a10bd6f76cbf3974f5 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
6fb6dd879b83380187fc720e2668e412c3eacb66350e466b4aa1c3e6dba76edd docs/API.md
7598602a113e951bc1b0af5f1aedd3dae7c4486270599cc014f17ab552085f5a docs/API_ACCESS.md
c46fa66c4f6b56ce74456d557072c712964e8180d599d10adee4d4f20b6d8329 docs/API_ACCESS.md
0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md
cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md
c25f3f6763c35dacf718b4b87c5665c2030b1fb3584e8c20940fc3ca7a2d26a3 docs/CACHE_DIAGNOSTICS.md
@@ -50,28 +50,28 @@ c51ae0b6cacdd9b34f7b80872192d4a1e1c4e2ad697006d24855864c9e136a1b docs/CUSTOM_LO
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
9a882f07945b207d032936feb843f29467e4654f8255fa0104d988a54987f982 docs/ERROR_LOGGING.md
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
3912d71181c50d4c96f53f592429e188f34bfee757e214c90c59b49acd181e34 docs/INSTALL.md
94dd9a1be26d95de773a0b5f07ad460988cd84e29833f6a724102383903c4c9a docs/INSTALL.md
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
b30e6307d00580471a8ea44cdc88cbe2139676357f736660cb53f7120e8fae24 docs/LIVE_LOGS.md
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
17571cbe8aa7de141bb6225d07231da6b92893c9bf3b654c07474caac8e7c3db docs/MIGRATION.md
75b2de691687af350c5cb6332f9a20d8c15ea1021813dde274a23b7f9a8a1293 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
51dc5a75acb7b58e894dd23427e32f75b062c770b9cf7772aebff90c07800edc docs/README.md
cf89251573e13f47344980c1ca4135dd01794914cb494d237d9d07c404cf6395 docs/README.md
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md
585ef0ac4492e03adca46949823e5a7538f56d9768090cd4dc080bfce4305b52 docs/ROADMAP_HISTORY.md
22c5ac843705fa34b2ed331b42b20b2e5fc657fe2601e42b1663d3d4114f7090 docs/SECRET_STORE_BACKUP.md
4bf7b3ea0bfe6c6b0ae0b7884b48b10ba29ff3a71611ec96957a3327d3c6ec88 docs/SECRET_STORE_BACKUP.md
a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
559a0653ec7d4bc05d677408f91931881be03bc49b87ead629d2cafab9af2916 docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
bf9fda355405b92309300cb2639a453b780ec2ebbefc8a9374174af41d8804fa docs/TEST_REPORT.md
68f8985f169620ee6cc571cb4aba146466ffade4bf21af096bef979454f6fb90 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
b02957b708174c9def830ced3c70c943a47af7a33467ef858255d04ab92c4af0 docs/TRANSPORT.md
@@ -95,6 +95,7 @@ d0e609febbcd1cfdb8ea0b00b4fa6f081eac01e2d43a81e38a53ae25783152cc docs/V0.24.8_A
6131ef0ac65af2c9544d9d44ecfb680587cb358b37efd9b2d1cf69f26e935861 docs/V0.25.0_AUDIT.md
1a8a07eb150100e6d5ab8443dac664038e1d838033609df168aaf60a15aeb84f docs/V0.25.0_SECURITY_PREPROD_AUDIT.md
32c4053d48e8f24eb6c3395a4bbbc18f1c9790601166c73fb83173a14142789e docs/V0.25.1_AUDIT.md
41793fe2d0fa4a2bd88448f5b3870649f08eeaadd4cd420820647d332e7fdb5a docs/V0.25.2_AUDIT.md
d64070b2186f9971862e9c929373d90a38022a3a9f3030e2014e280cd7b392f8 docs/VHOSTS.md
e32472cea5ef3cd25c058e55df8adfcc1d7bf03e0d12dba69d2f2b9b94fe04b7 docs/VHOST_ACCESS.md
f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md
@@ -110,13 +111,13 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
17e7971fa9d9b141019fba3135d87cb1aa0cbc55d56b0718f879f880b615da26 examples/vhosts-balanced.json
e06905b6363103b1bcee1a09b9e02292033f114d88f3edbea22ddab53e9e5071 examples/vhosts-private.json
529b47c6d4ea44d2a25d1cf9561cb5ee1e05f50a568c6dba5889741bcf936434 grafana/README.md
a64b996085d39688a3c5c5dee986a3ecdf576a7ec4b7430091765678b1b44d7b grafana/rustproxy-overview.json
0cbe8c6805f62a884a81d5bf6c2d0ced49f269c1dd055237478f39ffca405f25 grafana/rustproxy-overview.json
30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
402b463e17362363f3199d743286d26fc69b2f4342c23d4b6747974d21451a28 langs/en_US.json
931dbbcb75711d5a52bce45a3e1b5fdd0eec2ae13eb7854ce322b2e3cebd8a5b langs/pl_pl.json
de2a01f795e2ca6dd52ca8de46d9f41b810a2e798678c3f8651a4cbf0df35814 make_zip.py
615786f55505353ffa79930e726eb76001f04070d3f1b719c899df583fe25c3d make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
@@ -134,11 +135,11 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
7e524285bfa2e3dbde0391257660635b039f23b0b3807e9510df663f757a5ad8 scripts/admin_audit.py
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
23fd428ef2285f473b09cf2a945c562a9f0738913614b837190423f4654569fb scripts/check_source.py
23a42be7b6c405aa678576bdc316db50e6f09e8a7288cb04fed44d1bf6102d4e scripts/check_source.py
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
d1bba7e7e810b4bbd4fd6b0292c00099e93f34dae734b236328230112fac4292 scripts/deploy_core.py
d9eefaad26f831d07a2dc030ac39f13e6e6430c1fd94dace0073dba390c9aae2 scripts/deploy_core.py
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
741f49cafc470baab8a00779cb954cb78530ac51cefa344580d463a88dd3226b scripts/docs_check.py
0d6b407fcaa5adf674a9fe425b7c3ef5150d87cdbe18c575648e0f8a7f3e1f2b scripts/docs_check.py
6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py
2f846042b98a9148be944fb74a2b023b871ff884693a36847837228613a17679 scripts/memory_contracts.py
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py
@@ -148,8 +149,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
693907aa06a608600761b666fb0ee7ed081898c2b4664d2d134b216debdd9005 scripts/preflight.py
29d71ff431637282f540ed36214606443c17bc7e7c0dec0611cb4636e4c561bf scripts/release_smoke.py
e50e1ac1a55f9266da81cdf353638183c59f42fb9561209b18d8ba57dc7de4eb scripts/preflight.py
c0913cceb60ca9f82bb0966f384a966d3c34ad7624c0dcdef4b7a22b8424802e 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
@@ -161,11 +162,11 @@ ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_s
1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py
295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py
2470b4b4625070fd896ae8438f7cb672ce3bfa8ffab328e2d83f2fd3b5f0af31 scripts/source_module_contracts.py
732645adaefae38cca28901ce3786d5ac5756030bc5aa879400a9d90801d5225 scripts/swagger_assets.py
e4d58ea628547a9d5b0247f320cd4c655caf83b8a942f07bfdc8faf1ab808a59 scripts/swagger_assets.py
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
1f4d60d918939e7e7cbd9e13b0ed40668f0c40b16317cfc4ab3d3d0733d83670 scripts/upgrade.py
115093b451c8be421652e23d3661f8fe3606eaa7569a65ca97fdfe88c1f07491 scripts/upgrade.py
ea20bd340ecf3687c2721c522b4da09e88498de683bc4288bb85202969fd3d75 scripts/web_assets.py
b630b5221730a9e7939c8751acbea8c046ebe2832ae265f3f825785041e9a0f1 scripts/web_server_contracts.py
c2b4ff7f13d1b1e4887d465fd35f1f8c2a431e642c54190dad9644d012238889 scripts/web_server_contracts.py
b8ce3e8fd08865015ce312fae9c0457612af18d6ef4513a6281b9e3212f8d458 scripts/web_service_ui_test.mjs
d8e2b51c39252dbb4305512fa9b1ef508ec610968aed316b10514fd160298762 src/acl.rs
e8ffed9a9f50a467062d0758aec6aefdc8a39615282b2eef73603d08bc33c00a src/acl/tests.rs
@@ -177,11 +178,11 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi
7acbaf34e7b953b753ab074893e47b5024f0cd756dc4c7a1074067dd936c2e8e src/acme/tests.rs
8a07a8c9d845e55ffa47332d4379e8ffdc2cee09b22e6c6396edb8e768b24540 src/admin/access.rs
21e4ae7e72b0c86ba674bc820aabdd201cd62126fdd09ba22cc4b420e38ad300 src/admin/certificates.rs
e414672b1a7f8c083fe7020405ca05df3cbce193139b7247ed74cbaea8fc8954 src/admin/documentation.rs
ae1ac6507a66bf2d0b27e6e4df99fe31bd81a52e616784eed2803730ba234f6f src/admin/documentation.rs
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
cc5c84d8476e2ba11c3a4b1996ca616eb402bb0aea734434ac6e185e1041736e src/admin/logging_api.rs
468b6062cb806ef489bc107f69d4725dbf9c08d7e18d15b4482ae0f309c15d2f src/admin/mod.rs
27543e3a5034cae663827a3ec2bd93c3a0e70c105abbfe45be16c2f8418de1b6 src/admin/openapi.json
4a8df2959f43c71b12a6d5d77613137ae508408a0baf5d30900dffad1c042c58 src/admin/openapi.json
04ac147a823a1b5510977185fc10b4f5e3fddaa76c00ee73ab5209782dd4d859 src/admin/operations.rs
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
b7a1b9535d01db8b9cb0f97048203dffd55404b331ec8e0386c63e8c417bf19a src/admin/router.rs
@@ -195,9 +196,9 @@ b31d594499d7f8164c14b66dd77971b9985cefd99bd4d3686f9ea26e11e5a59c src/admin/test
7c1c18d2d07311f7fcffd2872cca6d5e56212242e4ab7a8912395b4df6d3dcd2 src/admin/tests/part_04.rs
829d0ef87b6d0c4d2a8a9e1e299c27d2d84f83a73ea222df45b0384abc2e2da8 src/admin/tests/part_05.rs
23c55f23deeec844a9abc2289440c9e0583aaeb72416fc5f3632a43a864c0efd src/admin/web/acme.js
c4f879c172e30e569ef2bd4003abdd73b3b3d775943a69c38157f011ae0ef9df src/admin/web/app/core.js
d164400471f97860792fd7c0726df9ca807cff9076b122e80337f7511a31ba80 src/admin/web/app/runtime.js
ad8fc39dc4b3227730d8707b1091f80f366827c17f47dc54567acea6c23c1fdd src/admin/web/app/settings.js
f97188d3c9dcd60b19ff1a18f1e3eb5c942c996fbefe805fdc2ca07fa9785139 src/admin/web/app/core.js
19abc332a1009bcb087fe4ebb58d458739b27480ab50715b03d07fd408302942 src/admin/web/app/runtime.js
006952bbdd922772814276e37b9d3f1ca4d93b1d1ca266972647743aadadf78f 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
@@ -212,7 +213,7 @@ f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js
6370e91e80e5ddb049a332e9269afcc60ee9e8160408a82b5778d02c1f989692 src/admin/web/index.html
5ac6490404f1f23dd558d5b3f5a07b9098c2813b502b09b7efc272a6c1fe4f59 src/admin/web/index.html
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json
a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html
@@ -228,15 +229,16 @@ f42f079f648ce80ec6c55d281d73c9dcc69a120e6cdb7e3eb32edda6bf99aecc src/admin/web/
3c483d7b1eb69455d7bceb83f44f2b8fd60e02b2ec0dd03f4b69308525fa54a4 src/admin/web/vhosts/editor.js
769383dfc5afa28e01a1e99c9eebcce39fc9457fba0602992f750acab9911236 src/admin/web/vhosts/render.js
0de35a0671f3ac8fed62260d63ae43d04efce4947428ebdb9254cff0295295f9 src/admin/web/web-server.js
42c99af2cd73ab9d257e076d38c1b1a81e743ed04e96c440ad9617a394fbbf6a src/auth/files.rs
98c00d99822f9be54fc58afb6d4b577901712effd2c5eddce014312681864625 src/auth/files.rs
b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs
d19f47e061d5965eb3f4100063c396f8438f14e14c0fe5e9d4d32f82f72dddb7 src/auth/mod.rs
e0b37c58d4c391d8f4fa4b1af38a6b53d5107789584797440afb6ef20a711f3b src/auth/mod.rs
1ceb57d97cbff655f185e126d98b8dc7884c7aac865e1f70e3c6d199685c13a7 src/auth/store.rs
745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs
4f123aa55c69cd3b13db31ae5d9b6984911eecd922b5e8fc60437fd54265d1f1 src/backup.rs
968bf5c4ddb67fd9b53aed051234fb42df35b5045f26add611c144ba399814dc src/backup/archive.rs
b1740707011300581d1e5d414b81ecfb774083567aebfea807b4b53ed425e52a src/backup/tests.rs
4b143f32b96a0640345353f71d69e345b415efbbe89d7b24b00641aae4e5a7d4 src/backup/validation.rs
b0efb68dec4b0e8611684457b21044d136882ddb5c529f934061a55d377d9b28 src/backup.rs
002f8f4ba59684937f7601967b6d88b84c4aed641a13222e50500330917c4916 src/backup/archive.rs
61b0b50d5ed6b4465b8a38ce10f386d877a6c8961a2ec3813976e1a65a511d12 src/backup/snapshot.rs
daf0686d97d6f570f061b601e3904e15d1fcfa4c11fba9c50199cbfe0ab8e6a2 src/backup/tests.rs
32b9da896fd275256ca749189184aa42e82a2ede7e11332ca221b8c2c9f4181f src/backup/validation.rs
426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs
6521d493edffd36a68a5753d8c808dd66394243f97fd96984b7cbe60588666f1 src/balance/health.rs
35d42b26de1757a490e936b907bae6803c51582aaaa6a9209eb61a701dd586d9 src/balance/lease_runtime.rs
@@ -302,7 +304,7 @@ fcd733906d4477911a20ba9315635c88c5fa3f8d515420390024e06a750571a0 src/config/rou
7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/config/tests.rs
be6852c11041bcea2c387b76ba3d66a8ac06f8425bd1c976075efe5cc802902d src/config/tests/part_01.rs
467b89435d0d115b227c8c8093d06c3443b5c4c063bc18e322e7d2edf635a02e src/config/tests/part_02.rs
236f27a916b278411d0f45408b1489339043c35e8fa9beea1e131821c2588c1a src/db.rs
8568fcbc8e5d06631827fea4fd165c5596bd530c948ec50babca30f7fc4dade0 src/db.rs
cef6f5d63aa33433df5acc40c22e55a55c8ea275424b200084eceea3e3432cbd src/db/tests.rs
19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs
e69031875ffec32c9f548c43bed5f1a5c4c626e9568c9173e3b093f9a8b73036 src/diagnostics.rs
@@ -327,11 +329,11 @@ b1f2fa0df9ffb7e621d0009c30a188402f0741701f68b77977ef1f40421ab659 src/logging/mo
d55dddba3f35a4f51a9b224af3b776ce7d25c8e57ae061e130e451aa5769edea src/logging/tests.rs
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
79e1d39d7be966c22faad99146e384407427f2b1cec917b73786c5808ecc12f1 src/main/admin_runtime.rs
91cc63d6a3404361b2aadaff2899bfe7dba6871bfac28599ab3d5c5266a2718a src/main/bootstrap.rs
077c840d16e90847d8ec41b804405214048b6645d6425034f5da4d8f78d2a4ee src/main/bootstrap.rs
192623ac6d9ef376648d3dc372e7dedc0a37afbaa3dacf737e2e372228de09e5 src/main/proxy_runtime.rs
f58e3b75ed39e85fb4f943b970ea5bb271e709bce0979fd96a25e416387a81d9 src/main/shutdown.rs
a8783a34c01a288822c71b2a6807cc8d7645e22af5766053fed5c429d4536407 src/main/shutdown/tests.rs
6392ac75b2c4e3adef89cd071f5de24be7ea6f69e9209c49e714cce530ec97d2 src/main/tasks.rs
54fe6de7bf34db0237c1dbd0cf626f114320c27687923c801a1ec8c8b1af1f3f src/main/tasks.rs
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
b1ed4042f2d6c1029523c3977b85e2ab6020e2da7d5079f98051cef5e96ca390 src/metrics/vhost.rs
@@ -435,7 +437,7 @@ fae5a47efda9a4ae4dc94711ecc5857e10b3c20aa2bf2c742c59651a85b9d978 src/vhost/test
9c1c8d6eedecb95eadbfb23c3caea562f6f7a995606ef988f25b58c2d03bcdd5 src/web_server/config/model.rs
6191f4d541476c4a3ae296e7dd28bdb9f8a123fc509b47619f6f44519c44d278 src/web_server/config/validation.rs
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
d06e753a2ac7c3867e4a9311716d5ee071bcc7ad05ba81d3613dbb7e2f4f2c92 src/web_server/fastcgi/protocol.rs
5e3a0148ac9e2d50160c8e655b378c4a061f8912012d1cb272be50300ec8ce8a 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
@@ -445,14 +447,14 @@ d06e753a2ac7c3867e4a9311716d5ee071bcc7ad05ba81d3613dbb7e2f4f2c92 src/web_server
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
ce97afa23fcb6ca1fff7327dc8286e293bdecfbf704a98057a7bef8b1a189f0a src/web_server/response/file.rs
d0c5802ed8b1d6248c3fe75f3c1853a649f2407324ec802cd1f066720c5468de 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
c5ad6d26743fbbd934fe6fa146e92b699a945fb57e28a8666eb017f2c319f247 src/web_server/tests/part_02.rs
f9135f6a387b9b163a96f8b3d7b3da75f13b4f3795be66c0ea7609912816221c 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.1.
This directory builds the `rustproxy` binary package for RustProxy 0.25.2.
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.1_*.deb' -o -name 'rustproxy_0.25.1_*.changes' -o -name 'rustproxy_0.25.1_*.buildinfo' \) \
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.25.2_*.deb' -o -name 'rustproxy_0.25.2_*.changes' -o -name 'rustproxy_0.25.2_*.buildinfo' \) \
-exec cp -f {} dist/ \;
echo "Packages copied to $ROOT/dist"
+8
View File
@@ -1,3 +1,11 @@
rustproxy (0.25.2) stable; urgency=medium
* Allow consistent encrypted backup creation while the service is running.
* Preserve safe internal Certbot symlinks across backup and restore.
* Recover resumed admin tabs from transient invalid-origin state without relaxing origin checks.
-- RustProxy Release <release@localhost> Sun, 04 Oct 2026 16:26:00 +0200
rustproxy (0.25.1) stable; urgency=medium
* Reorganize Traffic into compact Overview, Clients, Requests and Delivery tabs with donut and bar charts.
+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.1
RustProxy 0.25.2
./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.1...\n' >&2
printf 'Building RustProxy 0.25.2...\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.1"
const version = "0.25.2"
type config struct {
Listen string
+5 -2
View File
@@ -1,4 +1,4 @@
# Administration API access - 0.25.1
# Administration API access - 0.25.2
## Sign-in boundary
@@ -41,7 +41,10 @@ Exact anonymous methods/paths:
The document allowlist above is intentionally session-aware: a browser refresh on a protected
panel path first validates the cookie server-side instead of rendering a false logged-out state.
The login document also rechecks `/api/auth/session` when restored from browser history, preventing
a stale back/forward-cache copy from contradicting a still-valid session. Every other admin
a stale back/forward-cache copy from contradicting a still-valid session. The authenticated shell
also treats `auth.invalid_origin` during startup/session revalidation as a stale restored-page signal
and performs at most one reload of the current same-origin URL. A repeated failure is shown normally;
server Host/Origin, request-marker and CSRF validation is unchanged. Every other admin
path/method, including full assets, `/api/metrics`, `/metrics`, unknown endpoints and the OpenAPI
schema, requires an active database session.
Missing/invalid/expired cookies return `401 {"error":"auth.required"}` before
+1 -1
View File
@@ -1,4 +1,4 @@
# Installing and updating RustProxy 0.25.1
# Installing and updating RustProxy 0.25.2
The scripts prepare a Linux/systemd installation. This source archive is **not a
verified production build**: Rust/Cargo, real CA operations, live systemd startup
+6
View File
@@ -1,5 +1,11 @@
# Deployment and configuration migration history
## 0.25.1 -> 0.25.2
There is no SQLite schema or saved-configuration migration. Full backup creation may now run while the service is active; restore still requires RustProxy to be stopped. Backup archive version 2 preserves validated internal relative symlinks such as Certbot `live/` entries pointing into the profile `archive/` tree. Existing archive-version-1 backups remain accepted for restore.
The administration server keeps the same strict Host/Origin/request-marker/CSRF checks. The browser shell only adds a bounded one-shot same-origin reload when a restored or resumed authenticated tab receives `auth.invalid_origin`.
## 0.25.0 -> 0.25.1
There is no SQLite schema or saved-configuration migration. 0.25.1 reorganizes the Traffic dashboard and source tree only; existing Web Services, vhosts, traffic settings, cache state and administrator data remain compatible. Browser asset URLs remain `/assets/app.js` and `/assets/vhosts.js` even though their source is now split into bounded modules.
+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.1 release audit](V0.25.1_AUDIT.md) - Traffic/module-split release scope, compiler-regression guards and remaining release-host gate.
- [0.25.2 release audit](V0.25.2_AUDIT.md) - live backup, internal symlink and resumed-origin recovery scope plus release gates.
- [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.
- [0.25.0 pre-production security audit](V0.25.0_SECURITY_PREPROD_AUDIT.md) - threat-oriented source/configuration review, fixes made in-session and mandatory runtime security gates.
- [TEST_REPORT](TEST_REPORT.md) - verification commands, results and environment limitations.
@@ -56,7 +57,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.1_AUDIT.md](V0.25.1_AUDIT.md); [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.2_AUDIT.md](V0.25.2_AUDIT.md); [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.
+5 -4
View File
@@ -18,7 +18,7 @@ Do not copy only `config.sqlite3` as a Secret Store backup: the local key materi
## Create a full backup
Stop RustProxy first so the data directory lock is available. The output file must be outside the RustProxy data directory and must not already exist.
`backup create` may run while RustProxy is serving traffic. If the service is stopped, the CLI takes the data-directory lock; if the service is running, it creates a consistent SQLite snapshot and stages durable filesystem state without taking ownership of that lock. The output file must be outside the RustProxy data directory and must not already exist.
```bash
rust-proxy --data-dir /var/lib/rustproxy backup create \
@@ -36,15 +36,16 @@ The encrypted backup contains the durable application state needed for recovery,
- backup-wrapped Secret Store key material,
- certificates and private keys,
- ACME state and durable provider data,
- other durable RustProxy data accepted by the backup manifest.
- other durable RustProxy data accepted by the backup manifest,
- validated internal relative symlinks, including Certbot `live/` links into the same profile's `archive/` tree.
It excludes rebuildable/runtime data such as cache, logs, temporary files, sockets, PID/lock files and SQLite WAL/SHM sidecars.
It excludes rebuildable/runtime data such as cache, logs, temporary files, sockets, PID/lock files and SQLite WAL/SHM sidecars. Symlinks are accepted only when their target is relative and resolves inside the staged RustProxy data tree; absolute, escaping or dangling links fail backup/restore. Restore recreates accepted links as links rather than dereferencing them.
The outer container derives a backup key with Argon2id and encrypts authenticated chunks with AES-256-GCM. The backup password is independent from the local Secret Store key.
## Restore
Restore is CLI-only and requires an interactive TTY for the password:
Restore is CLI-only, requires RustProxy to be stopped, and requires an interactive TTY for the password:
```bash
rust-proxy --data-dir /var/lib/rustproxy backup restore \
+2 -2
View File
@@ -1,8 +1,8 @@
# Verification report - RustProxy 0.25.1
# Verification report - RustProxy 0.25.2
## Scope
RustProxy 0.25.1 is a focused Traffic-UX and source-modularization release on top of the reusable Web Server foundation from 0.25.0. Web Services are independent configuration resources; vhosts and custom locations only choose a source type and reference a Web Service ID. The current development tree provides static GET/HEAD streaming, directory/index behavior, validators, single ranges, document-root confinement, ordered `try_files`, Web Service rewrites, a bounded migration-oriented `.htaccess` compatibility layer, expanded MIME policy, per-extension/MIME Cache-Control rules, fixed source headers, precompressed sidecars, forced-download extensions and optional bounded FastCGI/PHP-FPM. The Web Service editor is organized into General, Routing, .htaccess, MIME, Cache & headers, Delivery, PHP-FPM and Security tabs, with one-shot presets for Static/SPA/PHP/WordPress/Drupal/Laravel/Symfony/Joomla/PrestaShop/Downloads. Existing 0.25 work also includes repeatable directory listing, Web Server vhost-wide metrics, Logging sub-tabs, corrected structured-log source validation, bounded per-vhost Traffic analytics with effective-client-IP rankings/donut charts, User-Agent/optional-country logging, stable Web Service rendering after fast refreshes, generic/customizable 403 handling and automatic RSS relief for reclaimable allocator/HOT-cache memory. The `/docs` surface separates the Markdown Reader from API/OpenAPI and documents the implemented PHP-FPM runtime.
RustProxy 0.25.2 is a focused live-backup, backup-symlink and resumed-admin-session hardening release on top of the 0.25.1 source tree. Web Services are independent configuration resources; vhosts and custom locations only choose a source type and reference a Web Service ID. The current development tree provides static GET/HEAD streaming, directory/index behavior, validators, single ranges, document-root confinement, ordered `try_files`, Web Service rewrites, a bounded migration-oriented `.htaccess` compatibility layer, expanded MIME policy, per-extension/MIME Cache-Control rules, fixed source headers, precompressed sidecars, forced-download extensions and optional bounded FastCGI/PHP-FPM. The Web Service editor is organized into General, Routing, .htaccess, MIME, Cache & headers, Delivery, PHP-FPM and Security tabs, with one-shot presets for Static/SPA/PHP/WordPress/Drupal/Laravel/Symfony/Joomla/PrestaShop/Downloads. Existing 0.25 work also includes repeatable directory listing, Web Server vhost-wide metrics, Logging sub-tabs, corrected structured-log source validation, bounded per-vhost Traffic analytics with effective-client-IP rankings/donut charts, User-Agent/optional-country logging, stable Web Service rendering after fast refreshes, generic/customizable 403 handling and automatic RSS relief for reclaimable allocator/HOT-cache memory. The `/docs` surface separates the Markdown Reader from API/OpenAPI and documents the implemented PHP-FPM runtime.
## Source contracts reviewed
+28
View File
@@ -0,0 +1,28 @@
# RustProxy 0.25.2 release audit
RustProxy 0.25.2 is a focused backup-integrity and resumed-admin-session hardening release. It does not change the SQLite schema or saved configuration format.
## Backup and restore
| Requirement | Status | Evidence |
| --- | --- | --- |
| Create a full encrypted backup while RustProxy is running | IMPLEMENTED | `backup create` no longer requires ownership of `process.lock`; SQLite is captured with `VACUUM INTO` and validated before archiving. |
| Keep offline behavior deterministic | IMPLEMENTED | When the service is stopped, the CLI still acquires and holds the data-directory lock during backup. |
| Preserve Certbot-style internal symlinks | IMPLEMENTED | Archive version 2 records file/directory/symlink entry type and restores validated relative symlink targets as symlinks. |
| Prevent symlink escape | IMPLEMENTED | Absolute or lexically escaping targets are rejected; the staged tree is canonicalized and every link must resolve inside it before archive creation/activation. |
| Reject dangling backup symlinks | IMPLEMENTED | Live snapshot validation requires every staged symlink target to exist. |
| Keep configured certificate files coherent with the DB snapshot | IMPLEMENTED | Certificate files referenced by the exact SQLite snapshot are required to exist as regular files in the staged filesystem snapshot; capture retries on concurrent rotation. |
| Restore older backups | IMPLEMENTED | Archive version 1 remains accepted; version-1 entries continue to deserialize as regular files. |
| Restore safely | IMPLEMENTED | Restore still requires the service to be stopped, verifies authenticated encryption, manifest/checksums, SQLite/Secret Store state, stages privately, then atomically activates. |
## Administration origin recovery
The administration server's Host, externally visible scheme/authority, Origin, request-marker and CSRF validation is unchanged. The authenticated browser shell now recognizes only `auth.invalid_origin` during startup/session revalidation and performs at most one reload of the current same-origin URL. A successful session check clears the recovery marker; a repeated failure is surfaced normally instead of looping.
This addresses browser-restored/BFCache-style tabs where a manual refresh previously repaired the page without weakening the server trust boundary.
## Verification gates
Rust-independent source, documentation, web-contract and release-smoke checks are part of the release tree. Backup tests include a simulated live process lock and round-trip of a Certbot-style `live/fullchain.pem -> ../../archive/...` symlink, plus escaping/absolute-link rejection.
Compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` remain mandatory on a release host with the Rust toolchain installed.
+1 -1
View File
@@ -3672,7 +3672,7 @@
},
"timepicker": {},
"timezone": "browser",
"title": "RustProxy 0.25.1 - Operations",
"title": "RustProxy 0.25.2 - Operations",
"uid": "rustproxy-0170",
"version": 2,
"weekStart": ""
+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.1.zip')
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.25.2.zip')
output = parser.parse_args().output.resolve()
paths = files()
checksum = ROOT / 'SHA256SUMS'
+14 -12
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.1"
assert manifest["package"]["version"] == "0.25.2"
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.1"' in lock
assert 'name = "rust-proxy"\nversion = "0.25.2"' in lock
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
for release_doc, marker in {
"README.md": "# RustProxy 0.25.1",
"CHANGELOG.md": "## 0.25.1",
"ROADMAP.md": "## 0.25.1",
"AGENTS.md": "Current source version: 0.25.1.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.25.1",
"README.md": "# RustProxy 0.25.2",
"CHANGELOG.md": "## 0.25.2",
"ROADMAP.md": "## 0.25.2",
"AGENTS.md": "Current source version: 0.25.2.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.25.2",
}.items():
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
@@ -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.1"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
for marker in ['"version": "0.25.2"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
assert marker in openapi, marker
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker
@@ -470,7 +470,7 @@ for marker in ['"/api/config/{section}"', 'async fn update_section', 'section_up
for route in ['/overview', '/vhosts', '/redirects', '/404-hosts', '/streams', '/acl-profiles', '/configuration', '/tls', '/cache', '/transform', '/logging', '/settings']:
assert f'.route("{route}", get(public::index))' in admin, route
app_js = read_app_js(root)
for marker in ['const PANEL_PATHS', 'history.pushState', 'window.addEventListener("popstate"', 'async function saveSection', 'async function revalidateSession', 'Object.values(PANEL_PATHS), "/configuration"', 'certificateCovers', 'covered_hosts', 'refreshCertificateInventory', 'certificateDetails', 'certificate-domain-pills', 'certificateIssuerName', 'certificateDaysRemaining', 'certificateRenewalStatus']:
for marker in ['const PANEL_PATHS', 'history.pushState', 'window.addEventListener("popstate"', 'async function saveSection', 'async function revalidateSession', 'Object.values(PANEL_PATHS), "/configuration"', 'certificateCovers', 'covered_hosts', 'refreshCertificateInventory', 'certificateDetails', 'certificate-domain-pills', 'certificateIssuerName', 'certificateDaysRemaining', 'certificateRenewalStatus', 'recoverInvalidOrigin', 'auth.invalid_origin', 'ORIGIN_RECOVERY_KEY']:
assert marker in app_js, marker
for marker in ['.certificate-details', '.certificate-domain-pills', '.certificate-card .pill', '.certificate-operation-card', '.renewal-meta', '#panel-tls .certificate-tabs']:
assert marker in css, marker
@@ -521,9 +521,11 @@ for feature_marker in [
assert feature_marker in secret_store_source or feature_marker in db_source, feature_marker
for feature_marker in [
"Argon2", "RPROXYB1", "RPROXYA1", "backup create", "secret-store-key.enc",
"validate_backup_version", "stage",
"validate_backup_version", "stage", "ARCHIVE_VERSION: u32 = 2", "EntryKind::Symlink",
"try_lock_directory", "validate_snapshot_symlinks", "validate_symlink_target",
]:
assert feature_marker in backup_source or feature_marker in main_source, feature_marker
assert "VACUUM INTO" in db_source and "consistent SQLite snapshot" in db_source
for feature_marker in [
"ErrorLogSettings", "ERROR_LOG_QUEUE", "RedactingStderr", "open_error_file",
"ErrorLogLayer", "ERROR",
@@ -1156,7 +1158,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.1"' in openapi
assert '"RuntimeInformation"' in openapi and '"version": "0.25.2"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs")
@@ -1201,5 +1203,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.1 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-v7 migrations.")
print("PASS: 0.25.2 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-v7 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.1'})
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.25.2'})
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.1 release."""
"""Rust-independent documentation/Reader consistency checks for the 0.25.2 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.2_AUDIT.md",
DOCS / "V0.25.1_AUDIT.md",
DOCS / "V0.25.0_AUDIT.md",
DOCS / "TEST_REPORT.md",
@@ -24,7 +25,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.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.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")):
@@ -51,13 +52,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.1")
assert readme.startswith("# RustProxy 0.25.2")
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
assert roadmap.startswith("# RustProxy roadmap") and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
assert roadmap.startswith("# RustProxy 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.1" in changelog[:800]
assert changelog.startswith("# Changelog\n") and "## 0.25.2" in changelog[:800] and "## 0.25.1" in changelog[:1200]
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/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/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.1_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/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.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.1 source tree: missing {path}")
raise ValueError(f"Incomplete RustProxy 0.25.2 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.1 entry point; extract the complete package, not individual files.")
raise ValueError("src/lib.rs is not the 0.25.2 entry point; extract the complete package, not individual files.")
return messages
+6 -6
View File
@@ -122,7 +122,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.1"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert 'version = "0.25.2"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
assert (target / "langs/site.json").is_file()
@@ -133,7 +133,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.1"
assert manifest["to_version"] == "0.25.2"
assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists()
assert not (backup / "legacy-data").exists()
@@ -404,7 +404,7 @@ def ui_contract_smoke() -> None:
assert 'viewBox: "0 0 300 60"' in app and 'M0 15H300 M0 30H300 M0 45H300' in app
assert '.network-interface-row {' in css and 'background: transparent' in css
assert "hostTagEditor" in vhosts and "vhost-chart-line" in vhosts and "vhLifetimeChart" in vhosts
for marker in ["refreshCertificateInventory", "certificateDetails", "certificate-domain-pills", "certificateCoverageUnknown", "async function revalidateSession", 'Object.values(PANEL_PATHS), "/configuration"']:
for marker in ["refreshCertificateInventory", "certificateDetails", "certificate-domain-pills", "certificateCoverageUnknown", "async function revalidateSession", 'Object.values(PANEL_PATHS), "/configuration"', "recoverInvalidOrigin", "auth.invalid_origin", "ORIGIN_RECOVERY_KEY"]:
assert marker in app, marker
for marker in ['data-certificate-tab="installed"', 'data-certificate-tab="renewal"', 'data-certificate-tab="generate"', 'id="certificate-page-installed"', 'id="certificate-page-renewal"', 'id="certificate-page-generate"']:
assert marker in html, marker
@@ -544,7 +544,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.1"' in openapi_0241
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.25.2"' in openapi_0241
assert 'id="panel-logging"' not in html
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
assert marker in html, marker
@@ -988,7 +988,7 @@ 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.1"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
for marker in ['"version": "0.25.2"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker
assert '"software_update"' in openapi and '"update_available"' in openapi
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
@@ -1163,7 +1163,7 @@ def main() -> None:
source_contract_smoke()
container_contract_smoke()
debian_grafana_contract_smoke()
print("PASS: 0.25.1 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.2 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.1"})
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.25.2"})
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.1-" + uuid.uuid4().hex[:12])
backup = target / ".upgrade-backup" / ("before-0.25.2-" + 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.1", "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.2", "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.1. Previous managed source files: {backup}")
print(f"Installed RustProxy 0.25.2. 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.1</b>",
"RustProxy <b>0.25.2</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.1_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()
docs_html = (ROOT / "src/admin/web/docs.html").read_text()
+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.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"))),
("docs/VHOSTS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/VHOSTS.md"))),
("docs/VHOST_ACCESS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/VHOST_ACCESS.md"))),
+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.25.1",
"version": "0.25.2",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
},
"servers": [
+19 -1
View File
@@ -3,6 +3,23 @@ const $ = (id) => document.getElementById(id);
let language = null;
try { language = localStorage.getItem("rp-language"); } catch { }
let csrf = "", authenticated = false, setupRequired = false, sessionInfo = null;
const ORIGIN_RECOVERY_KEY = "rp-origin-recovery-v1";
function recoverInvalidOrigin(error) {
if (error?.code !== "auth.invalid_origin" || typeof location === "undefined") return false;
const marker = `${location.origin}${location.pathname}${location.search}`;
try {
if (sessionStorage.getItem(ORIGIN_RECOVERY_KEY) === marker) return false;
sessionStorage.setItem(ORIGIN_RECOVERY_KEY, marker);
} catch {
if (recoverInvalidOrigin.attempted) return false;
recoverInvalidOrigin.attempted = true;
}
location.reload();
return true;
}
function clearInvalidOriginRecovery() {
try { sessionStorage.removeItem(ORIGIN_RECOVERY_KEY); } catch { recoverInvalidOrigin.attempted = false; }
}
const ADMIN_THEMES = new Set(["dark", "light"]);
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve();
function normalizeAdminTheme(value) { return ADMIN_THEMES.has(value) ? value : "dark"; }
@@ -400,10 +417,11 @@ async function revalidateSession() {
const epoch = authEpoch;
try {
const info = await api("/api/auth/session");
clearInvalidOriginRecovery();
if (epoch !== authEpoch) return;
if (!authenticated) { await activate(info); return; }
sessionInfo = info; csrf = info.csrf_token; renderSession(); await refresh(true); startLive();
} catch (error) { if (error.status !== 401) toast(error.message, true); }
} catch (error) { if (recoverInvalidOrigin(error)) return; if (error.status !== 401) toast(error.message, true); }
}
window.addEventListener("pageshow", event => { if (event.persisted) void revalidateSession(); });
document.addEventListener("visibilitychange", () => { if (document.hidden) stopLive(); else if (authenticated) { void revalidateSession(); } });
+2
View File
@@ -4,12 +4,14 @@
// app.js belongs to the authenticated shell and must not fall back to its
// legacy embedded sign-in form when another startup step fails.
const info = await api("/api/auth/session");
clearInvalidOriginRecovery();
await loadLanguages();
setupRequired = false;
translate();
syncAuthForm();
await activate(info);
} catch (e) {
if (recoverInvalidOrigin(e)) return;
if (e.status === 401) {
location.replace(`${location.pathname}${location.search}`);
return;
+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.1";
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.2";
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
}
+2 -2
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.1"
id="server-value" maxlength="256" placeholder="RustProxy/v0.25.2"
spellcheck="false" />
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
<div class="switch-grid">
@@ -2219,7 +2219,7 @@
</section>
</div>
</section>
<footer class="page-footer"><span>RustProxy <b>0.25.1</b></span><span id="footer-status">API - /
<footer class="page-footer"><span>RustProxy <b>0.25.2</b></span><span id="footer-status">API - /
dropped: 0</span></footer>
</div>
</main>
+9 -4
View File
@@ -43,14 +43,19 @@ pub fn prepare_directory(path: &Path) -> Result<()> {
}
Ok(())
}
pub fn lock_directory(path: &Path) -> Result<File> {
pub fn try_lock_directory(path: &Path) -> Result<Option<File>> {
let file = private_options()
.create(true)
.truncate(false)
.open(path.join("process.lock"))?;
file.try_lock_exclusive()
.context("another RustProxy process already owns this data directory")?;
Ok(file)
match file.try_lock_exclusive() {
Ok(()) => Ok(Some(file)),
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
Err(error) => Err(error).context("could not lock RustProxy data directory"),
}
}
pub fn lock_directory(path: &Path) -> Result<File> {
try_lock_directory(path)?.context("another RustProxy process already owns this data directory")
}
pub fn load_setup_token(dir: &Path) -> Result<String> {
let path = dir.join("setup-token");
+1 -1
View File
@@ -9,7 +9,7 @@ use argon2::{
Algorithm, Argon2, Params, Version,
};
pub use files::{
load_or_create_setup_token, load_setup_token, lock_directory, prepare_database,
load_or_create_setup_token, load_setup_token, lock_directory, prepare_database, try_lock_directory,
prepare_directory,
};
use http::{header::COOKIE, HeaderMap, StatusCode};
+1
View File
@@ -3,6 +3,7 @@
//! logs, sockets and temporary runtime state.
include!("backup/archive.rs");
include!("backup/snapshot.rs");
include!("backup/validation.rs");
#[cfg(test)]
+27 -4
View File
@@ -16,7 +16,7 @@ const BACKUP_MAGIC: &[u8; 8] = b"RPROXYB1";
const ARCHIVE_MAGIC: &[u8; 8] = b"RPROXYA1";
const KEY_MAGIC: &[u8; 4] = b"RPK1";
const FORMAT_VERSION: u32 = 1;
const ARCHIVE_VERSION: u32 = 1;
const ARCHIVE_VERSION: u32 = 2;
const CHUNK_SIZE: usize = 1024 * 1024;
const ARGON_MEMORY_KIB: u32 = 64 * 1024;
const ARGON_ITERATIONS: u32 = 3;
@@ -56,11 +56,29 @@ struct Manifest {
entries: Vec<ManifestEntry>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
enum EntryKind {
File,
Directory,
Symlink,
}
impl Default for EntryKind {
fn default() -> Self {
Self::File
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ManifestEntry {
path: String,
size: u64,
sha256: String,
#[serde(default, skip_serializing_if = "is_file_entry")]
kind: EntryKind,
}
fn is_file_entry(kind: &EntryKind) -> bool {
*kind == EntryKind::File
}
enum EntrySource {
@@ -121,9 +139,14 @@ fn validate_backup_version(version: &str) -> Result<()> {
Ok(())
}
fn harden_tree(path: &Path) -> Result<()> {
fn harden_tree(root: &Path, path: &Path) -> Result<()> {
let metadata = fs::symlink_metadata(path)?;
ensure!(!metadata.file_type().is_symlink(), "restore staging contains a symlink");
if metadata.file_type().is_symlink() {
let resolved = fs::canonicalize(path).context("restored symlink target is missing")?;
let canonical_root = fs::canonicalize(root)?;
ensure!(resolved.starts_with(&canonical_root), "restored symlink escapes the data directory");
return Ok(());
}
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
@@ -132,7 +155,7 @@ fn harden_tree(path: &Path) -> Result<()> {
}
if metadata.is_dir() {
for entry in fs::read_dir(path)? {
harden_tree(&entry?.path())?;
harden_tree(root, &entry?.path())?;
}
}
Ok(())
+244
View File
@@ -0,0 +1,244 @@
fn excluded_file(name: &str) -> bool {
name == "process.lock"
|| name == "config.sqlite3"
|| name == "config.sqlite3-wal"
|| name == "config.sqlite3-shm"
|| name == "master.key"
|| name == ".lock"
|| name.ends_with(".sock")
|| name.ends_with(".pid")
|| name.ends_with(".log")
|| name.ends_with(".jsonl")
}
fn archive_data_path(relative: &Path) -> Result<String> {
let value = relative
.to_str()
.context("backup paths must be valid UTF-8")?
.replace('\\', "/");
ensure!(!value.is_empty(), "empty backup path");
Ok(format!("data/{value}"))
}
fn backup_input(path: &Path) -> Result<File> {
let mut options = OpenOptions::new();
options.read(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
options.custom_flags(libc::O_NOFOLLOW);
}
let file = options.open(path)?;
ensure!(file.metadata()?.is_file(), "backup input must be a regular file");
Ok(file)
}
fn snapshot_metadata_unchanged(before: &std::fs::Metadata, after: &std::fs::Metadata) -> bool {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
return before.dev() == after.dev()
&& before.ino() == after.ino()
&& before.size() == after.size()
&& before.mtime() == after.mtime()
&& before.mtime_nsec() == after.mtime_nsec()
&& before.ctime() == after.ctime()
&& before.ctime_nsec() == after.ctime_nsec();
}
#[cfg(not(unix))]
{
before.len() == after.len() && before.modified().ok() == after.modified().ok()
}
}
fn copy_snapshot_file(source: &Path, target: &Path) -> Result<(u64, String)> {
let mut input = backup_input(source)?;
let before = input.metadata()?;
let mut output = private_file_options().create_new(true).open(target)?;
let mut hasher = Sha256::new();
let mut size = 0u64;
let mut buffer = [0u8; 1024 * 1024];
loop {
let read = input.read(&mut buffer)?;
if read == 0 {
break;
}
output.write_all(&buffer[..read])?;
hasher.update(&buffer[..read]);
size = size.checked_add(read as u64).context("backup input too large")?;
}
output.sync_all()?;
let after = input.metadata()?;
ensure!(
snapshot_metadata_unchanged(&before, &after),
"durable file changed while backup snapshot was being captured: {}",
source.display()
);
Ok((size, format!("{:x}", hasher.finalize())))
}
fn symlink_target_bytes(target: &Path) -> Result<Vec<u8>> {
#[cfg(unix)]
{
use std::os::unix::ffi::OsStrExt;
let bytes = target.as_os_str().as_bytes().to_vec();
ensure!(!bytes.is_empty() && bytes.len() <= 4096, "invalid symlink target");
return Ok(bytes);
}
#[cfg(not(unix))]
{
let bytes = target
.to_str()
.context("symlink target must be valid UTF-8")?
.as_bytes()
.to_vec();
ensure!(!bytes.is_empty() && bytes.len() <= 4096, "invalid symlink target");
Ok(bytes)
}
}
fn symlink_target_path(bytes: &[u8]) -> Result<PathBuf> {
ensure!(!bytes.is_empty() && bytes.len() <= 4096, "invalid symlink target");
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
return Ok(PathBuf::from(std::ffi::OsString::from_vec(bytes.to_vec())));
}
#[cfg(not(unix))]
{
Ok(PathBuf::from(std::str::from_utf8(bytes).context("symlink target is not UTF-8")?))
}
}
fn validate_symlink_target(link_relative: &Path, target: &Path) -> Result<()> {
ensure!(!target.is_absolute(), "backup symlink target must be relative");
let mut depth = link_relative.parent().map_or(0usize, |parent| parent.components().count());
for component in target.components() {
match component {
Component::Normal(_) => depth = depth.checked_add(1).context("symlink target too deep")?,
Component::CurDir => {}
Component::ParentDir => {
ensure!(depth > 0, "backup symlink escapes the data directory");
depth -= 1;
}
Component::RootDir | Component::Prefix(_) => bail!("backup symlink target must be relative"),
}
}
Ok(())
}
fn create_symlink(target: &Path, link: &Path) -> Result<()> {
#[cfg(unix)]
{
std::os::unix::fs::symlink(target, link)?;
return Ok(());
}
#[cfg(not(unix))]
{
let _ = (target, link);
bail!("backup symlink restore is supported only on Unix")
}
}
fn collect_data_files(
root: &Path,
current: &Path,
snapshot_root: &Path,
entries: &mut Vec<ArchiveEntry>,
symlinks: &mut Vec<PathBuf>,
) -> Result<()> {
for item in fs::read_dir(current)? {
let item = item?;
let path = item.path();
let name = item.file_name();
let name_text = name.to_string_lossy();
if excluded_file(&name_text) || excluded_directory(&name_text) || path == root.join("secrets") {
continue;
}
let meta = fs::symlink_metadata(&path)?;
let relative = path.strip_prefix(root)?;
let archive_path = archive_data_path(relative)?;
let snapshot_path = snapshot_root.join(relative);
if meta.is_dir() {
private_dir(&snapshot_path)?;
entries.push(ArchiveEntry {
manifest: ManifestEntry {
path: archive_path,
size: 0,
sha256: hash_bytes(&[]),
kind: EntryKind::Directory,
},
source: EntrySource::Bytes(Vec::new()),
});
collect_data_files(root, &path, snapshot_root, entries, symlinks)?;
} else if meta.file_type().is_symlink() {
let target = fs::read_link(&path)?;
validate_symlink_target(relative, &target)?;
if let Some(parent) = snapshot_path.parent() {
private_dir(parent)?;
}
create_symlink(&target, &snapshot_path)?;
let bytes = symlink_target_bytes(&target)?;
entries.push(ArchiveEntry {
manifest: ManifestEntry {
path: archive_path,
size: bytes.len() as u64,
sha256: hash_bytes(&bytes),
kind: EntryKind::Symlink,
},
source: EntrySource::Bytes(bytes),
});
symlinks.push(snapshot_path);
} else if meta.is_file() {
if let Some(parent) = snapshot_path.parent() {
private_dir(parent)?;
}
let (size, sha256) = copy_snapshot_file(&path, &snapshot_path)?;
entries.push(ArchiveEntry {
manifest: ManifestEntry {
path: archive_path,
size,
sha256,
kind: EntryKind::File,
},
source: EntrySource::File(snapshot_path),
});
} else {
bail!("backup refuses non-file runtime object: {}", path.display());
}
}
Ok(())
}
fn validate_snapshot_symlinks(snapshot_root: &Path, symlinks: &[PathBuf]) -> Result<()> {
let root = fs::canonicalize(snapshot_root)?;
for link in symlinks {
let resolved = fs::canonicalize(link)
.with_context(|| format!("backup symlink target is missing: {}", link.display()))?;
ensure!(resolved.starts_with(&root), "backup symlink escapes the data directory: {}", link.display());
}
Ok(())
}
fn validate_snapshot_config(envelope: &crate::config::Envelope, snapshot_root: &Path) -> Result<()> {
let cert_dir = snapshot_root.join("certs");
for certificate in &envelope.config.tls.certificates {
for name in [&certificate.cert_file, &certificate.key_file] {
crate::tls::safe_name(name)?;
let path = cert_dir.join(name);
let meta = fs::symlink_metadata(&path)
.with_context(|| format!("configured certificate file changed during backup: {name}"))?;
ensure!(meta.is_file() && !meta.file_type().is_symlink(), "configured certificate file is not a regular file: {name}");
}
}
if envelope.config.administration.tls && envelope.config.administration.certificate_id.is_none() {
for name in ["default.pem", "default.key"] {
let path = cert_dir.join(name);
let meta = fs::symlink_metadata(&path)
.with_context(|| format!("default administration certificate changed during backup: {name}"))?;
ensure!(meta.is_file() && !meta.file_type().is_symlink(), "default administration certificate is not a regular file: {name}");
}
}
Ok(())
}
+64
View File
@@ -28,4 +28,68 @@ mod tests {
assert!(validate_backup_version(env!("CARGO_PKG_VERSION")).is_ok());
assert!(validate_backup_version("999.0.0").is_err());
}
#[cfg(unix)]
#[tokio::test]
async fn live_backup_round_trips_internal_certbot_symlink() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().unwrap();
let data = dir.path().join("data");
crate::auth::prepare_directory(&data).unwrap();
let database = data.join("config.sqlite3");
crate::auth::prepare_database(&database).unwrap();
let store = crate::db::Store::open(&database, &crate::config::Config::default())
.await
.unwrap();
crate::secret_store::initialize(&data).unwrap();
store.close().await;
let config = data.join("certs/acme/production-linuxiarz/config");
let archive_dir = config.join("archive/rustproxy");
let live_dir = config.join("live/rustproxy");
fs::create_dir_all(&archive_dir).unwrap();
fs::create_dir_all(&live_dir).unwrap();
fs::write(archive_dir.join("fullchain1.pem"), b"certificate-v1").unwrap();
symlink(
Path::new("../../archive/rustproxy/fullchain1.pem"),
live_dir.join("fullchain.pem"),
)
.unwrap();
let running_lock = crate::auth::lock_directory(&data).unwrap();
let archive = dir.path().join("live-backup.rpb");
create(&data, &archive, "correct horse battery staple")
.await
.unwrap();
drop(running_lock);
let restored = dir.path().join("restored");
restore(&archive, &restored, "correct horse battery staple")
.await
.unwrap();
let restored_link = restored.join("certs/acme/production-linuxiarz/config/live/rustproxy/fullchain.pem");
assert_eq!(
fs::read_link(&restored_link).unwrap(),
PathBuf::from("../../archive/rustproxy/fullchain1.pem")
);
assert_eq!(fs::read(restored_link).unwrap(), b"certificate-v1");
}
#[test]
fn symlink_targets_must_remain_inside_data_directory() {
assert!(validate_symlink_target(
Path::new("certs/acme/profile/config/live/rustproxy/fullchain.pem"),
Path::new("../../archive/rustproxy/fullchain1.pem")
)
.is_ok());
assert!(validate_symlink_target(
Path::new("certs/link.pem"),
Path::new("../../../etc/passwd")
)
.is_err());
assert!(validate_symlink_target(Path::new("certs/link.pem"), Path::new("/etc/passwd"))
.is_err());
}
}
+99 -66
View File
@@ -1,45 +1,3 @@
fn excluded_file(name: &str) -> bool {
name == "process.lock"
|| name == "config.sqlite3"
|| name == "config.sqlite3-wal"
|| name == "config.sqlite3-shm"
|| name == "master.key"
|| name == ".lock"
|| name.ends_with(".sock")
|| name.ends_with(".pid")
|| name.ends_with(".log")
|| name.ends_with(".jsonl")
}
fn collect_data_files(root: &Path, current: &Path, entries: &mut Vec<ArchiveEntry>) -> Result<()> {
for item in fs::read_dir(current)? {
let item = item?;
let path = item.path();
let meta = fs::symlink_metadata(&path)?;
ensure!(!meta.file_type().is_symlink(), "backup refuses symlinks in the data directory: {}", path.display());
let name = item.file_name();
let name_text = name.to_string_lossy();
if meta.is_dir() {
if excluded_directory(&name_text) || (path == root.join("secrets")) {
continue;
}
collect_data_files(root, &path, entries)?;
} else if meta.is_file() {
if excluded_file(&name_text) {
continue;
}
let relative = path.strip_prefix(root)?;
let archive_path = format!("data/{}", relative.to_string_lossy().replace('\\', "/"));
let (size, sha256) = hash_file(&path)?;
entries.push(ArchiveEntry {
manifest: ManifestEntry { path: archive_path, size, sha256 },
source: EntrySource::File(path),
});
}
}
Ok(())
}
fn write_inner_archive(path: &Path, manifest: &Manifest, entries: &[ArchiveEntry]) -> Result<()> {
let mut file = private_file_options().create_new(true).open(path)?;
file.write_all(ARCHIVE_MAGIC)?;
@@ -56,16 +14,16 @@ fn write_inner_archive(path: &Path, manifest: &Manifest, entries: &[ArchiveEntry
file.write_all(&entry.manifest.size.to_be_bytes())?;
match &entry.source {
EntrySource::File(source) => {
let mut input = File::open(source)?;
let mut input = backup_input(source)?;
let mut remaining = entry.manifest.size;
while remaining > 0 {
let take = usize::try_from(remaining.min(buffer.len() as u64))?;
let read = input.read(&mut buffer[..take])?;
ensure!(read > 0, "backup input changed while being read");
ensure!(read > 0, "backup snapshot changed while being archived");
file.write_all(&buffer[..read])?;
remaining -= read as u64;
}
ensure!(input.read(&mut buffer[..1])? == 0, "backup input changed while being read");
ensure!(input.read(&mut buffer[..1])? == 0, "backup snapshot changed while being archived");
}
EntrySource::Bytes(bytes) => file.write_all(bytes)?,
}
@@ -100,6 +58,43 @@ fn safe_relative(path: &str) -> Result<PathBuf> {
Ok(value.to_path_buf())
}
fn validate_manifest_entries(manifest: &Manifest) -> Result<()> {
let mut kinds = std::collections::HashMap::new();
for entry in &manifest.entries {
ensure!(kinds.insert(entry.path.clone(), entry.kind).is_none(), "duplicate backup entry");
if entry.path == "keys/secret-store-key.enc" {
ensure!(entry.kind == EntryKind::File, "invalid Secret Store key entry type");
continue;
}
let relative = entry.path.strip_prefix("data/").context("unsupported backup entry")?;
safe_relative(relative)?;
if manifest.archive_version == 1 {
ensure!(entry.kind == EntryKind::File, "legacy backup contains unsupported entry type");
}
match entry.kind {
EntryKind::Directory => ensure!(entry.size == 0 && entry.sha256 == hash_bytes(&[]), "invalid directory backup entry"),
EntryKind::Symlink => ensure!(entry.size > 0 && entry.size <= 4096, "invalid symlink backup entry"),
EntryKind::File => {}
}
}
for (path, kind) in &kinds {
let Some(relative) = path.strip_prefix("data/") else { continue };
let relative = Path::new(relative);
let mut parent = relative.parent();
while let Some(value) = parent.filter(|value| !value.as_os_str().is_empty()) {
let ancestor = format!("data/{}", value.to_string_lossy().replace('\\', "/"));
if let Some(ancestor_kind) = kinds.get(&ancestor) {
ensure!(*ancestor_kind == EntryKind::Directory, "backup entry has a non-directory ancestor");
}
parent = value.parent();
}
if *kind == EntryKind::Symlink {
ensure!(manifest.archive_version >= 2, "legacy backup cannot contain symlinks");
}
}
Ok(())
}
fn extract_inner_archive(path: &Path, stage: &Path) -> Result<(Manifest, Vec<u8>)> {
let mut file = File::open(path)?;
let mut magic = [0u8; 8];
@@ -110,11 +105,13 @@ fn extract_inner_archive(path: &Path, stage: &Path) -> Result<(Manifest, Vec<u8>
let mut manifest_bytes = vec![0u8; manifest_len];
file.read_exact(&mut manifest_bytes)?;
let manifest: Manifest = serde_json::from_slice(&manifest_bytes).context("invalid backup manifest")?;
ensure!(manifest.archive_version == ARCHIVE_VERSION, "unsupported backup archive version");
ensure!((1..=ARCHIVE_VERSION).contains(&manifest.archive_version), "unsupported backup archive version");
validate_backup_version(&manifest.rustproxy_version)?;
ensure!(manifest.schema_version <= crate::db::SCHEMA_VERSION, "backup database schema is newer than this RustProxy binary");
ensure!(manifest.entries.len() <= 100_000, "backup contains too many files");
validate_manifest_entries(&manifest)?;
let mut wrapped_key = None;
let mut pending_symlinks = Vec::new();
let mut buffer = vec![0u8; 1024 * 1024];
for expected in &manifest.entries {
let path_len = read_u16(&mut file)? as usize;
@@ -133,11 +130,18 @@ fn extract_inner_archive(path: &Path, stage: &Path) -> Result<(Manifest, Vec<u8>
hasher.update(&bytes);
wrapped_key = Some(bytes);
} else {
let relative = archive_path
.strip_prefix("data/")
.ok_or_else(|| anyhow::anyhow!("unsupported backup entry"))?;
let relative = safe_relative(relative)?;
let target = stage.join(relative);
let relative_text = archive_path.strip_prefix("data/").context("unsupported backup entry")?;
let relative = safe_relative(relative_text)?;
let target = stage.join(&relative);
match expected.kind {
EntryKind::Directory => private_dir(&target)?,
EntryKind::Symlink => {
let mut bytes = vec![0u8; size as usize];
file.read_exact(&mut bytes)?;
hasher.update(&bytes);
pending_symlinks.push((relative, bytes));
}
EntryKind::File => {
if let Some(parent) = target.parent() {
private_dir(parent)?;
}
@@ -152,10 +156,21 @@ fn extract_inner_archive(path: &Path, stage: &Path) -> Result<(Manifest, Vec<u8>
}
output.sync_all()?;
}
}
}
ensure!(format!("{:x}", hasher.finalize()) == expected.sha256, "backup entry checksum mismatch");
}
let mut extra = [0u8; 1];
ensure!(file.read(&mut extra)? == 0, "trailing data in backup payload");
for (relative, bytes) in pending_symlinks {
let target = symlink_target_path(&bytes)?;
validate_symlink_target(&relative, &target)?;
let link = stage.join(relative);
if let Some(parent) = link.parent() {
private_dir(parent)?;
}
create_symlink(&target, &link)?;
}
let key = wrapped_key.context("backup is missing Secret Store key material")?;
ensure!(stage.join("config.sqlite3").is_file(), "backup is missing database");
Ok((manifest, key))
@@ -163,47 +178,65 @@ fn extract_inner_archive(path: &Path, stage: &Path) -> Result<(Manifest, Vec<u8>
pub async fn create(data_dir: &Path, output: &Path, password: &str) -> Result<BackupSummary> {
crate::auth::prepare_directory(data_dir)?;
let _lock = crate::auth::lock_directory(data_dir)
.context("stop RustProxy before creating a full backup")?;
let offline_lock = crate::auth::try_lock_directory(data_dir)?;
let database = data_dir.join("config.sqlite3");
ensure!(database.is_file(), "database does not exist at {}", database.display());
crate::auth::prepare_database(&database)?;
let store = crate::db::Store::open(&database, &crate::config::Config::default()).await?;
crate::secret_store::initialize(data_dir)?;
let cert_dir = data_dir.join("certs");
if cert_dir.is_dir() {
if offline_lock.is_some() && cert_dir.is_dir() {
crate::secret_store::migrate_acme_credentials(&store, data_dir, &cert_dir).await?;
}
let parent = parent_or_dot(output);
let data_root = fs::canonicalize(data_dir).context("cannot resolve data directory")?;
let output_parent = fs::canonicalize(parent).context("cannot resolve backup output directory")?;
ensure!(
!output_parent.starts_with(&data_root),
"backup output must be outside the RustProxy data directory"
);
ensure!(!output_parent.starts_with(&data_root), "backup output must be outside the RustProxy data directory");
let temp = create_temp_root(parent, "rustproxy-backup")?;
let snapshot = temp.0.join("config.sqlite3");
store.backup_to(&snapshot).await?;
let envelope = store.load().await?;
let snapshot_root = temp.0.join("data-snapshot");
let mut captured = None;
let mut last_error = None;
for _ in 0..3 {
let _ = fs::remove_file(&snapshot);
let _ = fs::remove_dir_all(&snapshot_root);
private_dir(&snapshot_root)?;
let envelope = store.backup_to(&snapshot).await?;
let mut entries = Vec::new();
let mut symlinks = Vec::new();
let result = collect_data_files(data_dir, data_dir, &snapshot_root, &mut entries, &mut symlinks)
.and_then(|_| validate_snapshot_symlinks(&snapshot_root, &symlinks))
.and_then(|_| validate_snapshot_config(&envelope, &snapshot_root));
match result {
Ok(()) => {
captured = Some((envelope, entries));
break;
}
Err(error) => last_error = Some(error),
}
}
let (envelope, mut entries) = captured.ok_or_else(|| {
last_error.unwrap_or_else(|| anyhow::anyhow!("could not capture a stable live data snapshot"))
})?;
let crypto = new_crypto(password)?;
let master = crate::secret_store::read_master_key_for_backup(data_dir)?;
let key_blob = seal_master_key(master.as_ref(), crypto.key.as_ref())?;
let (db_size, db_hash) = hash_file(&snapshot)?;
let mut entries = vec![ArchiveEntry {
entries.push(ArchiveEntry {
manifest: ManifestEntry {
path: "data/config.sqlite3".into(),
size: db_size,
sha256: db_hash,
kind: EntryKind::File,
},
source: EntrySource::File(snapshot),
}];
collect_data_files(data_dir, data_dir, &mut entries)?;
});
entries.push(ArchiveEntry {
manifest: ManifestEntry {
path: "keys/secret-store-key.enc".into(),
size: key_blob.len() as u64,
sha256: hash_bytes(&key_blob),
kind: EntryKind::File,
},
source: EntrySource::Bytes(key_blob),
});
@@ -222,6 +255,7 @@ pub async fn create(data_dir: &Path, output: &Path, password: &str) -> Result<Ba
write_inner_archive(&plaintext, &manifest, &entries)?;
encrypt_archive(&plaintext, output, &crypto)?;
store.close().await;
drop(offline_lock);
Ok(BackupSummary {
path: output.to_path_buf(),
files: manifest.entries.len(),
@@ -248,8 +282,7 @@ pub async fn restore(archive: &Path, data_dir: &Path, password: &str) -> Result<
let (manifest, wrapped_key) = extract_inner_archive(&plaintext, &stage)?;
let master = open_master_key(&wrapped_key, backup_key.as_ref())?;
crate::secret_store::write_restored_master_key(&stage, master.as_ref())?;
harden_tree(&stage)?;
harden_tree(&stage, &stage)?;
crate::auth::prepare_directory(&stage)?;
let stage_db = stage.join("config.sqlite3");
crate::auth::prepare_database(&stage_db)?;
+17 -3
View File
@@ -104,7 +104,7 @@ impl Store {
tx.commit().await?;
Ok(Self { pool })
}
pub async fn backup_to(&self, path: &Path) -> Result<()> {
pub async fn backup_to(&self, path: &Path) -> Result<Envelope> {
ensure!(!path.exists(), "backup database destination already exists");
let path_text = path
.to_str()
@@ -130,9 +130,23 @@ impl Store {
let result: String = sqlx::query_scalar("PRAGMA quick_check")
.fetch_one(&check_pool)
.await?;
check_pool.close().await;
ensure!(result == "ok", "SQLite snapshot integrity check failed");
Ok(())
let row = sqlx::query("SELECT revision, document FROM configuration WHERE id = 1")
.fetch_one(&check_pool)
.await?;
let revision: i64 = row.try_get("revision")?;
let document: String = row.try_get("document")?;
ensure!(
document.len() <= crate::config::MAX_CONFIG_BYTES,
"stored configuration exceeds size limit"
);
let envelope = Envelope {
revision: revision.try_into()?,
config: serde_json::from_str(&document)
.context("invalid stored configuration in SQLite snapshot")?,
};
check_pool.close().await;
Ok(envelope)
}
pub async fn load(&self) -> Result<Envelope> {
+2 -2
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.1"
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.25.2"
)]
struct Cli {
#[arg(
@@ -135,7 +135,7 @@ enum ConfigCommand {
}
#[derive(Subcommand)]
enum BackupCommand {
/// Create an encrypted full backup. Stop the RustProxy service first.
/// Create an encrypted full backup. Safe while RustProxy is serving.
Create {
/// Destination .rpb file. Existing files are never overwritten.
#[arg(long)]
+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.1 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
println!("RustProxy live logs 0.25.2 - 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();
+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.1".into());
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.25.2".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.1</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.2</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.1</b>"));
assert!(body.contains("RustProxy <b>0.25.2</b>"));
}
#[tokio::test]