v0.25.2
This commit is contained in:
1 parent
59ddabb72b
commit
174389e5b1
48 files changed
+676
-200
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
|
||||
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
|
||||
compatibility. Current source version: 0.25.1.
|
||||
compatibility. Current source version: 0.25.2.
|
||||
|
||||
## Delivery
|
||||
|
||||
|
||||
@@ -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
@@ -1688,7 +1688,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.25.1"
|
||||
version = "0.25.2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
Vendored
+1
-1
@@ -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`
|
||||
|
||||
Vendored
+1
-1
@@ -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"
|
||||
Vendored
+8
@@ -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.
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
package main
|
||||
|
||||
const version = "0.25.1"
|
||||
const version = "0.25.2"
|
||||
|
||||
type config struct {
|
||||
Listen string
|
||||
|
||||
+5
-2
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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.")
|
||||
@@ -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.')
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"))),
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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(); } });
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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};
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)?;
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user