This commit is contained in:
Mateusz Gruszczyński committed 2026-10-08 10:36:34 +02:00
1 parent 3b3f0db377
commit d940f9995f
47 files changed
+542 -187

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.28.0.
compatibility. Current source version: 0.28.1.
## Delivery
+9
View File
@@ -4,6 +4,15 @@
> test-only changes and packaging implementation notes were moved to
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.28.1
- Add manual certificate support for absolute external certificate/key paths, including `/etc/letsencrypt/live/...` pairs. Certbot-style symlinks are resolved again on every reload so an external renewal can be hot-published without copying files into RustProxy storage.
- Keep `POST /api/certificates/{certificate_id}/reload` revision-stable for Certbot/acme.sh wrapper hooks and reject use of the deploy endpoint against an ID configured with external file paths.
- Preserve the legacy/simple filename mode for `<data-dir>/certs`, reject mixed relative/absolute pairs, and leave upload/deploy request bodies path-free.
- Exclude externally referenced PEM material from RustProxy backups; only their configured absolute paths are retained, so the external files/mounts must be restored separately.
- Clarify certificate source/storage behavior in English and Polish across ACME, Import and Existing-files UI modes.
- Keep SQLite schema 8 and COLD format 3 unchanged.
## 0.28.0
- Add a manual GeoIP **Download now** action and replace the fragile release/checksum-base coupling with direct source URLs. CSV mode accepts separate IPv4/IPv6 files; MMDB mode accepts one native MaxMind DB for both families. Publisher SHA-256 URLs are optional per file; size/format validation, staging, the updater lock, atomic publication and hot reload remain mandatory.
Generated
+1 -1
View File
@@ -1688,7 +1688,7 @@ dependencies = [
[[package]]
name = "rust-proxy"
version = "0.28.0"
version = "0.28.1"
dependencies = [
"anyhow",
"arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "rust-proxy"
version = "0.28.0"
version = "0.28.1"
edition = "2021"
rust-version = "1.88"
description = "RustProxy - independent reverse proxy with local administration"
+14 -15
View File
@@ -1,9 +1,9 @@
# RustProxy 0.28.0
# RustProxy 0.28.1
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.28.0 release source.**
**0.28.1 release source.**
## Layer-4 streams
@@ -11,18 +11,17 @@ 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.28.0
## What changed in 0.28.1
- Add an authenticated **Download now** action for GeoIP and move managed sources to direct per-file URLs. CSV mode uses separate IPv4/IPv6 files; MMDB mode accepts one native MaxMind DB for both families. Publisher SHA-256 URLs are optional per file, while size/format validation, staging, atomic publication and hot reload remain mandatory. Country ACLs, country rate limits, access-log country capture, Prometheus status and administration status use the new database without restarting proxy traffic.
- Make active listener state easier to scan in Settings: each active HTTP/HTTPS bind is rendered as a separate endpoint card with explicit active protocol badges (`H1`, `H2`, `H2C`, `H3/UDP`) derived from the running listener configuration.
- Add a configurable cache response `Via` value, enabled by default as `Via: RustProxy` and disableable from Cache settings. Existing upstream `Via` values are preserved and RustProxy appends its own value.
- Stop full cache purges for request-bypass edits and response-only cache diagnostic/Via header changes. Settings that can change stored representations or compatibility still rotate the cache revision and invalidate incompatible objects.
- Fix the vhost Security request/response header-rule layout so the Delete control is separated from the first Operation selector.
- Report the completed ACME Python/Certbot runtime check through the normal INFO log instead of printing a speculative `checking/preparing` line in the startup banner.
- Retain the 0.27.3 maintenance fixes for restore ownership, production/development data directories, automatic GeoIP updates, explicit listener override semantics, strict WebSocket 101 framing, fail-soft release checks and scaled sidebar layout.
- Keep SQLite schema 8 and COLD on-disk format 3 unchanged; the new `cache.via_header` field is defaulted for older saved configurations.
- Add an explicit **External files (absolute paths)** certificate mode for operator-managed PEM pairs such as `/etc/letsencrypt/live/example.com/fullchain.pem` and `privkey.pem`. RustProxy resolves Certbot-style `live/` symlinks on every reload, validates the resulting pair and publishes it atomically for new TLS/H2/H3 handshakes without copying the files into `<data-dir>/certs`.
- Keep the existing `POST /api/certificates/{certificate_id}/reload` contract for renewal wrappers. A Certbot/acme.sh hook may renew files in place and then call the targeted reload endpoint; the saved configuration revision is unchanged.
- Preserve the safer managed-file mode for simple filenames inside `<data-dir>/certs`, reject mixed managed/external pairs, and keep upload/deploy endpoints from accepting arbitrary host filesystem paths in their request bodies.
- Clarify all certificate-source UI text in English and Polish: ACME tabs state that RustProxy/Certbot owns the generated files, Import explains that browser-selected PEMs are copied into RustProxy storage, and Existing files explains the managed-directory versus external-path modes.
- Exclude externally referenced certificate material from RustProxy backups. The absolute paths remain in configuration and must exist or be mounted separately after restore.
- Retain all 0.28.0 GeoIP, listener protocol-badge, cache Via/invalidation, Security-editor and ACME runtime-reporting improvements.
- Keep SQLite schema 8 and COLD on-disk format 3 unchanged.
0.28.0 retains the 0.27.3 HTTP/3 cache and maintenance work plus earlier administration/proxy behavior. Historical details are in [CHANGELOG.md](CHANGELOG.md).
0.28.1 is configuration-compatible with 0.28.0; no database migration is required. Historical details are in [CHANGELOG.md](CHANGELOG.md).
### Historical 0.26.7 release notes
@@ -90,7 +89,7 @@ Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost
- Replace backend sort text (`A-Z` / `Z-A`) with direction arrows (`↑` / `↓`).
- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.1 requires no data migration.
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.28.0 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.28.1 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
## Documentation
@@ -181,7 +180,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \
Package-build metadata remains outside this source tree and is maintained with the official package repositories above.
The 0.28.0 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
The 0.28.1 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
@@ -437,7 +436,7 @@ python3 tools/checks/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.28.0.zip
python3 make_zip.py /tmp/RustProxy-0.28.1.zip
```
## New management and test-origin guide
+12
View File
@@ -1,5 +1,17 @@
# RustProxy roadmap
## 0.28.1 - External certificate paths and certificate-source clarity
Current direction: 0.28.0 -> 0.28.1
- [x] Allow manual certificate entries to reference an absolute external certificate path and absolute external private-key path.
- [x] Resolve Certbot-style `live/` symlinks on every reload while preserving bounded PEM parsing, regular-file checks and private-key permission checks.
- [x] Keep targeted certificate reload revision-stable so renewal wrappers can call `POST /api/certificates/{certificate_id}/reload` after renewing external files.
- [x] Preserve managed `<data-dir>/certs` filenames and reject mixed managed/external pairs.
- [x] Exclude external certificate material from RustProxy backups while retaining the configured absolute paths.
- [x] Clarify certificate-source, storage and path expectations in English/Polish UI text and operator documentation.
- [x] Keep SQLite schema 8 and COLD format 3 unchanged.
## 0.28.0 - GeoIP control, listener clarity and cache policy refinement
Current direction: 0.27.3 -> 0.28.0
+46 -45
View File
@@ -1,18 +1,18 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
b934978c1c6999e0a5c0ccb443ef2b7257327714c50d2065552ee78a9425a95f AGENTS.md
46621196e80c93b9f116d1f323b14906d6d2d05e2c51190b66b552f78e64718f CHANGELOG.md
09de70df5818af3f6ac155374300737ab3bd33709d0c4eec488a957bc6e910e2 Cargo.lock
c5b85b95201b85562014a0715384c68be56398f1d11b0eac28a431c812991cc8 Cargo.toml
a3c26b50db33b62a2da3741f278ff9e4d0c64026850daee61db2dd921db763b2 AGENTS.md
b2c08597dd9c6e77d06f130a13291baaf278779d0f1120b702b75368b504fc70 CHANGELOG.md
821290c7a251e9e9962d264306340e85ac81edbb060b9416d278ffcbabaa9d0b Cargo.lock
ca10fd79662dbdd719d08c6fbea4d01f3e6cb4cb2943d466cc8756ff513d3290 Cargo.toml
75b008f237b2226c62a7721b7901018ce83bfc2726f047a83a17c66fcb799f3b Dockerfile
2914d3a970d9cb952aab78e9113792716681eeba179be1646c793422219974c8 README.md
623fc7e391037cbb5def4850280a4924357df00c3ad96709d94e1e77b851af30 ROADMAP.md
8ce8f96cb98b2c59bc6f02479ec0fc53c71be3ee2e0d1db104e84f10870543ec README.md
e0453e362a48a16961cea02e8e4c280392d4a43c09a31d5568b60bc27813e913 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md
31d4e0cd454060b46f359fdb84a14b16a5fe0d3baabb387dd584d76e555558bb deploy/rustproxy.service
4f6abab66d07588855c3885ace57b6494ccbfd799fae959cf8d95230d71e25c0 dev.sh
c5e254880fb348b71e39d683652fc2d7777e2444d2c0b1c51c43b7820853582e dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -21,25 +21,25 @@ bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/d
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go
5a827e94b9295a98b7554252e080903fa7e2ea723eff9f0e3cbd79a355fb76e5 docker-agent/model.go
f9dfa0f572a5a5f6b6fc3c99eeca6b577dbf63a41d3d656075e13c11dbb8c2e0 docker-agent/model.go
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
875faa3969df9409d79f050a84b4183496f557edda3d34aaec19fe556cf8c471 docs/ACME_PROVIDERS.md
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
e51a93b5d09d89878f64119cf7df8ae20773ae754c4fde023b5b79690f776cd9 docs/API.md
1d59f24bf5e9c36591e6a54dbb8909cfea045fe5fd7f11f1e54c50f9f01078ba docs/API_ACCESS.md
cca5747a345cc1c238564213a5b1ba3167994e81e603fba0668f70e247523852 docs/API.md
824aeb96e1678b3c84a9f8217bedea6f3a38b60175073054a88f752415843d7c docs/API_ACCESS.md
4c5288c9ed427cfb919bd023ed9a321dcf6d64bc4cad3e1768393356c5d835ba docs/ARCHITECTURE.md
3f18d48514ad003ac24ad07ea47271a5719dd8bc1f2329f0c3fbae774e086426 docs/CACHE.md
6266163e17d2921468abdae92c1620266af8351321530e2c9b7dca6f3dd89c88 docs/CACHE_DIAGNOSTICS.md
c6003dcbc0b95e2cfa9523b7017186cc8b4826248b588af77111ac5eb15174f9 docs/CHANGELOG_TECHNICAL_ARCHIVE.md
106eb9347b339546bcca703c64973cfc586a43dda806281c5bb894bc5cec5ca0 docs/CONTAINER.md
17e42b2924788960f1d97407619ab4cbc27dc5dc0e45b50d2b6e7c4629e8626a docs/CONTAINER.md
9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md
16d8f59908c66e595d6003495cbf5c068c68b10aa9495eb8612c38b2de0e4a7c docs/DOCKER_DISCOVERY.md
9ea8db4d034ecb573976dd323da29f044538525c230df9f43a47be1ac867b24c docs/ERROR_LOGGING.md
1da868c5e965ab0f99f3b5c58851bfe3be96533bee5774f6e40318dcad7092e8 docs/FILE_LOGS.md
bf127609d35246e95b5183411795cd7f3112164af13b97044c3dd498ab3bd68f docs/FILE_LOGS.md
8dfc89a8b3512103b87864bd7c24a3dc3de4465edf8f9874e6bed1f4634dd930 docs/HTTP3.md
80e0d15afe4c8f68b0d6ff9c4047626d42201e62874996947fdee38447b6ad0a docs/INSTALL.md
9e728d982d4ee709b95959ed426c27fe1c25ee7c7aec7a39742404120cb0d715 docs/INSTALL.md
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
@@ -50,20 +50,20 @@ e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND
89a1af6c9fe10c572b211f9632ce256166e67990b5dacacf2ae07265d99a5172 docs/NPM_MIGRATION.md
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
71dba13d40df2199c2e6ae432681c73b6e72e5715ce074e3926ef9034a512932 docs/README.md
9a08c40b2b22fffca6ed9a4f32106cefc4f55c8c954c9380775c9136fa655419 docs/README.md
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
be661dbf81af840d4d8aeeb5347c24c7faecdecc73a8cf0f6f09053f1abe04c1 docs/RENEWAL.md
1ce39b327026b86d77e81ea15f9376f1abd83150669503f8275a171288247bf0 docs/RENEWAL.md
a2bb837cdd3559d58158880c1f832badf64035376a3623050bb4e40c2bca104c docs/ROADMAP_HISTORY.md
2b760c88e0e093d7705820560fbacf30bfc7bf6d67f2e68a500b3d4d970d80b6 docs/SECRET_STORE_BACKUP.md
759bf2987f2b602488829d1856fa33d57c2a3b23bc681ef5a931b9633af3a533 docs/SECRET_STORE_BACKUP.md
5dbdf45fddba198a28f511dbbd2f12bf7d2b88c41c3cbedbc20dfea3858779d3 docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
63c776e2531c3e58d067caa921fbee79d0c9dd8c26a5e91a7ec10af088ed9be6 docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
1a89213f3863b5bf140b9fa48f7ea33ea497da8a89caaf71e4314ebff32fed6f docs/TEST_REPORT.md
0fb8555ecc7c2b3692c690768934f8b571fab7b83f58f8927a604a4e920443f2 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md
6564b378dac5dd03c49f20eb1a267d88a2c41d922d517aff96fa12cf9d937871 docs/TRANSPORT.md
96a6816915d526f00e261846131ee3f942ef332af996cf0f73c79ec1ba9f9c7a docs/V0.21.0_AUDIT.md
0c1c3ba6e8724d85ec54e254e70bbf05e063db025280ef4af866b774fb43f76b docs/V0.22.0_AUDIT.md
3f811c457bf6be662969fbac32c3cfcc49b13c2f00d0125e4f2eeab768f59f0e docs/V0.22.1_AUDIT.md
@@ -100,6 +100,7 @@ efd5935852d4cfd09758bf67c70e437490cfce2baade46c8733201e506dbcc42 docs/V0.27.0_I
845cc7b7fcd553ef99613817e70fcecc4e50dd8fa77066a75577d63a7224cdec docs/V0.27.2_IMPLEMENTATION.md
49fca8928f3ed6169df4480f9658b65b8d9dd5d1331ef5bb2b1f9adaca8212eb docs/V0.27.3_IMPLEMENTATION.md
713518c1a09969703d7b09b83380d5e659f5d1587ef23585abfa1fa87c033ae5 docs/V0.28.0_IMPLEMENTATION.md
8e1b45415a2bffcf26fe113bbc25627daf5d8758eb9e44c38abb060a4a402a46 docs/V0.28.1_IMPLEMENTATION.md
bc2ac0117528ed4616c68cf7fe05cb1d9bbd36ecaa8573828cb3b3136923f265 docs/VHOSTS.md
09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md
4026a4adeee70f98a96cc7aa8d8be16cda42ffb2e5890d38f1ba638134ab0675 docs/WEB_SERVER.md
@@ -114,8 +115,8 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
62ca0f17538ae6f7b38aacf1cf9cffe765a7fc51579f17637f9268b378aaf7d1 grafana/README.md
3c565201675dba48ea7f2504fafa5e74017519154404b1ae944a9fbd51f7794c grafana/rustproxy-overview.json
dacefab2e3d97113dac1065db3ca10476a2760ee38d181b01913e9cadc65d2a4 grafana/README.md
a63837ec7742fed482d7f9738f22bdad2b340d8730f6f27022eda46646a0d068 grafana/rustproxy-overview.json
a51d911526d2631282a358be4152ef1ad6ed4034e34f961a5290b1b90fffaf32 install-debian13.sh
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
@@ -128,9 +129,9 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md
c54cc75de6855c7932004de5d770badab43c99cf8ddcbc867a5bf6d3557bdd1d langs/en_US.json
008553b6897ecd614d4908063cb864284ee7090765fbc96bcf332891aaaf9de1 langs/pl_pl.json
f80544d7db2a69a4866c72e1523920587c4295af77ffcad49f8c0af404bd966b make_zip.py
3847e8eca1de8c6e7f5d51d8eb7ef1f47221822e3f501da08baa050d5e98dce6 langs/en_US.json
986da8af322bea5ad4da6373fa246b97e6dc7e1f9f058f06bbc652a98a89fe80 langs/pl_pl.json
c3e837100baa919445387bae65a579ea7dd05a315554ba48b3361131104f6168 make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
@@ -146,11 +147,11 @@ ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_r
6e036f736a2b40feb78eec17bf34871771524190f283591d1c89e48e6d2271e6 nagios/check_rustproxy_queues
7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml
2145ad17ae7a823756371ceab6bdbeb332211df93193ffb4e486f7a80c4f29ae scripts/README.md
9e71cb39113e034a9e8bce90d3a2cfee9502c3b1bb17772b2896c6b3e6d0b96a scripts/build/swagger_assets.py
1ffd80b4e699ecc4daf30eeec1884bcc83ae506e8abd8833d336e170261e7638 scripts/build/swagger_assets.py
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy/deploy.py
a9b3e34db18f623f5d7a6c98f721ff38d8b1273bdb7e3bbea091440088638493 scripts/deploy/deploy_core.py
9e24d65db00e1f89166a2aad95b3989f949cec6041e92de6f1aa63a2c0125ad3 scripts/deploy/deploy_core.py
2eec7ff7c689416135984e38a90c177a8b06bdf176da69294cce25e5571847d5 scripts/deploy/deploy_runtime.py
d5fca6ac46b6760f253f35210134d26f68e6076c28c7f1a93123158add83c40a scripts/deploy/upgrade.py
390e8d14a338bbdc84cea738a8e7722cbe0e21e1840c7f131a40c2f263ab5d59 scripts/deploy/upgrade.py
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
@@ -167,15 +168,15 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi
c8941a929dfef9e8875adf534394ca01a1532e567d5099fefe3657826886ee25 src/acme/runtime_env.rs
7acbaf34e7b953b753ab074893e47b5024f0cd756dc4c7a1074067dd936c2e8e src/acme/tests.rs
55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs
d18efa3f797f62b3916265e30ba546725a41e7ccfd9b123965ff9f522e3baa88 src/admin/certificates.rs
123f3ae5d19cdb5e9a478f634a8b04df067f944f281c180bb1e8d735955162b3 src/admin/certificates.rs
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
cba6baa861709a77983586da493d0762e2892ea5614f187fd6c429bc735b4cf4 src/admin/documentation/documents.rs
9440c8a6cae7fce2e88ab0a8f1e6c804890add140760612cc25ead01443e8cc2 src/admin/documentation/documents.rs
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs
d3210e037481e1f95e840550428e088ffd83ec9a42f1810e4c976298fb555a9a src/admin/file_logs.rs
de331ab75c31df65a4f0f1607dbfb26972799bff5d9f232d8e914c38225cffc7 src/admin/logging_api.rs
58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs
490627376801eda6341bf8c28a646bcb77460bbd39de6d7934576b6a7391df43 src/admin/openapi.json
74a15448f57c52972343645521e4e4238d8d37dc2b392e5db34f26c2d46b6b83 src/admin/openapi.json
6b14f2a884e41745ad6f99d5ff4d36f502a3c0b5ba09ba15802eddabccc46177 src/admin/operations.rs
5287fff6b3ad436ac695e9b66feb1b0b5705b744a9f00cec7ea6a3e951e5eebf src/admin/preferences.rs
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
@@ -195,7 +196,7 @@ f869e4da5de149fc04ce1aab658befeaf3208c862f149ca976235d15832c3cdb src/admin/test
446011c2c7afddf7734ceb268c75a3b4146dbe8e915b11986495e956b6c62c69 src/admin/web/app/config-history.js
ed063b8a414cf3b5e539c900e8722aa82d84c561a5cd54d875650793a25529e2 src/admin/web/app/core.js
cb5c4c2dd4964795b61393b05537cc1528c72d7c1390953f4d119a5e55c65c7c src/admin/web/app/preferences.js
b93b5117ff0977df2f5ae783ba7f08b6b35e3ce6103cd0a38a4a38047ce26485 src/admin/web/app/runtime.js
36b6d6c00fae30acdd26279cb88ab343c4926acd1f713500eb9db76aa0a1ecea src/admin/web/app/runtime.js
3b63cc15068d287fe873956b38306abaae8c88a2f288ecd98159c5c47acb98e1 src/admin/web/app/settings.js
74777adaba5a448f3a9abf3a46d91efc6eb81ff6f3b5441e5b99a9e1d6c09af0 src/admin/web/cache-panel.js
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
@@ -213,7 +214,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
ba9e0c1c874ace33d55287b1ba36935cc8fc49dae3eb5673e82b5c7f8fc05e12 src/admin/web/index.html
05e89230f3d15436e20331ac327f112d70c53e8cdad0b7c2dc42f6469b4bcdfd src/admin/web/index.html
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
af0fbd94d9b88ce1cce28e39dfd2c35de031adcba412dd4a9a17d9b416c256dd src/admin/web/login.html
@@ -237,7 +238,7 @@ c96a87e82ff0c1c9ce3d0cd5135903bb53af78d1c4fb6ebfe5fea503bf5644ea src/auth/store
09c3a622a7d2169e49e3369ea5f0f816eab2f717eaf1675c625d518c8f34d367 src/backup.rs
ebce56a154c57bf20d6618de161359c7c49392d7d24e8d23428b7d78a7a060c1 src/backup/archive.rs
732cd4f9ed38ae69f8bbae61b1db8baaa6f86b07ca21c41f24a206a30f0a96fc src/backup/ownership.rs
a4ed02528513a5c991463c368a2ce4c0e2dcad023d106900a37b560cefc8084a src/backup/snapshot.rs
076350506215faec3cfe7d9de4e60baf29672cc22a41eabe601dcc510c257799 src/backup/snapshot.rs
a00571b409d305af86ab2a57a25434b5ab58706772df440d017fc1fec097f7b4 src/backup/tests.rs
b2ce83a48736c082e6b820e4e363cca7a5018f3ae2a1ce5adfd6af1a03d62598 src/backup/validation.rs
426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs
@@ -339,11 +340,11 @@ a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/fo
17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
2f6b5a9517453541bf43580a9d963e64e36338575ee9ad295ca3718803a59ed6 src/main/admin_runtime.rs
c8972e8b2ba3d8ab91678ad990dfb7da39f901d81c74887a3910a4baec5194d6 src/main/bootstrap.rs
4ab51b81c126409a8cd33925958165a366d28f91cecdb42df348d8dd5a06e333 src/main/bootstrap.rs
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
c97330d33287e3ce8901b58117b21c0a66f92446df243ec39eae5fe1affa0a4f src/main/tasks.rs
085b1e39c4376a691ce254e1071ebfe6ac68ffc7965cac6aee375035eedf3dd8 src/main/tasks.rs
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs
@@ -415,10 +416,10 @@ d9123c623e2a97d09e41edaa29993b4b2c8cba39a3abdf2b48a205aa9a9c2939 src/system_sta
0e1d87554db55e724f30c41ec4a5c80bdb8fc9c8cba8700ac509864d4abf8871 src/tinyauth/tests.rs
7d94e9ea622abb2ffee9f2bba43b3d41e603d3e907a42f0fe2a83b0c78faa7b1 src/tls.rs
e62b55a057ab39daf540e225342e98a57a6266f1603d685c48468f2f7c37c4b8 src/tls/certificates.rs
b094b69eed8fa37fbd00f4cc0ff32a0d72c39c0423e98a352ba7046fc66fc91f src/tls/server_config.rs
4b546ac47c142286e27c729f24df23810e38fc33d02c3b90837663041e55630c src/tls/tests.rs
3e6947dde167f82a5efc407ccf9bd4c9f5b26cc8633d1418a10284bc1a1503d4 src/tls/server_config.rs
05ae78a3c495bfd97889a1f5499b48d68cc6c42b9bb5e4ee6577d47bdda3a54d src/tls/tests.rs
e0ab65406911d4b95cb26594df65779a88d9011d5b11adb22c1a28831cda3826 src/tls/tests/acme.rs
9196ec11be2f0107bc86c71e302a10868801709c979d07aa9a1119296803c932 src/tls/types.rs
601ee1d8e2bdd9d55f99041c86897699b77d6fbf7a936ed1b580029d490d75bf src/tls/types.rs
438506b1743daddcbefe0ebec5a6050d5f9555710446a1578c0a3d326d4b68ac src/traffic.rs
36301bcbd20b557dd3d58e77a9175f3581947f577c0bb0b972899cec6786b03f src/traffic/tests.rs
d88c92f5a2a7773bc3e715d24039db35da4df53881f1a0859eec819b96f06d3b src/transform/apply.rs
@@ -450,7 +451,7 @@ ff4980f0a091da9ff1722d6b9f6662529cbd7c5e2ac68dcd22a507ce6aaf3011 src/vhost/vali
c1fef6dfb79275479c15097b211cd5f063ad7e0852a0767b0145cf4f32f751da src/web_server/config/model.rs
f1d3e427c53f8b5be5768d85e69ead746d83647fcd3b355acc98e3f9d5e217e0 src/web_server/config/validation.rs
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
82c3f7ba6c5a610d0819002a9c3c68fc0e00c69b38832cdfac7536a747288863 src/web_server/fastcgi/protocol.rs
3a7414dd4fd65b3ef1c3553c6db72ec79683cf54f2fdaac9149006d6a0faad8b 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
@@ -460,14 +461,14 @@ f1d3e427c53f8b5be5768d85e69ead746d83647fcd3b355acc98e3f9d5e217e0 src/web_server
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
35c03934f0e4e9e59d5639f69d488d8ba9283eeff8ac01d798a168aa40ec2ce2 src/web_server/response/file.rs
02c6353294707b5b352b3802b9f456ba01a1d6f16b247428e5787c8239b2d5c3 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
9885d5c91066668e4f59fe28498dc4412ee09920a274fbdaf4d3c9c7b93e41fe src/web_server/tests/part_02.rs
95dbefdfee00d8db187fcc5abcc01afec9a9fb8ac4ceb452dc19bc2107b1683c src/web_server/tests/part_02.rs
e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
@@ -476,19 +477,19 @@ eecf680f001f2110dde4eceaccf2f922859a16c946a644f8854e8de4d3771f7c tests/policies
728999110a6d8cacb62bc0399bc333fa23a66526ee23f563a3c93bf00e5fad19 tests/transport_config.rs
b04362ad082a32c9ccc4cf38c46f44d71347adecf7aba3224b3af3ea4872e3d0 tools/README.md
358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py
2a90edbfe9f7d8764a73205a8726c8c8ac904590ebf0d11ed782c7a69b395797 tools/checks/check_source.py
bdeb2977fcff8950cc224264739c49d6febe4327a35b384ad14cabd9c9133726 tools/checks/docs_check.py
e5a533eda85138fd13388fb17579dc7dc27ebfca9a350cc73e10995c7514a848 tools/checks/check_source.py
9d2c29b1b3636b127c6f2b05df00926b19a2ab83b9a8a01e497ed4cf0690751b tools/checks/docs_check.py
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
cf762780c4085791c72c3c5f93054d7f2a1107403bee0616a1f4113bbcbb99f9 tools/checks/memory_contracts.py
27eab52ae463df0b06468e1b7fb2372f3135867869421078fb9d7beb4e717b92 tools/checks/preflight.py
0874a5faba6beb1c21c8afc0c1375322518a662d8709ddb0704840f5ed95f6a2 tools/checks/release_smoke.py
dfcc74597bbaa1853608db5d62908f5832a48453be40fbdaeebce68d041461f1 tools/checks/preflight.py
b1ac96ebdbea050e20c762eaac3199611f1bbb768e0e9fb7e2fe77414f17c9d0 tools/checks/release_smoke.py
f69845bc26ec33160fc756953991cf32aa8ad6a7fb4a131caa2093db91e783ef tools/checks/rust_module_contracts.py
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py
adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py
23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py
2309c41e769e797c838d4f40d50bb3c6b9ad9ad0f9ed07fb93e07f95d66563f9 tools/checks/source_module_contracts.py
0aaa80453e25aae7ea953fadb91b3ae3eaff627977c81c9ab5a2779f9a93582f tools/checks/web_assets.py
54d6d339071743d4098ed5a6aba7fccd142d57f224b15431c4373ea35949fceb tools/checks/web_server_contracts.py
b3299d86d94fac19e0e33462c65547c7353e4276fa619145c6e12420d48f2b46 tools/checks/web_server_contracts.py
c245f8f832bd2c36a3fbd5e66fe98e7367c70a8db840c79e6b640f1d9d74f2ac tools/checks/web_service_ui_test.mjs
d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py
+2 -2
View File
@@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi
case "$mode" in
help|-h|--help)
cat <<'HELP'
RustProxy 0.28.0
RustProxy 0.28.1
./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.28.0...\n' >&2
printf 'Building RustProxy 0.28.1...\n' >&2
cargo build --locked
BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
case "$mode" in
+1 -1
View File
@@ -1,6 +1,6 @@
package main
const version = "0.28.0"
const version = "0.28.1"
type config struct {
Listen string
+15 -3
View File
@@ -389,7 +389,7 @@ previous snapshot stays active. The response contains `reloaded`, `revision`, `p
handshakes use the refreshed certificates; established connections are not renegotiated.
Administration TLS is startup-fixed and therefore reports when a process restart is needed.
`POST /api/certificates/{certificate_id}/reload` is the targeted variant for a manual/file-backed certificate (`issuer == null`). It validates the requested configured ID and atomically republishes the immutable TLS snapshot at the same configuration revision. It is intended for external renewers that replace the files already referenced by RustProxy. Application-managed ACME certificate IDs are rejected with `409` because their renewal pipeline owns those files. The endpoint accepts either the normal administrator session + CSRF flow or a scoped Certificate API token.
`POST /api/certificates/{certificate_id}/reload` is the targeted variant for a manual/file-backed certificate (`issuer == null`). It validates the requested configured ID and atomically republishes the immutable TLS snapshot at the same configuration revision. It is intended for external renewers that replace the files already referenced by RustProxy. A manual entry may use simple filenames from `<data-dir>/certs` or two absolute external paths such as `/etc/letsencrypt/live/example/fullchain.pem` and `/etc/letsencrypt/live/example/privkey.pem`. Absolute paths are resolved again on every reload, so Certbot's `live/` symlinks follow the newly renewed generation. Application-managed ACME certificate IDs are rejected with `409` because their renewal pipeline owns those files. The endpoint accepts either the normal administrator session + CSRF flow or a scoped Certificate API token.
`POST /api/certificates/deploy` accepts
`{"certificate_id":"shared-cert","cert_pem":"...","key_pem":"..."}` under the same
@@ -414,8 +414,9 @@ CLI deploy hook, which calls that loopback API after bounded local file checks:
The command resolves Certbot's local `live/` symlinks, checks bounded regular files/private-key permissions, then sends the pair to `POST /api/certificates/deploy`. The running service validates the certificate/key and DNS SANs, stages generation-specific files inside RustProxy's private certificate directory, and creates or switches the **manual** certificate id through the normal revision-CAS configuration path. A single id may already be selected by multiple vhosts, so those vhosts switch together
without individual edits. RustProxy-managed ACME entries (`issuer != null`) are rejected to
avoid two renewal owners for the same lineage. The existing `/api/certificates/upload` remains
the generic PEM staging endpoint; private certificate paths are never accepted from a remote
request.
the generic PEM staging endpoint; deploy/upload request bodies never accept host filesystem
paths. Absolute paths may only be saved explicitly in administrator TLS configuration as a
manual certificate source, where they are validated and never returned as PEM contents.
## 0.23.0 scoped Certificate API credentials
@@ -428,6 +429,17 @@ Authorization: Bearer <token>
X-RustProxy-Certificate-Key: <key-id>
```
Example Certbot/acme.sh post-renew reload for an external-path certificate:
```bash
curl --fail --show-error -X POST \
-H 'Authorization: Bearer TOKEN' \
-H 'X-RustProxy-Certificate-Key: certbot-main' \
-H 'Content-Type: application/json' \
--data '{}' \
https://proxy-admin.example/api/certificates/shared-cert/reload
```
A Certificate API credential is accepted only by `POST /api/certificates/deploy`, `POST /api/certificates/reload`, and `POST /api/certificates/{certificate_id}/reload`; it cannot read configuration, logs, metrics or any other administration endpoint. Non-loopback use requires the configured HTTPS administration surface. The normal administrator session + Origin/request-marker + CSRF path remains valid for the same operations. Credential management remains administrator-only: `GET /api/certificate-api` lists metadata, `PUT /api/certificate-api` creates/rotates one Key ID, `DELETE /api/certificate-api/{key_id}` revokes one token, and `DELETE /api/certificate-api` revokes all tokens.
Example external deploy:
+1 -1
View File
@@ -1,4 +1,4 @@
# Administration API access - 0.28.0
# Administration API access - 0.28.1
## Sign-in boundary
+2 -2
View File
@@ -9,7 +9,7 @@ each Docker host, so the proxy container never needs Docker-socket access. See
Build locally with a normal OCI builder:
```bash
docker build -t rustproxy:0.28.0 .
docker build -t rustproxy:0.28.1 .
```
Persist `/var/lib/rustproxy`; it contains the SQLite database, certificates and the
@@ -22,7 +22,7 @@ docker volume create rustproxy-data
docker run --rm -d --name rustproxy \
-p 8080:8080 \
-v rustproxy-data:/var/lib/rustproxy \
rustproxy:0.28.0
rustproxy:0.28.1
docker exec rustproxy rust-proxy admin setup-token
```
+1 -1
View File
@@ -1,6 +1,6 @@
# Stored log explorer
RustProxy 0.28.0 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent.
RustProxy 0.28.1 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent.
## Separation from Live Logs
+37 -7
View File
@@ -1,4 +1,4 @@
# Installing and updating RustProxy 0.28.0
# Installing and updating RustProxy 0.28.1
The scripts prepare a Linux/systemd installation. This source archive is **not a
verified production build**: Rust/Cargo, real CA operations, live systemd startup
@@ -200,8 +200,36 @@ The provider script is embedded in the RustProxy binary; this command is diagnos
If another service already owns a certificate through the system Certbot, keep that lineage
outside RustProxy's managed ACME profiles and create/use a **manual** RustProxy certificate id.
A Certbot deploy hook can publish every successful renewal to all vhosts that already select
that id:
The preferred 0.28.1 workflow is to select **Existing server files / external paths** and then
**External files (absolute paths)** in the Certificates panel. For example:
```text
Certificate: /etc/letsencrypt/live/example.com/fullchain.pem
Private key: /etc/letsencrypt/live/example.com/privkey.pem
```
RustProxy does not copy these files into its data directory. On every targeted reload it resolves
Certbot's `live/` symlinks again, validates the new certificate/key pair and atomically publishes a
new public TLS snapshot. A renewal wrapper can therefore call:
```bash
curl --fail --show-error -X POST \
-H 'Authorization: Bearer TOKEN' \
-H 'X-RustProxy-Certificate-Key: certbot-main' \
-H 'Content-Type: application/json' \
--data '{}' \
https://proxy-admin.example/api/certificates/shared-cert/reload
```
The `rustproxy` service identity must be able to traverse the parent directories and read both
files. An external private key may be `0600` when owned by that identity or `0640` with a
restricted group readable by RustProxy; world access, group write and executable bits are
rejected. These external files are not included in a RustProxy backup.
If you do not want the RustProxy service to read `/etc/letsencrypt`, keep the previous copy/deploy
workflow instead. A Certbot deploy hook can publish every successful renewal into RustProxy's
managed certificate directory:
```bash
/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy tls deploy --id shared-cert \
@@ -209,10 +237,12 @@ that id:
--key /etc/letsencrypt/live/example/privkey.pem
```
The command reads the Certbot files locally and sends them to the authenticated loopback `POST /api/certificates/deploy` endpoint. The running RustProxy service writes the private generation with its own filesystem identity and hot-publishes it for new public TLS/H2/H3 handshakes. No per-vhost edit is needed. If another
local process instead replaces the exact files already referenced by RustProxy, run
`/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy tls reload` (or authenticated `POST /api/certificates/reload`) afterward. If the replacement belongs to one configured manual certificate ID, automation may instead call `POST /api/certificates/{certificate_id}/reload`; the same endpoint accepts a scoped Certificate API Key ID + Bearer token and does not change the configuration revision.
Administration TLS is startup-fixed and may still require a service restart.
The deploy command reads the Certbot files locally and sends them to the authenticated loopback
`POST /api/certificates/deploy` endpoint. The running RustProxy service writes a private managed
generation and hot-publishes it for new public TLS/H2/H3 handshakes. Do not use `deploy` for a
certificate ID configured with external absolute paths; renew those files in place and call the
targeted reload endpoint instead. Administration TLS is startup-fixed and may still require a
service restart.
Optional environment overrides go in a root-owned `/etc/rustproxy/environment`
(mode 0600); create the directory when needed. Restart after changing service
+2 -1
View File
@@ -15,6 +15,7 @@ This is the canonical index for the current project documentation. The authentic
- [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.
- [0.28.1 implementation note](V0.28.1_IMPLEMENTATION.md) - external certificate paths, Certbot symlink reloads, backup semantics and certificate-source UX.
- [0.28.0 implementation note](V0.28.0_IMPLEMENTATION.md) - manual/hot-reloaded GeoIP, listener status UX, cache Via/narrow invalidation and header-rule layout.
- [0.27.3 implementation note](V0.27.3_IMPLEMENTATION.md) - HTTP/3 HEAD cache parity, COLD/rate-limit memory reduction, backup scope and package-path update.
- [0.27.2 implementation note](V0.27.2_IMPLEMENTATION.md) - managed Certbot relocation/renewal repair.
@@ -72,7 +73,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. The current release implementation note is [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md); [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md) is retained as the prior HTTP/3/cache-maintenance milestone, [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability.
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.28.1_IMPLEMENTATION.md](V0.28.1_IMPLEMENTATION.md); [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md) is retained as the prior GeoIP/listener/cache milestone, and [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md) is retained as the prior HTTP/3/cache-maintenance milestone, [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability.
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above.
+25 -4
View File
@@ -79,7 +79,27 @@ migration of its stored paths. A system-wide timer should not also manage these
RustProxy-isolated profiles.
For a certificate intentionally owned by a separate system Certbot instead of this managed
renewal subsystem, use a manual certificate id and a Certbot deploy hook that runs, for example:
renewal subsystem, 0.28.1 can reference the Certbot lineage directly. Configure a manual
certificate id with both absolute paths, for example
`/etc/letsencrypt/live/example.com/fullchain.pem` and
`/etc/letsencrypt/live/example.com/privkey.pem`. RustProxy resolves the `live/` symlinks again on
every targeted reload, so a renewal wrapper only needs to call:
```bash
curl --fail --show-error -X POST \
-H 'Authorization: Bearer TOKEN' \
-H 'X-RustProxy-Certificate-Key: certbot-main' \
-H 'Content-Type: application/json' \
--data '{}' \
https://proxy-admin.example/api/certificates/shared-cert/reload
```
The service identity must be able to traverse the Certbot directories and read the files. External
private keys may be owner-only (`0600`) or group-readable (`0640`) for a restricted group; they
must not be executable, group-writable or world-accessible. The referenced files are not copied
into RustProxy backups.
If direct read access to the Certbot tree is undesirable, use the copy/deploy workflow instead:
```bash
/opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy tls deploy --id shared-cert \
@@ -87,9 +107,10 @@ renewal subsystem, use a manual certificate id and a Certbot deploy hook that ru
--key /etc/letsencrypt/live/example/privkey.pem
```
The command resolves Certbot's `live/` symlinks locally, validates and stages a new pair, then
publishes the existing id through the normal revisioned configuration path. Do not point this
workflow at a certificate carrying RustProxy `issuer` metadata; one lineage must have one owner.
That command resolves the Certbot `live/` symlinks locally, validates and stages a managed pair,
then publishes the existing id. Do not use deploy for an id configured with external absolute
paths, and do not point either external-manual workflow at a certificate carrying RustProxy
`issuer` metadata; one lineage must have one owner.
Staging certificates are not trusted by browsers. A Dry run checks renewal
validation but does not deploy the staging result. Only an explicit production
+1 -1
View File
@@ -39,7 +39,7 @@ The encrypted backup contains the durable application state needed for recovery,
- 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, SQLite WAL/SHM sidecars and the managed Certbot Python environment (`acme-python`; the `acme_python` spelling is excluded as well). Certificate/ACME durable state remains included; the virtual environment is rebuilt when needed. 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.
It excludes rebuildable/runtime data such as cache, logs, temporary files, sockets, PID/lock files, SQLite WAL/SHM sidecars and the managed Certbot Python environment (`acme-python`; the `acme_python` spelling is excluded as well). Certificate/ACME durable state inside the RustProxy data directory remains included; the virtual environment is rebuilt when needed. Manual certificates configured with absolute external paths (for example `/etc/letsencrypt/live/...`) are intentionally **not** copied into the backup: only the configured path remains in SQLite, so the external files or mounts must be restored separately. Symlinks inside the backed-up RustProxy data tree are accepted only when their target is relative and resolves inside that staged tree; absolute, escaping or dangling links fail backup/restore. Restore recreates accepted in-tree 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.
+9 -11
View File
@@ -1,16 +1,12 @@
# Verification report - RustProxy 0.28.0
# Verification report - RustProxy 0.28.1
## Scope
RustProxy 0.28.0 adds authenticated on-demand GeoIP refresh through the same staging/lock path as the automatic worker, replaces coupled release/checksum bases with direct per-database sources, makes publisher SHA-256 URLs optional, supports either separate IPv4/IPv6 CSV ranges or one native MaxMind DB (`.mmdb`), and verifies hot replacement of the shared `ArcSwap` database. It also improves active-listener presentation with running `H1`/`H2`/`H2C`/`H3` badges, reports the completed ACME Python/Certbot probe in the INFO log instead of printing speculative startup state, adds configurable cache `Via` metadata, narrows cache compatibility invalidation for bypass/response-only diagnostics, and fixes the vhost Security header-rule Delete/Operation layout. It retains the 0.27.3 restore/listener/WebSocket/update-check/automatic-GeoIP fixes.
RustProxy 0.28.1 adds absolute external certificate/key paths for manual certificates while preserving the existing managed filename mode under `<data-dir>/certs`. The intended renewal path is Certbot/acme.sh updating `/etc/letsencrypt/live/...` followed by `POST /api/certificates/{certificate_id}/reload`. External paths are canonicalized on every load/reload so Certbot `live/` symlinks follow the current archive generation, then the final file is opened as a bounded regular file. Mixed managed/external pairs are rejected, certificate/key matching remains mandatory and managed private keys retain owner-only checks, while external keys may be `0640` for a restricted service group but cannot be group-writable, executable or world-accessible.
SQLite schema 8 and COLD cache format 3 remain unchanged. Older saved configurations remain compatible: legacy GeoIP `release_url`/`checksum_url` values are accepted and migrated into the direct-source model, while the old moving default checksum source is deliberately treated as unpinned and therefore optional. `geoip_updates` and the new `cache.via_header` are serde-defaulted when absent. The cache `Via` value defaults to `RustProxy`; `null` disables it.
The targeted reload path remains revision-stable and republishes one immutable TLS snapshot. New public H1/H2/H3 handshakes use the refreshed material; invalid replacement files leave the previous snapshot active. The deploy endpoint refuses an ID configured with absolute external paths so a renewal wrapper cannot accidentally change storage ownership. Externally referenced files are excluded from RustProxy backups; only their configured paths survive, and the operator must restore or mount the external material separately.
The GeoIP request path reads `state.geoip.load()` when a frontend/location policy, country rate limit or country log capture needs a country. Manual and automatic updates both publish through the same `ArcSwap<Database>`. The regression test keeps an old `Arc`, publishes a replacement database and verifies that the old snapshot remains valid while subsequent slot loads return the new country mapping. CSV mode validates both IP families and ordering; MMDB mode validates MaxMind DB v2 structure and supports standard `country.iso_code`, `registered_country.iso_code`, sapics-style top-level `country_code`, and a two-letter root string used by simple custom databases.
When publisher checksum URLs are empty, downloads remain size-bounded and structurally validated. RustProxy computes the staged file's local SHA-256 and compares it with the installed file, so unchanged content is not republished. If an optional checksum URL is configured, the publisher digest is additionally enforced. A disabled MMDB downloader may use a locally managed `<data-dir>/geoip/country.mmdb`; the manual refresh action validates and hot-reloads that local file.
Cache compatibility hashing intentionally ignores `bypass_query`, `bypass_prefixes` and response-only cache diagnostic/Via fields. Those settings affect admission/wire behavior on subsequent requests but do not mutate a stored representation. Representation/origin-affecting cache, transform, header, forwarding and vhost changes remain in the compatibility identity.
The certificate UI now distinguishes RustProxy-managed filenames from external absolute paths and provides explicit English/Polish storage guidance for ACME, Import and Existing-files modes. 0.28.1 retains all 0.28.0 GeoIP direct-source/MMDB/hot-swap, listener protocol-badge, ACME runtime-reporting, cache Via/narrow invalidation and Security-editor behavior. SQLite schema 8 and COLD cache format 3 remain unchanged.
## Verification status in this workspace
@@ -25,6 +21,7 @@ Rust/Cargo is not installed in this workspace. Rust-independent release checks c
| Cache Via/narrow invalidation source contracts | PASS |
| Listener protocol-badge/header-editor UI source contracts | PASS |
| ACME startup reporting contract (no speculative banner state; final INFO event) | PASS |
| External certificate path / Certbot-symlink / managed-vs-external source contracts | PASS |
| Documentation and language-pack checks | PASS |
| Web Server, security and dependency source contracts | PASS |
| `tools/checks/check_source.py` | PASS (compiler/runtime explicitly not verified) |
@@ -33,8 +30,9 @@ Rust/Cargo is not installed in this workspace. Rust-independent release checks c
| Browser Web Service UI regression (`node tools/checks/web_service_ui_test.mjs`) | PASS |
| Docker Agent (`go test ./...`) | PASS |
| `./dev.sh test` static/pre-compiler stages | PASS; command then exits 127 at the Rust/Cargo gate |
| Rust `cargo fmt/check/test`, linking and Rust unit tests (including synthetic standard/sapics/custom-MMDB lookup fixtures, GeoIP hot-swap, cache Via and cache-revision tests) | NOT VERIFIED - Cargo/Rust is not installed in this workspace |
| Live manual/automatic GeoIP direct-source download and real-world MMDB lookup | NOT VERIFIED - requires network access and a compiled 0.28.0 binary |
| Live HTTP/2 + HTTP/3 protocol tests | NOT VERIFIED - requires a compiled 0.28.0 binary and QUIC-capable runtime |
| Rust `cargo fmt/check/test`, linking and Rust unit tests (including external-path/Certbot-symlink reload, synthetic standard/sapics/custom-MMDB lookup fixtures, GeoIP hot-swap, cache Via and cache-revision tests) | NOT VERIFIED - Cargo/Rust is not installed in this workspace |
| Live manual/automatic GeoIP direct-source download and real-world MMDB lookup | NOT VERIFIED - requires network access and a compiled 0.28.1 binary |
| Live external Certbot-path reload through `POST /api/certificates/{certificate_id}/reload` | NOT VERIFIED - requires a compiled 0.28.1 binary plus readable external certificate material |
| Live HTTP/2 + HTTP/3 protocol tests | NOT VERIFIED - requires a compiled 0.28.1 binary and QUIC-capable runtime |
`./dev.sh test` is expected to exit with status 127 only when it reaches the Rust toolchain gate; all earlier release/static checks must complete successfully for this report to remain valid.
+9 -4
View File
@@ -96,9 +96,12 @@ Changing listener addresses or enabling a new listener requires a restart.
`data/certs/default.pem` and `default.key` are created once when needed. Generated
certificates are self-signed development material; they do not provide public trust.
Explicit bindings use exact hostname names and already-present certificate/key files.
The API stores filenames and names, never private key bytes. CLI generate/import
refuses to overwrite named files. Input size, regular-file status, private-key
permissions, matching keys, chain parsing and name coverage are validated.
Manual entries support two addressing modes: simple filenames are loaded from
`<data-dir>/certs`, while two absolute paths are read in place (for example Certbot
`/etc/letsencrypt/live/<name>/fullchain.pem` plus `privkey.pem`). Mixed relative/absolute
pairs are rejected. The API stores filenames/paths and names, never private key bytes.
CLI generate/import refuses to overwrite named files. Input size, regular-file status,
private-key permissions, matching keys, chain parsing and name coverage are validated.
```bash
rust-proxy tls generate --name app --hosts app.localhost,alias.localhost
@@ -127,7 +130,9 @@ handshakes use the refreshed snapshot, while established sessions/tunnels are no
The administration TLS listener remains startup-fixed and reports that a service restart is
required when its selected certificate changed.
For an externally managed/manual certificate whose configured files were replaced in place, `POST /api/certificates/{certificate_id}/reload` provides the same revision-stable hot-publication contract while targeting that certificate ID. The TLS resolver is immutable, so RustProxy validates and republishes a complete snapshot atomically; this avoids partially updated SNI state. Managed ACME certificate IDs are not accepted by this manual reload path.
For an externally managed/manual certificate whose configured files were replaced in place, `POST /api/certificates/{certificate_id}/reload` provides the same revision-stable hot-publication contract while targeting that certificate ID. For absolute external paths RustProxy canonicalizes the path again on every reload, intentionally following Certbot `live/` symlinks to the current `archive/` generation before opening the final regular file. The TLS resolver is immutable, so RustProxy validates and republishes a complete snapshot atomically; this avoids partially updated SNI state. Managed ACME certificate IDs are not accepted by this manual reload path.
External absolute-path files are not copied into RustProxy full backups. Their path strings remain in configuration; restore or mount the certificate material separately on the destination host before starting TLS. The `rustproxy` service identity must be able to traverse every parent directory and read both files. External private keys may be owner-only (`0600`) or read-only for a restricted service group (`0640`); group-write, execute and any world permission are rejected.
## WebSocket behavior
+46
View File
@@ -0,0 +1,46 @@
# RustProxy 0.28.1 implementation note
## Scope
0.28.1 adds first-class support for certificate/key pairs that are managed outside the RustProxy data directory, with Certbot's `/etc/letsencrypt/live/...` layout as the primary operator case. It retains the managed filename mode under `<data-dir>/certs`, all 0.28.0 GeoIP/listener/cache/UI changes, SQLite schema 8 and COLD cache format 3.
## Certificate file modes
Manual certificate entries use the existing `cert_file` and `key_file` fields. Their addressing mode is derived without adding a database/configuration schema field:
- two simple filenames, for example `app.pem` and `app.key`, are resolved inside `<data-dir>/certs`;
- two absolute paths, for example `/etc/letsencrypt/live/example.com/fullchain.pem` and `/etc/letsencrypt/live/example.com/privkey.pem`, are read in place;
- mixed relative/absolute pairs are rejected.
Managed filenames retain the existing traversal protection and `O_NOFOLLOW` open behavior. Absolute external paths are bounded to 4096 bytes. Each external path is canonicalized at load/reload time, which intentionally resolves Certbot `live/` symlinks to the current generation under `archive/`; the resolved final file is then opened with `O_NOFOLLOW`, must be a regular file and remains subject to the 128 KiB PEM bound. Managed private keys remain owner-only. External private keys may grant read-only group access (for example `0640`) so the unprivileged `rustproxy` service can use a restricted TLS group, but they may not be executable, group-writable or world-accessible; every parent directory must also be traversable by the service identity.
The certificate/key pair is still parsed by rustls and must match. Config publication remains atomic: invalid or unreadable replacement material leaves the previous immutable TLS snapshot active.
## Renewal wrapper workflow
For an externally managed certificate, renew the files using Certbot/acme.sh/another issuer and then call:
```text
POST /api/certificates/{certificate_id}/reload
```
The endpoint re-reads configured files and republishes the complete immutable TLS/routing snapshot without changing the saved configuration revision. New HTTP/1.1, HTTP/2 and HTTP/3 handshakes use the refreshed certificate; established TLS sessions are not renegotiated. A selected administration certificate remains startup-fixed and reports that process restart is required.
Scoped Certificate API credentials can call the targeted reload endpoint. `POST /api/certificates/deploy` remains a PEM-body deployment API and deliberately refuses to overwrite an ID that is configured with absolute external file paths; this prevents a renewal owner from silently switching storage models.
## Backups
RustProxy full backups contain managed certificate material under `<data-dir>/certs`. Absolute external certificate/key files are not copied into the backup. Their configured paths remain in SQLite/configuration, so the files or mounts must exist separately on the restored host before TLS can be compiled successfully.
## Administration UI
Certificates -> Generate/Add -> Existing server files now has an explicit file-location selector:
- **RustProxy certificate directory (`<data-dir>/certs`)** accepts filenames only;
- **External files (absolute paths)** accepts complete server paths and shows a Certbot example.
English and Polish provider hints also explain that ACME modes generate/manage their own material, Import copies browser-selected PEM files into RustProxy storage, and Existing files references server-side material. Installed-certificate cards mark absolute-path entries as external.
## Compatibility
No migration is required from 0.28.0. Existing filename-only certificate entries keep their prior meaning. Existing deploy/upload/ACME flows continue to create RustProxy-managed filenames and never place PEM key bytes in configuration responses.
+1 -1
View File
@@ -19,7 +19,7 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw
are intentionally absent from Prometheus labels.
Import the JSON in Grafana and select a Prometheus datasource when prompted. The
dashboard expects RustProxy 0.28.0 metric names and a scrape interval around 15s.
dashboard expects RustProxy 0.28.1 metric names and a scrape interval around 15s.
Collectors can be disabled in RustProxy settings; panels for disabled collectors
will simply show no series.
+2 -2
View File
@@ -4529,9 +4529,9 @@
},
"timepicker": {},
"timezone": "browser",
"title": "RustProxy 0.28.0 - Operations",
"title": "RustProxy 0.28.1 - Operations",
"uid": "rustproxy-0170",
"version": 5,
"weekStart": "",
"description": "RustProxy 0.28.0 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
"description": "RustProxy 0.28.1 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
}
+18 -12
View File
@@ -177,13 +177,13 @@
"certificateBindings": "Certificate bindings",
"addCertificate": "Add certificate",
"certificateId": "Certificate ID",
"certificateFile": "Certificate filename",
"privateKeyFile": "Private key filename",
"certificateFile": "Certificate filename / path",
"privateKeyFile": "Private key filename / path",
"defaultCertificate": "Use as the default certificate",
"saveCertificate": "Apply certificate",
"reloadCertificates": "Reload certificate files",
"certificateHelp": "Upload a PEM pair, reference files already in data/certs, or deploy an externally issued certificate with the installed RustProxy binary. Reload re-reads configured files without changing the configuration revision.",
"certificateHint": "External Certbot hook (standard service install): rust-proxy --data-dir /var/lib/rustproxy tls deploy --id shared-cert --cert /etc/letsencrypt/live/example/fullchain.pem --key /etc/letsencrypt/live/example/privkey.pem",
"certificateHelp": "Use RustProxy-managed files, absolute external paths, uploaded PEM pairs, or ACME. Managed filenames are loaded from <data-dir>/certs. External paths are read in place and can be reloaded after an outside renewal without changing the configuration revision.",
"certificateHint": "External renewal: keep fullchain.pem/privkey.pem paths in the certificate entry, renew them with Certbot/acme.sh, then use Reload certificate or POST /api/certificates/{certificate_id}/reload.",
"emptyCertificates": "No explicit SNI bindings. The generated default certificate is used while the TLS listener is enabled.",
"certificateSafety": "Certificates secure TLS connections, not routing. Assign a certificate to a vhost. Changes affect new handshakes.",
"wsActive": "WebSocket tunnels",
@@ -724,13 +724,13 @@
"providerOvh": "Let's Encrypt - OVH API (DNS-01)",
"providerRfc2136": "Let's Encrypt - RFC2136 / TSIG (DNS-01)",
"providerImport": "Import certificate and private key (PEM)",
"providerExisting": "Existing server files (advanced)",
"providerHint_http01": "Requires Certbot. Every domain must resolve to this proxy and be reachable on public TCP port 80. RustProxy serves the challenge without stopping HTTP traffic. Wildcards need DNS-01. Staging certificates are not browser-trusted; automatic renewal is not installed.",
"providerHint_cloudflare": "Requires Certbot + certbot-dns-cloudflare. Use a restricted API token with Zone:DNS:Edit for the necessary zones only. Supports wildcard certificates. Secrets stay in private server files.",
"providerHint_ovh": "Requires Certbot + certbot-dns-ovh. Supply application key, application secret and consumer key authorized for the required DNS zones. Supports wildcard certificates.",
"providerHint_rfc2136": "Requires Certbot + certbot-dns-rfc2136 and a DNS server accepting signed RFC2136 updates. Limit the TSIG key to the required challenge records.",
"providerHint_import": "Upload a matching PEM/fullchain and private key. Save in the library, then assign the certificate in a vhost SSL tab.",
"providerHint_existing": "Advanced: only existing private files inside the configured certificate directory. Never put private key contents in JSON.",
"providerExisting": "Existing server files / external paths (advanced)",
"providerHint_http01": "RustProxy runs Certbot HTTP-01. You do not provide certificate file paths here. Every domain must resolve to this proxy and public TCP port 80 must reach its HTTP listener. The issued pair is stored in RustProxy-managed certificate storage. Wildcards require DNS-01.",
"providerHint_cloudflare": "RustProxy runs Certbot DNS-01 with the Cloudflare plugin. You do not provide certificate file paths here. Use a restricted Zone:DNS:Edit API token; the issued pair is stored in RustProxy-managed certificate storage and can be renewed automatically.",
"providerHint_ovh": "RustProxy runs Certbot DNS-01 with the OVH plugin. You do not provide certificate file paths here. Supply credentials limited to the required DNS zones; the issued pair is stored in RustProxy-managed certificate storage and can be renewed automatically.",
"providerHint_rfc2136": "RustProxy runs Certbot DNS-01 with RFC2136/TSIG. You do not provide certificate file paths here. The DNS server must accept signed updates for the required challenge records; the issued pair is stored in RustProxy-managed certificate storage.",
"providerHint_import": "Choose certificate/fullchain and private-key PEM files from your computer. RustProxy validates the pair and copies it into <data-dir>/certs; the browser-side source path is not saved.",
"providerHint_existing": "Reference files already present on the RustProxy server. Choose managed filenames from <data-dir>/certs or absolute external paths such as /etc/letsencrypt/live/example.com/fullchain.pem and privkey.pem. External files are not copied into RustProxy backups.",
"acmeCfToken": "Cloudflare API token",
"acmeOvhEndpoint": "OVH API region",
"acmeOvhApplicationKey": "OVH application key",
@@ -2182,6 +2182,12 @@
"geoipIpv6ChecksumUrl": "IPv6 SHA-256 URL (optional)",
"geoipMmdbUrl": "MaxMind DB direct URL",
"geoipMmdbChecksumUrl": "MaxMind DB SHA-256 URL (optional)",
"geoipSourceHelp": "CSV mode uses separate IPv4 and IPv6 range files. MMDB mode uses one native MaxMind DB file and supports standard country.iso_code, registered_country.iso_code, or country_code records. Leave a checksum URL empty to skip publisher-checksum verification; every download is still size-bounded and format-validated before atomic publish. For a local custom MMDB, place it at <data-dir>/geoip/country.mmdb, select MMDB and disable automatic updates."
"geoipSourceHelp": "CSV mode uses separate IPv4 and IPv6 range files. MMDB mode uses one native MaxMind DB file and supports standard country.iso_code, registered_country.iso_code, or country_code records. Leave a checksum URL empty to skip publisher-checksum verification; every download is still size-bounded and format-validated before atomic publish. For a local custom MMDB, place it at <data-dir>/geoip/country.mmdb, select MMDB and disable automatic updates.",
"certificateFileSource": "File location",
"certificateSourceManaged": "RustProxy certificate directory (<data-dir>/certs)",
"certificateSourceExternal": "External files (absolute paths)",
"certificateSourceManagedHelp": "Enter filenames only, for example app.pem and app.key. RustProxy loads them from <data-dir>/certs (normally /var/lib/rustproxy/certs for a packaged service).",
"certificateSourceExternalHelp": "Enter absolute server paths. Certbot example: /etc/letsencrypt/live/example.com/fullchain.pem and /etc/letsencrypt/live/example.com/privkey.pem. Certbot live/ symlinks are resolved again on every reload. The rustproxy service user needs directory traversal and file read access; external private keys may use 0600 when owned by the service or 0640 with a restricted group, never world access.",
"certificateExternalFiles": "External file paths"
}
}
+18 -12
View File
@@ -177,13 +177,13 @@
"certificateBindings": "Powiązania certyfikatów",
"addCertificate": "Dodaj certyfikat",
"certificateId": "ID certyfikatu",
"certificateFile": "Nazwa pliku certyfikatu",
"privateKeyFile": "Nazwa pliku klucza prywatnego",
"certificateFile": "Plik certyfikatu / ścieżka",
"privateKeyFile": "Plik klucza prywatnego / ścieżka",
"defaultCertificate": "Użyj jako certyfikatu domyślnego",
"saveCertificate": "Zastosuj certyfikat",
"reloadCertificates": "Wczytaj pliki certyfikatów",
"certificateHelp": "Wgraj parę PEM, wskaż pliki w data/certs albo wdrażaj certyfikat z zewnętrznego wystawcy zainstalowaną binarką RustProxy. Ponowne wczytanie odczytuje skonfigurowane pliki bez zmiany rewizji.",
"certificateHint": "Hook Certbota (standardowa instalacja usługi): rust-proxy --data-dir /var/lib/rustproxy tls deploy --id shared-cert --cert /etc/letsencrypt/live/example/fullchain.pem --key /etc/letsencrypt/live/example/privkey.pem",
"certificateHelp": "Możesz użyć plików zarządzanych przez RustProxy, pełnych ścieżek zewnętrznych, importu PEM albo ACME. Nazwy plików są ładowane z <data-dir>/certs. Ścieżki zewnętrzne są czytane bez kopiowania i można je przeładować po odnowieniu certyfikatu bez zmiany rewizji konfiguracji.",
"certificateHint": "Odnowienie zewnętrzne: pozostaw ścieżki fullchain.pem/privkey.pem w konfiguracji, odnów je przez Certbot/acme.sh, a następnie użyj Przeładuj certyfikat albo POST /api/certificates/{certificate_id}/reload.",
"emptyCertificates": "Brak jawnych powiązań SNI. Aktywny listener TLS używa wygenerowanego certyfikatu domyślnego.",
"certificateSafety": "Certyfikaty samopodpisane służą do testów. Certyfikat nie tworzy trasy. Zmiana dotyczy nowych połączeń TLS.",
"wsActive": "Tunele WebSocket",
@@ -724,13 +724,13 @@
"providerOvh": "Let's Encrypt - OVH API (DNS-01)",
"providerRfc2136": "Let's Encrypt - RFC2136 / TSIG (DNS-01)",
"providerImport": "Import certyfikatu i klucza prywatnego (PEM)",
"providerExisting": "Istniejące pliki na serwerze",
"providerHint_http01": "Walidacja domeny HTTP-01 wymaga publicznego portu 80 prowadzącego do aktywnego listenera HTTP. Nie obsługuje wildcardów.",
"providerHint_cloudflare": "DNS-01 przez API Cloudflare. Użyj tokenu z uprawnieniem Zone:DNS:Edit ograniczonym do odpowiednich stref.",
"providerHint_ovh": "DNS-01 przez API OVH. Podaj klucze aplikacji i klienta z uprawnieniami do rekordów odpowiednich stref DNS.",
"providerHint_rfc2136": "DNS-01 przez dynamiczną aktualizację DNS i klucz TSIG. Serwer musi zezwalać na zmianę rekordów walidacyjnych.",
"providerHint_import": "Wgraj certyfikat lub fullchain i odpowiadający mu klucz prywatny w formacie PEM.",
"providerHint_existing": "Wskaż pliki PEM z katalogu certyfikatów RustProxy. Treść klucza nie trafia do konfiguracji.",
"providerExisting": "Istniejące pliki na serwerze / ścieżki zewnętrzne",
"providerHint_http01": "RustProxy uruchamia Certbota w trybie HTTP-01. Nie podajesz tutaj ścieżek do certyfikatu. Każda domena musi wskazywać na ten serwer, a publiczny TCP/80 musi trafiać do listenera HTTP. Wydana para trafia do magazynu certyfikatów RustProxy. Wildcard wymaga DNS-01.",
"providerHint_cloudflare": "RustProxy uruchamia Certbota DNS-01 z pluginem Cloudflare. Nie podajesz tutaj ścieżek do certyfikatu. Użyj tokenu Zone:DNS:Edit ograniczonego do wymaganych stref; wydana para trafia do magazynu RustProxy i może być odnawiana automatycznie.",
"providerHint_ovh": "RustProxy uruchamia Certbota DNS-01 z pluginem OVH. Nie podajesz tutaj ścieżek do certyfikatu. Podaj poświadczenia ograniczone do wymaganych stref DNS; wydana para trafia do magazynu RustProxy i może być odnawiana automatycznie.",
"providerHint_rfc2136": "RustProxy uruchamia Certbota DNS-01 przez RFC2136/TSIG. Nie podajesz tutaj ścieżek do certyfikatu. Serwer DNS musi przyjmować podpisane aktualizacje rekordów walidacyjnych; wydana para trafia do magazynu certyfikatów RustProxy.",
"providerHint_import": "Wybierz z komputera certyfikat/fullchain i odpowiadający klucz prywatny PEM. RustProxy sprawdzi parę i skopiuje ją do <data-dir>/certs; lokalna ścieżka z przeglądarki nie jest zapisywana.",
"providerHint_existing": "Wskaż pliki, które już istnieją na serwerze RustProxy. Wybierz nazwy plików z <data-dir>/certs albo pełne ścieżki zewnętrzne, np. /etc/letsencrypt/live/example.com/fullchain.pem i privkey.pem. Pliki zewnętrzne nie są kopiowane do backupu RustProxy.",
"acmeCfToken": "Token API Cloudflare",
"acmeOvhEndpoint": "Region API OVH",
"acmeOvhApplicationKey": "OVH application key",
@@ -2182,6 +2182,12 @@
"geoipIpv6ChecksumUrl": "URL SHA-256 IPv6 (opcjonalny)",
"geoipMmdbUrl": "Bezpośredni URL MaxMind DB",
"geoipMmdbChecksumUrl": "URL SHA-256 MaxMind DB (opcjonalny)",
"geoipSourceHelp": "Tryb CSV używa osobnych plików zakresów IPv4 i IPv6. Tryb MMDB używa jednego natywnego pliku MaxMind DB i obsługuje standardowe pola country.iso_code, registered_country.iso_code albo country_code. Puste pole URL sumy wyłącza weryfikację sumy wydawcy; każdy plik nadal ma limit rozmiaru i jest walidowany przed atomową publikacją. Dla lokalnej własnej bazy MMDB umieść plik w <data-dir>/geoip/country.mmdb, wybierz MMDB i wyłącz automatyczne aktualizacje."
"geoipSourceHelp": "Tryb CSV używa osobnych plików zakresów IPv4 i IPv6. Tryb MMDB używa jednego natywnego pliku MaxMind DB i obsługuje standardowe pola country.iso_code, registered_country.iso_code albo country_code. Puste pole URL sumy wyłącza weryfikację sumy wydawcy; każdy plik nadal ma limit rozmiaru i jest walidowany przed atomową publikacją. Dla lokalnej własnej bazy MMDB umieść plik w <data-dir>/geoip/country.mmdb, wybierz MMDB i wyłącz automatyczne aktualizacje.",
"certificateFileSource": "Położenie plików",
"certificateSourceManaged": "Katalog certyfikatów RustProxy (<data-dir>/certs)",
"certificateSourceExternal": "Pliki zewnętrzne (pełne ścieżki)",
"certificateSourceManagedHelp": "Podaj tylko nazwy plików, np. app.pem i app.key. RustProxy ładuje je z <data-dir>/certs (przy instalacji pakietowej zwykle /var/lib/rustproxy/certs).",
"certificateSourceExternalHelp": "Podaj pełne ścieżki na serwerze. Przykład Certbota: /etc/letsencrypt/live/example.com/fullchain.pem oraz /etc/letsencrypt/live/example.com/privkey.pem. Symlinki Certbota w live/ są rozwiązywane ponownie przy każdym przeładowaniu. Użytkownik usługi rustproxy musi mieć prawo przejścia przez katalogi i odczytu plików; zewnętrzny klucz może mieć 0600, gdy należy do usługi, albo 0640 z ograniczoną grupą, nigdy dostęp dla innych.",
"certificateExternalFiles": "Zewnętrzne ścieżki plików"
}
}
+1 -1
View File
@@ -86,7 +86,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.28.0.zip')
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.28.1.zip')
output = parser.parse_args().output.resolve()
paths = files()
checksum = ROOT / 'SHA256SUMS'
+1 -1
View File
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
"""TLS verification uses Python's default trust store; never executes a shell."""
started = time.monotonic()
opener = build_opener(HttpsRedirects())
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.28.0"})
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.28.1"})
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:
+1 -1
View File
@@ -95,7 +95,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.28.0'})
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.28.1'})
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.')
+3 -3
View File
@@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
for name in MANAGED:
if (target / name).is_symlink():
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
backup = target / ".upgrade-backup" / ("before-0.28.0-" + uuid.uuid4().hex[:12])
backup = target / ".upgrade-backup" / ("before-0.28.1-" + 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.28.0", "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.28.1", "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[2], 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.28.0. Previous managed source files: {backup}")
print(f"Installed RustProxy 0.28.1. 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")
+10
View File
@@ -40,6 +40,16 @@ async fn certificate_deploy(
),
));
}
if crate::tls::certificate_files_are_external(&selected.cert_file, &selected.key_file)
.map_err(|error| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{error:#}")))?
{
return Err(ApiError(
StatusCode::CONFLICT,
format!(
"certificate {certificate_id} uses external file paths; renew those files in place and call the certificate reload endpoint instead of deploy"
),
));
}
Some((selected.cert_file.clone(), selected.key_file.clone()))
} else {
None
+1
View File
@@ -409,6 +409,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
"/docs/V0.25.1_AUDIT.md"
)),
),
reader_doc!("docs/V0.28.1_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.28.0_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.27.3_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.27.2_IMPLEMENTATION.md", "technical"),
+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "RustProxy Administration API",
"version": "0.28.0",
"version": "0.28.1",
"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": [
+24 -3
View File
@@ -20,6 +20,25 @@
}
})();
function certificateUsesExternalFiles(cert) {
return String(cert?.cert_file || "").startsWith("/") && String(cert?.key_file || "").startsWith("/");
}
function syncCertificateFileSource() {
const external = $("certificate-file-source").value === "external";
const cert = $("certificate-file"), key = $("certificate-key");
for (const input of [cert, key]) {
input.maxLength = external ? 4096 : 96;
if (external) input.removeAttribute("pattern");
else input.pattern = "[A-Za-z0-9_.-]+";
}
cert.placeholder = external ? "/etc/letsencrypt/live/example.com/fullchain.pem" : "app.pem";
key.placeholder = external ? "/etc/letsencrypt/live/example.com/privkey.pem" : "app.key";
const helpKey = external ? "certificateSourceExternalHelp" : "certificateSourceManagedHelp";
$("certificate-file-source-help").dataset.i18n = helpKey;
$("certificate-file-source-help").textContent = t(helpKey);
}
$("certificate-file-source").addEventListener("change", syncCertificateFileSource);
function renderCertificates() {
if (!envelope) return;
const settings = envelope.config.tls || {};
@@ -41,6 +60,7 @@ function renderCertificates() {
identity.append(el("span", cert.id, "entity-title"));
const statePills = el("div", undefined, "vhost-policy-pills certificate-state-pills");
if (managed) statePills.append(el("small", `${t("certificateManaged")} · ${cert.issuer.provider} / ${cert.issuer.environment}`, "pill"));
else if (certificateUsesExternalFiles(cert)) statePills.append(el("small", t("certificateExternalFiles"), "pill"));
if (settings.default_certificate === cert.id) statePills.append(el("small", t("defaultLabel"), "pill"));
if (inventory?.expiry_status === "expired") statePills.append(el("small", t("certificateExpired"), "pill status-error"));
else if (inventory?.expiry_status === "warning") statePills.append(el("small", t("certificateExpiresSoon"), "pill status-error"));
@@ -91,7 +111,7 @@ function renderCertificates() {
if (result.administration_restart_required) toast(t("certificateAdminRestartRequired"), true);
} catch (error) { toast(error.message, true); } finally { busy = false; }
}));
buttons.append(action(t("edit"), () => { window.rustProxyCertificates?.open("existing"); $("certificate-advanced").open = true; $("certificate-id").value = cert.id; $("certificate-hosts").value = cert.hosts.join(", "); $("certificate-file").value = cert.cert_file; $("certificate-key").value = cert.key_file; $("certificate-default").checked = settings.default_certificate === cert.id; }));
buttons.append(action(t("edit"), () => { window.rustProxyCertificates?.open("existing"); $("certificate-advanced").open = true; $("certificate-id").value = cert.id; $("certificate-hosts").value = cert.hosts.join(", "); $("certificate-file-source").value = certificateUsesExternalFiles(cert) ? "external" : "managed"; syncCertificateFileSource(); $("certificate-file").value = cert.cert_file; $("certificate-key").value = cert.key_file; $("certificate-default").checked = settings.default_certificate === cert.id; }));
}
const remove = action(t("remove"), async () => { if (inUse) { toast(t("certInUse"), true); return; } if (!(await appConfirm(t("confirmDelete"), { danger: true }))) return; const next = structuredClone(envelope.config); next.tls.certificates = next.tls.certificates.filter(v => v.id !== cert.id); await saveSection("tls", next.tls); });
remove.disabled = inUse; remove.title = inUse ? t("certInUse") : ""; buttons.append(remove);
@@ -101,7 +121,7 @@ function renderCertificates() {
$("certificate-list").replaceChildren(...entries);
if (!entries.length) $("certificate-list").append(el("p", t("emptyCertificates"), "empty"));
}
$("certificate-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const next = structuredClone(envelope.config); next.tls = next.tls || {}; next.tls.certificates = next.tls.certificates || []; const cert = { id: $("certificate-id").value.trim(), hosts: $("certificate-hosts").value.split(",").map(v => v.trim()).filter(Boolean), cert_file: $("certificate-file").value.trim(), key_file: $("certificate-key").value.trim() }; const index = next.tls.certificates.findIndex(v => v.id === cert.id); if (index >= 0 && next.tls.certificates[index].issuer) throw fieldError($("certificate-id"), t("certManagedReadOnly")); if (index < 0) next.tls.certificates.push(cert); else { const old = next.tls.certificates[index]; const sameFiles = old.cert_file === cert.cert_file && old.key_file === cert.key_file; next.tls.certificates[index] = { ...old, ...cert, covered_hosts: sameFiles ? (old.covered_hosts || []) : [], issuer: sameFiles ? old.issuer : null }; } if ($("certificate-default").checked) next.tls.default_certificate = cert.id; else if (next.tls.default_certificate === cert.id) delete next.tls.default_certificate; if (await saveSection("tls", next.tls)) { form.reset(); window.rustProxyCertificates?.close(); } }); } catch (error) { toast(error.message, true); } });
$("certificate-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; if (!envelope || busy) return; try { await withFormSubmitState(form, event.submitter, async () => { const next = structuredClone(envelope.config); next.tls = next.tls || {}; next.tls.certificates = next.tls.certificates || []; const cert = { id: $("certificate-id").value.trim(), hosts: $("certificate-hosts").value.split(",").map(v => v.trim()).filter(Boolean), cert_file: $("certificate-file").value.trim(), key_file: $("certificate-key").value.trim() }; const index = next.tls.certificates.findIndex(v => v.id === cert.id); if (index >= 0 && next.tls.certificates[index].issuer) throw fieldError($("certificate-id"), t("certManagedReadOnly")); if (index < 0) next.tls.certificates.push(cert); else { const old = next.tls.certificates[index]; const sameFiles = old.cert_file === cert.cert_file && old.key_file === cert.key_file; next.tls.certificates[index] = { ...old, ...cert, covered_hosts: sameFiles ? (old.covered_hosts || []) : [], issuer: sameFiles ? old.issuer : null }; } if ($("certificate-default").checked) next.tls.default_certificate = cert.id; else if (next.tls.default_certificate === cert.id) delete next.tls.default_certificate; if (await saveSection("tls", next.tls)) { form.reset(); syncCertificateFileSource(); window.rustProxyCertificates?.close(); } }); } catch (error) { toast(error.message, true); } });
$("reload-certificates").addEventListener("click", async () => {
if (!envelope || busy) return;
try {
@@ -114,7 +134,8 @@ $("reload-certificates").addEventListener("click", async () => {
} catch (error) { toast(error.message, true); }
finally { busy = false; }
});
$("reset-certificate").addEventListener("click", () => $("certificate-form").reset());
$("reset-certificate").addEventListener("click", () => { $("certificate-form").reset(); $("certificate-file-source").value = "managed"; syncCertificateFileSource(); });
syncCertificateFileSource();
// Dashboard metrics are actual server samples. Null means unavailable, not zero.
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null,
+8 -2
View File
@@ -1180,6 +1180,12 @@
placeholder="app" required="" />
<label data-i18n="certLegacyNames" for="certificate-hosts"></label><input
id="certificate-hosts" placeholder="app.localhost" />
<label data-i18n="certificateFileSource" for="certificate-file-source"></label><select
id="certificate-file-source">
<option data-i18n="certificateSourceManaged" value="managed"></option>
<option data-i18n="certificateSourceExternal" value="external"></option>
</select>
<p class="fine" id="certificate-file-source-help" data-i18n="certificateSourceManagedHelp"></p>
<label data-i18n="certificateFile" for="certificate-file"></label><input
id="certificate-file" maxlength="96" pattern="[A-Za-z0-9_.-]+"
placeholder="app.pem" required="" />
@@ -2878,7 +2884,7 @@
</section>
</div>
</section>
<footer class="page-footer"><span>RustProxy <b>0.28.0</b></span><span id="footer-status">API - /
<footer class="page-footer"><span>RustProxy <b>0.28.1</b></span><span id="footer-status">API - /
dropped: 0</span></footer>
</div>
</main>
@@ -3622,7 +3628,7 @@ max_execution_time = 30</code></pre>
<article class="card about-card">
<div class="about-product">
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
<div><span class="small-label">RustProxy</span><strong>0.28.0</strong></div>
<div><span class="small-label">RustProxy</span><strong>0.28.1</strong></div>
</div>
<dl class="about-meta">
<div>
+9 -1
View File
@@ -259,8 +259,16 @@ fn validate_snapshot_config(
) -> Result<()> {
let cert_dir = snapshot_root.join("certs");
for certificate in &envelope.config.tls.certificates {
if crate::tls::certificate_files_are_external(
&certificate.cert_file,
&certificate.key_file,
)? {
// External certificate material is intentionally outside the RustProxy
// data tree and is therefore referenced by configuration, not copied
// into a RustProxy backup. The operator must restore/mount it separately.
continue;
}
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}")
+1 -1
View File
@@ -41,7 +41,7 @@ use zeroize::Zeroizing;
#[command(
name = "rust-proxy",
version,
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.28.0"
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.28.1"
)]
struct Cli {
#[arg(
+1 -1
View File
@@ -239,7 +239,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.28.0 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
println!("RustProxy live logs 0.28.1 - 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();
+5 -8
View File
@@ -310,16 +310,14 @@ fn certificate_public_metadata(certificate: &[u8]) -> Result<CertificateMetadata
/// Used by the authenticated certificate inventory to enrich older/manual entries
/// that predate persisted `covered_hosts` metadata. Private key material is not read.
pub fn certificate_covered_hosts(directory: &Path, cert_file: &str) -> Result<Vec<String>> {
safe_name(cert_file)?;
let certificate = read_pem(&directory.join(cert_file), false)?;
let certificate = read_configured_pem(directory, cert_file, false)?;
certificate_dns_names(&certificate)
}
/// Read non-secret metadata directly from the configured public leaf certificate.
/// Private keys are never opened by this path.
pub fn certificate_metadata(directory: &Path, cert_file: &str) -> Result<CertificateMetadata> {
safe_name(cert_file)?;
let certificate = read_pem(&directory.join(cert_file), false)?;
let certificate = read_configured_pem(directory, cert_file, false)?;
certificate_public_metadata(&certificate)
}
@@ -340,11 +338,10 @@ fn parse_pair(certificate: &[u8], key: &[u8]) -> Result<CertifiedKey> {
Ok(certified)
}
fn load_pair(directory: &Path, cert: &str, key: &str) -> Result<CertifiedKey> {
safe_name(cert)?;
safe_name(key)?;
certificate_files_are_external(cert, key)?;
parse_pair(
&read_pem(&directory.join(cert), false)?,
&read_pem(&directory.join(key), true)?,
&read_configured_pem(directory, cert, false)?,
&read_configured_pem(directory, key, true)?,
)
}
/// Runs on a bounded control-plane blocking worker, never from a handshake.
+65
View File
@@ -158,6 +158,71 @@ mod tests {
assert!(safe_name(path).is_err(), "{path}");
}
assert!(safe_name("app-v2.key").is_ok());
assert!(certificate_files_are_external("app.pem", "app.key").is_ok());
assert!(certificate_files_are_external("app.pem", "/etc/tls/app.key").is_err());
}
#[cfg(unix)]
#[test]
fn external_absolute_paths_follow_certbot_style_symlinks_on_each_reload() {
use std::os::unix::fs::{symlink, PermissionsExt};
let root = tempfile::tempdir().unwrap();
let cert_dir = root.path().join("rustproxy-certs");
fs::create_dir(&cert_dir).unwrap();
initialize_default(&cert_dir).unwrap();
let archive = root.path().join("archive/example.test");
let live = root.path().join("live/example.test");
fs::create_dir_all(&archive).unwrap();
fs::create_dir_all(&live).unwrap();
let first = rcgen::generate_simple_self_signed(vec!["example.test".into()]).unwrap();
fs::write(archive.join("fullchain1.pem"), first.cert.pem()).unwrap();
fs::write(archive.join("privkey1.pem"), first.key_pair.serialize_pem()).unwrap();
fs::set_permissions(archive.join("privkey1.pem"), fs::Permissions::from_mode(0o640)).unwrap();
symlink("../../archive/example.test/fullchain1.pem", live.join("fullchain.pem")).unwrap();
symlink("../../archive/example.test/privkey1.pem", live.join("privkey.pem")).unwrap();
let cert_path = live.join("fullchain.pem").display().to_string();
let key_path = live.join("privkey.pem").display().to_string();
assert!(certificate_files_are_external(&cert_path, &key_path).unwrap());
let settings = Settings {
default_certificate: Some("letsencrypt".into()),
certificates: vec![Certificate {
issuer: None,
id: "letsencrypt".into(),
covered_hosts: Vec::new(),
hosts: vec!["example.test".into()],
cert_file: cert_path.clone(),
key_file: key_path.clone(),
}],
..Settings::default()
};
assert!(compile(&settings, Some(&cert_dir)).unwrap().is_some());
assert_eq!(
certificate_metadata(&cert_dir, &cert_path).unwrap().covered_hosts,
vec!["example.test"]
);
let second = rcgen::generate_simple_self_signed(vec![
"example.test".into(),
"www.example.test".into(),
])
.unwrap();
fs::write(archive.join("fullchain2.pem"), second.cert.pem()).unwrap();
fs::write(archive.join("privkey2.pem"), second.key_pair.serialize_pem()).unwrap();
fs::set_permissions(archive.join("privkey2.pem"), fs::Permissions::from_mode(0o640)).unwrap();
fs::remove_file(live.join("fullchain.pem")).unwrap();
fs::remove_file(live.join("privkey.pem")).unwrap();
symlink("../../archive/example.test/fullchain2.pem", live.join("fullchain.pem")).unwrap();
symlink("../../archive/example.test/privkey2.pem", live.join("privkey.pem")).unwrap();
assert!(compile(&settings, Some(&cert_dir)).unwrap().is_some());
assert_eq!(
certificate_metadata(&cert_dir, &cert_path).unwrap().covered_hosts,
vec!["example.test", "www.example.test"]
);
}
#[test]
fn default_is_persistent_and_tls_advertises_both_protocols() {
+77 -6
View File
@@ -16,6 +16,7 @@ use std::{
use zeroize::Zeroizing;
const MAX_PEM: u64 = 131_072;
const MAX_CERTIFICATE_PATH: usize = 4096;
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct Settings {
@@ -324,7 +325,68 @@ pub fn safe_name(name: &str) -> Result<()> {
);
Ok(())
}
/// Validate how a configured certificate pair is addressed. Legacy/simple names
/// remain private to `<data-dir>/certs`; an explicitly selected external pair uses
/// two absolute paths. Mixing the two modes is rejected so the source is never
/// ambiguous.
pub fn certificate_files_are_external(cert_file: &str, key_file: &str) -> Result<bool> {
let cert_external = Path::new(cert_file).is_absolute();
let key_external = Path::new(key_file).is_absolute();
ensure!(
cert_external == key_external,
"certificate and private key must both use filenames from the RustProxy certificate directory or both use absolute external paths"
);
if cert_external {
ensure!(
!cert_file.is_empty()
&& !key_file.is_empty()
&& cert_file.len() <= MAX_CERTIFICATE_PATH
&& key_file.len() <= MAX_CERTIFICATE_PATH,
"external certificate paths must be absolute paths up to 4096 bytes"
);
Ok(true)
} else {
safe_name(cert_file)?;
safe_name(key_file)?;
Ok(false)
}
}
fn configured_certificate_path(directory: &Path, value: &str) -> Result<PathBuf> {
if Path::new(value).is_absolute() {
ensure!(
!value.is_empty() && value.len() <= MAX_CERTIFICATE_PATH,
"external certificate path must be an absolute path up to 4096 bytes"
);
// Certbot's live/ entries are symlinks to archive/. Resolve them on every
// reload, then open the final regular file with O_NOFOLLOW in read_pem.
return fs::canonicalize(value)
.with_context(|| format!("cannot resolve external certificate path {value}"));
}
safe_name(value)?;
Ok(directory.join(value))
}
fn read_configured_pem(
directory: &Path,
value: &str,
private: bool,
) -> Result<Zeroizing<Vec<u8>>> {
let external = Path::new(value).is_absolute();
let path = configured_certificate_path(directory, value)?;
read_pem_with_policy(&path, private, external)
}
fn read_pem(path: &Path, private: bool) -> Result<Zeroizing<Vec<u8>>> {
read_pem_with_policy(path, private, false)
}
fn read_pem_with_policy(
path: &Path,
private: bool,
external_group_read: bool,
) -> Result<Zeroizing<Vec<u8>>> {
let mut options = OpenOptions::new();
options.read(true);
#[cfg(unix)]
@@ -343,14 +405,23 @@ fn read_pem(path: &Path, private: bool) -> Result<Zeroizing<Vec<u8>>> {
#[cfg(unix)]
if private {
use std::os::unix::fs::PermissionsExt;
ensure!(
meta.permissions().mode() & 0o077 == 0,
"private key permissions must be 600: {}",
path.display()
);
let mode = meta.permissions().mode();
if external_group_read {
ensure!(
mode & 0o137 == 0,
"external private key may be owner/group-readable but must not be executable, group-writable, or world-accessible: {}",
path.display()
);
} else {
ensure!(
mode & 0o077 == 0,
"private key permissions must not grant group/world access: {}",
path.display()
);
}
}
#[cfg(not(unix))]
let _ = private;
let _ = (private, external_group_read);
let mut data = Zeroizing::new(Vec::new());
file.take(MAX_PEM + 1).read_to_end(&mut data)?;
ensure!(
+1 -1
View File
@@ -159,7 +159,7 @@ pub(super) fn fastcgi_params(
if context.tls { "on" } else { "off" }.into(),
);
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.28.0".into());
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.28.1".into());
params.insert("REDIRECT_STATUS".into(), "200".into());
if let Some(value) = headers
.get(CONTENT_TYPE)
+1 -1
View File
@@ -185,7 +185,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.28.0</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.28.1</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
html.into_bytes()
}
+1 -1
View File
@@ -202,7 +202,7 @@ async fn empty_index_can_go_directly_to_directory_listing() {
let body = std::str::from_utf8(&body).unwrap();
assert!(body.contains("file.txt"));
assert!(body.contains("DIRECTORY LISTING"));
assert!(body.contains("RustProxy <b>0.28.0</b>"));
assert!(body.contains("RustProxy <b>0.28.1</b>"));
}
#[tokio::test]
+41 -12
View File
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
preflight(root)
manifest = tomllib.loads((root / "Cargo.toml").read_text())
assert manifest["package"]["version"] == "0.28.0"
assert manifest["package"]["version"] == "0.28.1"
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.28.0"' in lock
assert 'name = "rust-proxy"\nversion = "0.28.1"' 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.28.0",
"CHANGELOG.md": "## 0.28.0",
"ROADMAP.md": "## 0.28.0",
"AGENTS.md": "Current source version: 0.28.0.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.28.0",
"README.md": "# RustProxy 0.28.1",
"CHANGELOG.md": "## 0.28.1",
"ROADMAP.md": "## 0.28.1",
"AGENTS.md": "Current source version: 0.28.1.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.28.1",
}.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"
@@ -284,7 +284,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.28.0"', '"/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.28.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']:
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
@@ -589,7 +589,8 @@ for marker in ['https://git.linuxiarz.pl/gru/RustProxy', 'https://rustproxy.linu
for language in ['langs/en_US.json', 'langs/pl_pl.json']:
language_text = (root / language).read_text(encoding='utf-8')
assert './dev.sh tls deploy' not in language_text
assert 'rust-proxy --data-dir /var/lib/rustproxy tls deploy' in language_text
assert '/api/certificates/{certificate_id}/reload' in language_text
assert '/etc/letsencrypt/live/example.com/fullchain.pem' in language_text
startup_source = rust_source(root, 'src/main/admin_runtime.rs')
for marker in ['https://git.linuxiarz.pl/gru/RustProxy', 'https://rustproxy.linuxiarz.pl/']:
assert marker in startup_source, marker
@@ -1393,7 +1394,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
assert '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.28.0"' in openapi
assert '"RuntimeInformation"' in openapi and '"version": "0.28.1"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs")
@@ -1479,7 +1480,7 @@ for marker_0273 in ['geoip-form', 'geoip_updates', '268435456', '86400']:
assert marker_0273 in settings_web_0273 or marker_0273 in html, marker_0273
assert '"geoip"' in openapi and 'GeoIpUpdateSettings' in openapi
# 0.28.0 operator controls and cache-policy regressions.
# 0.28.0 retained operator controls and cache-policy regressions.
geoip_tests_0280 = (root / "src/geoip/update_tests.rs").read_text()
cache_settings_0280 = (root / "src/cache/model/settings.rs").read_text()
cache_entry_0280 = (root / "src/cache/model/entry.rs").read_text()
@@ -1515,5 +1516,33 @@ assert 'listener-active-item' in runtime_web_0280 and 'listener-active-feature'
assert 'cache-via-enabled' in cache_web_0280 and 'via_header:' in cache_web_0280
assert 'location-header-direction location-header-editor' in vhost_core_0280
print("PASS: 0.28.0 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
# 0.28.1 external certificate paths and certificate-source UX.
tls_types_0281 = (root / "src/tls/types.rs").read_text()
tls_server_0281 = (root / "src/tls/server_config.rs").read_text()
tls_tests_0281 = (root / "src/tls/tests.rs").read_text()
backup_snapshot_0281 = (root / "src/backup/snapshot.rs").read_text()
certificate_admin_0281 = (root / "src/admin/certificates.rs").read_text()
for marker_0281 in [
'pub fn certificate_files_are_external',
'cert_external == key_external',
'fs::canonicalize(value)',
'Certbot\'s live/ entries are symlinks',
'read_configured_pem',
'external private key may be owner/group-readable',
]:
assert marker_0281 in tls_types_0281, marker_0281
assert 'certificate_files_are_external(cert, key)?;' in tls_server_0281
assert 'external_absolute_paths_follow_certbot_style_symlinks_on_each_reload' in tls_tests_0281
assert 'certificate_files_are_external(' in backup_snapshot_0281 and 'External certificate material is intentionally outside' in backup_snapshot_0281
assert 'uses external file paths; renew those files in place and call the certificate reload endpoint instead of deploy' in certificate_admin_0281
for marker_0281 in [
'id="certificate-file-source"',
'certificateSourceManaged',
'certificateSourceExternal',
'/etc/letsencrypt/live/example.com/fullchain.pem',
]:
assert marker_0281 in html or marker_0281 in runtime_web_0280 or marker_0281 in (root / 'langs/en_US.json').read_text(), marker_0281
assert 'Zewnętrzne ścieżki plików' in (root / 'langs/pl_pl.json').read_text()
print("PASS: 0.28.1 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
+6 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Rust-independent documentation/Reader consistency checks for the 0.28.0 release."""
"""Rust-independent documentation/Reader consistency checks for the 0.28.1 release."""
from pathlib import Path
import re
from rust_source import rust_source
@@ -14,7 +14,7 @@ required = [
DOCS / "README.md",
DOCS / "WEB_SERVER.md",
DOCS / "TODO.md",
DOCS / "V0.28.0_IMPLEMENTATION.md",
DOCS / "V0.28.1_IMPLEMENTATION.md",
DOCS / "V0.27.3_IMPLEMENTATION.md",
DOCS / "V0.27.2_IMPLEMENTATION.md",
DOCS / "V0.27.1_IMPLEMENTATION.md",
@@ -61,16 +61,16 @@ 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.28.0")
assert readme.startswith("# RustProxy 0.28.1")
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.28.0" in roadmap and "## 0.27.3" in roadmap and "## 0.27.2" in roadmap and "## 0.27.1" in roadmap and "## 0.27.0" in roadmap and "## 0.26.7" in roadmap and "## 0.26.6" in roadmap and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
assert roadmap.startswith("# RustProxy roadmap") and "## 0.28.1" in roadmap and "## 0.27.3" in roadmap and "## 0.27.2" in roadmap and "## 0.27.1" in roadmap and "## 0.27.0" in roadmap and "## 0.26.7" in roadmap and "## 0.26.6" in roadmap and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
assert changelog.startswith("# Changelog\n")
for release in ["0.28.0", "0.27.3", "0.27.2", "0.27.1", "0.27.0", "0.26.7", "0.26.6", "0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
for release in ["0.28.1", "0.28.0", "0.27.3", "0.27.2", "0.27.1", "0.27.0", "0.26.7", "0.26.6", "0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
assert f"## {release}" in changelog, release
assert changelog.index("## 0.28.0") < changelog.index("## 0.27.3") < changelog.index("## 0.27.2") < changelog.index("## 0.27.1") < changelog.index("## 0.27.0") < changelog.index("## 0.26.7") < changelog.index("## 0.26.6") < changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
assert changelog.index("## 0.28.1") < changelog.index("## 0.28.0") < changelog.index("## 0.27.3") < changelog.index("## 0.27.2") < changelog.index("## 0.27.1") < changelog.index("## 0.27.0") < changelog.index("## 0.26.7") < changelog.index("## 0.26.6") < changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
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}"
+2 -2
View File
@@ -75,10 +75,10 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
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/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]:
if not (root / path).is_file():
raise ValueError(f"Incomplete RustProxy 0.28.0 source tree: missing {path}")
raise ValueError(f"Incomplete RustProxy 0.28.1 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.28.0 entry point; extract the complete package, not individual files.")
raise ValueError("src/lib.rs is not the 0.28.1 entry point; extract the complete package, not individual files.")
return messages
+7 -6
View File
@@ -127,7 +127,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.28.0"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert 'version = "0.28.1"' 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()
@@ -138,7 +138,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.28.0"
assert manifest["to_version"] == "0.28.1"
assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists()
assert not (backup / "legacy-data").exists()
@@ -430,7 +430,8 @@ def ui_contract_smoke() -> None:
for language in ['langs/en_US.json', 'langs/pl_pl.json']:
text = (ROOT / language).read_text(encoding='utf-8')
assert './dev.sh tls deploy' not in text
assert 'rust-proxy --data-dir /var/lib/rustproxy tls deploy' in text
assert '/api/certificates/{certificate_id}/reload' in text
assert '/etc/letsencrypt/live/example.com/fullchain.pem' in text
startup_source = (ROOT / 'src/main/admin_runtime.rs').read_text(encoding='utf-8')
for marker in ['https://git.linuxiarz.pl/gru/RustProxy', 'https://rustproxy.linuxiarz.pl/']:
assert marker in startup_source, marker
@@ -609,7 +610,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.28.0"' in openapi_0241
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.28.1"' 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
@@ -1099,7 +1100,7 @@ def source_contract_smoke() -> None:
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")
assert '"/api/config/history/restore"' in openapi
for marker in ['"version": "0.28.0"', '"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.28.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"']:
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"']:
@@ -1304,7 +1305,7 @@ def main() -> None:
source_contract_smoke()
container_contract_smoke()
grafana_contract_smoke()
print("PASS: 0.28.0 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
print("PASS: 0.28.1 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
if __name__ == "__main__":
+1 -1
View File
@@ -96,7 +96,7 @@ need(web,
'HeaderValue::from_static("no-store")',
"builtin_error_response(StatusCode::FORBIDDEN, &method)",
"empty_index_can_go_directly_to_directory_listing",
"RustProxy <b>0.28.0</b>",
"RustProxy <b>0.28.1</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",