This commit is contained in:
Mateusz Gruszczyński committed 2026-10-10 11:35:04 +02:00
1 parent f3dd34dae4
commit f213d853e2
28 files changed
+795 -119

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. Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.30.0. compatibility. Current source version: 0.30.1.
## Delivery ## Delivery
+7
View File
@@ -4,6 +4,13 @@
> test-only changes and packaging implementation notes were moved to > test-only changes and packaging implementation notes were moved to
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md). > [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.30.1
- Fix protocol chart legend dots for HTTP and WebSocket (H1/H2/H3) so their colors always follow the configured line palette.
- Remove ACME mode and certificate Python diagnostics from About; provide **Settings -> System information** with live OS/process/hardware information, existing runtime/data paths, and actual selected Python/pip/Certbot versions.
- Use the existing `sysinfo` library for cross-platform OS/kernel identification instead of Linux-only parsers. Read bounded Python `pyvenv.cfg` and installed pip/Certbot `.dist-info` metadata without launching diagnostic subprocesses; report missing or ambiguous versions as unavailable. Improve Windows `.exe` tool discovery. Keep application settings, SQLite schema 9 and Cold Cache format 3 compatible.
- Rust compilation/live acceptance remain required on a Rust-equipped target.
## 0.30.0 ## 0.30.0
- Place per-backend health-check editing in the vhost **Health** tab with a selector containing only monitored backends. Keep independent edits when switching, and preserve overrides while health monitoring is disabled. Existing vhost JSON stays compatible. - Place per-backend health-check editing in the vhost **Health** tab with a selector containing only monitored backends. Keep independent edits when switching, and preserve overrides while health monitoring is disabled. Existing vhost JSON stays compatible.
Generated
+33 -1
View File
@@ -1270,6 +1270,15 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "ntapi"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "nu-ansi-term" name = "nu-ansi-term"
version = "0.50.3" version = "0.50.3"
@@ -1294,6 +1303,25 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.4" version = "1.21.4"
@@ -1688,7 +1716,7 @@ dependencies = [
[[package]] [[package]]
name = "rust-proxy" name = "rust-proxy"
version = "0.30.0" version = "0.30.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
@@ -2147,6 +2175,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f" checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f"
dependencies = [ dependencies = [
"libc", "libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows", "windows",
] ]
+2 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rust-proxy" name = "rust-proxy"
version = "0.30.0" version = "0.30.1"
edition = "2021" edition = "2021"
rust-version = "1.88" rust-version = "1.88"
description = "RustProxy - independent reverse proxy with local administration" description = "RustProxy - independent reverse proxy with local administration"
@@ -58,7 +58,7 @@ h3 = { package = "hpx-h3", version = "=2.5.19", features = ["quinn"] }
quinn = { version = "0.11.12", default-features = false, features = ["runtime-tokio", "rustls-ring"] } quinn = { version = "0.11.12", default-features = false, features = ["runtime-tokio", "rustls-ring"] }
metrics-process = { version = "=2.4.3", default-features = false, features = ["dummy"] } metrics-process = { version = "=2.4.3", default-features = false, features = ["dummy"] }
sysinfo = { version = "=0.38.4", default-features = false, features = ["network"] } sysinfo = { version = "=0.38.4", default-features = false, features = ["network", "system"] }
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "macos", target_os = "windows"))'.dependencies] [target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "macos", target_os = "windows"))'.dependencies]
systemstat = "=0.2.7" systemstat = "=0.2.7"
+10 -3
View File
@@ -1,9 +1,9 @@
# RustProxy 0.30.0 # RustProxy 0.30.1
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration, Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools. streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
**0.30.0 release source.** **0.30.1 release source.**
## Layer-4 streams ## Layer-4 streams
@@ -11,7 +11,14 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners. Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
## What changed in 0.30.0 ## What changed in 0.30.1
- Match H1/H2/H3 legend markers to the corresponding protocol-chart line colors across chart palettes.
- Keep **About** focused on the product, author, license and source/package links. Move Python/ACME diagnostics out of the About dialog.
- Add **Settings -> System information** (`/settings?tab=system-info`), showing OS release/kernel/architecture where available, CPU, RAM, uptime, process metrics, ACME interpreter/pip/Certbot versions where discoverable from local metadata, ACME mode/state, and data paths. OS/kernel details come from the existing `sysinfo` Rust library. Version display reads bounded `pyvenv.cfg` and Python `.dist-info/METADATA` files at ACME initialization; it does not execute diagnostic commands. Some system-installed tools lack these metadata files, so their versions will be shown as unavailable rather than guessed. Operational ACME checks and certificate issuance still run Python/Certbot as required.
- The authenticated status API exposes the extra read-only runtime fields; SQLite schema 9, Cold Cache format 3 and existing configurations are unchanged. Real Rust compilation and live integration verification are still required.
## Previously in 0.30.0
- Edit individual active health-check policies in **Vhosts → Health**. The backend selector lists only members with monitoring enabled in **Backends**. Choose a member and optionally customize its TCP/HTTP mode, path, Host, interval, timeout, thresholds and expected status range. Other backends retain their unsaved edits; disabling monitoring hides its editor but preserves the saved override for future re-enablement. No API or SQLite schema changes. - Edit individual active health-check policies in **Vhosts → Health**. The backend selector lists only members with monitoring enabled in **Backends**. Choose a member and optionally customize its TCP/HTTP mode, path, Host, interval, timeout, thresholds and expected status range. Other backends retain their unsaved edits; disabling monitoring hides its editor but preserves the saved override for future re-enablement. No API or SQLite schema changes.
- Improve sustained-memory handling: reclaim selective PURGE-generation bookkeeping, avoid unnecessary analytics-label allocations, reuse serialized access JSON across JSONL/Logstash/Loki, and avoid duplicate directory listing vectors/HTML buffers. Stored Logs discovery retains at most 256 per-vhost and 64 rotated-gzip candidates. See [memory stress verification](docs/MEMORY_STRESS.md) for the audit and real-world RSS/load test procedure; source-only checks cannot rule out runtime leaks. - Improve sustained-memory handling: reclaim selective PURGE-generation bookkeeping, avoid unnecessary analytics-label allocations, reuse serialized access JSON across JSONL/Logstash/Loki, and avoid duplicate directory listing vectors/HTML buffers. Stored Logs discovery retains at most 256 per-vhost and 64 rotated-gzip candidates. See [memory stress verification](docs/MEMORY_STRESS.md) for the audit and real-world RSS/load test procedure; source-only checks cannot rule out runtime leaks.
+7
View File
@@ -1,5 +1,12 @@
# RustProxy roadmap # RustProxy roadmap
## 0.30.1 - Protocol legend and system information
- [x] Color the H1/H2/H3 legend dots consistently with their themed chart strokes.
- [x] Remove ACME/Python runtime details from About and provide a read-only System information settings tab.
- [x] Read available ACME Python/pip/Certbot version metadata and OS/hardware/process details with existing libraries, without subprocesses for information gathering or new configuration fields.
- [ ] Compile and run Rust tests, verify OS/kernel and metadata-based ACME versions on Linux, BSD, macOS and Windows, and perform live UI/status checks before production acceptance.
## 0.30.0 - Per-backend Health editor and logging resource review ## 0.30.0 - Per-backend Health editor and logging resource review
- [x] Provide an intuitive Health-tab backend selector restricted to monitored members, with independently editable TCP/HTTP policies. - [x] Provide an intuitive Health-tab backend selector restricted to monitored members, with independently editable TCP/HTTP policies.
+27 -24
View File
@@ -1,18 +1,18 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
14cfdff48d3df448f246af54f80ddb5c4dc906f824fab7e6c219406243714cc3 AGENTS.md 2af2dd8a13e8e1f16639fbca5b72ad0d82f6abf3a2e3ac146341a6a007547904 AGENTS.md
793c2a2ebd9168745f39559d2ac4658517d407d578beee54d9c70594cb75c3c9 CHANGELOG.md 64179555a0e536fa58e4e265ad98cdd09e3809b81cdf19ad0ba8609c35d28ddc CHANGELOG.md
fc20847abf0b96bdcb54ddbfb1df4b134301f15fa2cbfd4ea11dd4d368296484 Cargo.lock e0409a737959eb4b8122efa279401eda5b3b78155a6e9d2a4fdc891bfb4a1754 Cargo.lock
d5d77528a9703ee0a0a0c736267f856a7caaf9400a9b9c6a411589feea7a18c3 Cargo.toml 125a8a3c3f785675190518b398f64aee12fb0c82120d29be61012865391b3636 Cargo.toml
75b008f237b2226c62a7721b7901018ce83bfc2726f047a83a17c66fcb799f3b Dockerfile 75b008f237b2226c62a7721b7901018ce83bfc2726f047a83a17c66fcb799f3b Dockerfile
39e7600cbf426c2f780b58b6d9c9f26726388b34146a38af815c7ba933d61dd7 README.md b8aca9e4be342f43b64a2eb04522c6582243edbfb764457afdf58ee894131480 README.md
0a5d19aa357cb6645ee6b48926201155510fabf3c7da07c90446f9e8b405a82e ROADMAP.md 40f21bcecdf85e951d07059fe5bd5a49b844a9f22e943410534a0d8b308a26ac ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service 4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
2661e66c34cdfcd6d6460b5c6e8194724d17d98b4fdb9e2c4020d9ff3087479c dev.sh 550197e512e5a3b3c1dec66f69ab31368e82a317dfac653ecd60b11dac45195e dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md 5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go 7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example 5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -32,7 +32,7 @@ f054206a09bed81018708608cdeecbfd09018ee7cec9d3abc038f8a37d47b815 docs/API_ACCES
4c5288c9ed427cfb919bd023ed9a321dcf6d64bc4cad3e1768393356c5d835ba docs/ARCHITECTURE.md 4c5288c9ed427cfb919bd023ed9a321dcf6d64bc4cad3e1768393356c5d835ba docs/ARCHITECTURE.md
598298303036912c19e0a4be1431acc7b7d3df7333f3e756e8268ac0e8c16921 docs/CACHE.md 598298303036912c19e0a4be1431acc7b7d3df7333f3e756e8268ac0e8c16921 docs/CACHE.md
9113f3abbfe81bafc4855d8680df8aa2a381ecc358cc3e1a0f78b6b756421117 docs/CACHE_DIAGNOSTICS.md 9113f3abbfe81bafc4855d8680df8aa2a381ecc358cc3e1a0f78b6b756421117 docs/CACHE_DIAGNOSTICS.md
d069a1ffcc2c791ca0ea0a26b2533b694db1ff48f416e6e7a5525236da3f8cbe docs/CHANGELOG_TECHNICAL_ARCHIVE.md 29cc1237ab205fdbe1cc0afc0e905c211048d9b4de2c349d1e4a8427bd53c001 docs/CHANGELOG_TECHNICAL_ARCHIVE.md
c8e69e492179ce39958cc63615cf46c4ac08cfdb7f3483aaab66868a3abcd687 docs/CONTAINER.md c8e69e492179ce39958cc63615cf46c4ac08cfdb7f3483aaab66868a3abcd687 docs/CONTAINER.md
befa50579e3475b22589f26380d85b3345b80e4fa947d49649a3b91a8dd8b8c2 docs/CUSTOM_LOCATIONS.md befa50579e3475b22589f26380d85b3345b80e4fa947d49649a3b91a8dd8b8c2 docs/CUSTOM_LOCATIONS.md
16d8f59908c66e595d6003495cbf5c068c68b10aa9495eb8612c38b2de0e4a7c docs/DOCKER_DISCOVERY.md 16d8f59908c66e595d6003495cbf5c068c68b10aa9495eb8612c38b2de0e4a7c docs/DOCKER_DISCOVERY.md
@@ -60,9 +60,9 @@ a2bb837cdd3559d58158880c1f832badf64035376a3623050bb4e40c2bca104c docs/ROADMAP_H
759bf2987f2b602488829d1856fa33d57c2a3b23bc681ef5a931b9633af3a533 docs/SECRET_STORE_BACKUP.md 759bf2987f2b602488829d1856fa33d57c2a3b23bc681ef5a931b9633af3a533 docs/SECRET_STORE_BACKUP.md
5dbdf45fddba198a28f511dbbd2f12bf7d2b88c41c3cbedbc20dfea3858779d3 docs/SECURITY.md 5dbdf45fddba198a28f511dbbd2f12bf7d2b88c41c3cbedbc20dfea3858779d3 docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md 620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
d5c5c59f6e63b8d83dad5d159657c4d4039dfeb4cc1d8af210169b576adbf7e2 docs/SETTINGS.md 4c584a657606f0423d40670b72c8c95c814368be4f6635b4d6adc8c729780d0f docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md 3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
b4c88af2c27c8dea853ea55bc451ca4cca2a59ec086b59e52ade1227794ac097 docs/TEST_REPORT.md ff24c09a3bde9607001fdc49acbb71b413a1c6479c8a59252da821492d7593f5 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md 751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
660687fc961f71aae2bdf71595aa47a336fa782f4e78ceee8c1d1e63e46393ee docs/TRANSPORT.md 660687fc961f71aae2bdf71595aa47a336fa782f4e78ceee8c1d1e63e46393ee docs/TRANSPORT.md
@@ -134,8 +134,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh 96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh 3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md 71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md
400b61050ff437aa8476f05a9cd05be6f3b8b2853ddf563591712d118bc95199 langs/en_US.json 86b95d536c1e81883f9e4002c7e72f19976436d5ece285cb103432b195ac648f langs/en_US.json
2b4564cfdacfc0ad7e5ec1554bae3af09842a77a45cece7652044506787c8135 langs/pl_pl.json 6a4e4fd967d29c38ee7e9a373004db85400bb8a1ecb51e1842f2da93b6247353 langs/pl_pl.json
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
@@ -172,8 +172,9 @@ ea272433e7f92e983509a8ab1aa54242440abef21df526c2f99944ef279e09b9 src/acl/tests.
c5334de6c732d76b2bea7bfc17b3ac305a250bbaad31a5420d6a7d7abb200449 src/acme/providers.rs c5334de6c732d76b2bea7bfc17b3ac305a250bbaad31a5420d6a7d7abb200449 src/acme/providers.rs
b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/providers/tests.rs b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/providers/tests.rs
29ff0647417b0f1d50a4cc075ab51afecff9ddf47db3eb7948d51db5a38f33f3 src/acme/runtime_bootstrap.rs 29ff0647417b0f1d50a4cc075ab51afecff9ddf47db3eb7948d51db5a38f33f3 src/acme/runtime_bootstrap.rs
c8941a929dfef9e8875adf534394ca01a1532e567d5099fefe3657826886ee25 src/acme/runtime_env.rs 87f64198770ae026c03a242fa542db2590ca734b02e756f1e5c28d66236f753a src/acme/runtime_env.rs
7acbaf34e7b953b753ab074893e47b5024f0cd756dc4c7a1074067dd936c2e8e src/acme/tests.rs f8075a5ef9ae5651d72a33ac0f0fd5b44ad0ae30f6e2d2318412177e0ed837f5 src/acme/runtime_versions.rs
e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests.rs
bc9eb32f0d2785bac122d5eff44742cd24306c503daa065564383d46416b2211 src/admin/access.rs bc9eb32f0d2785bac122d5eff44742cd24306c503daa065564383d46416b2211 src/admin/access.rs
019f4aa33e3596bc9c11ef8cf5703bcfffe46c15288c0eba83c8dd9e38055fb2 src/admin/certificates.rs 019f4aa33e3596bc9c11ef8cf5703bcfffe46c15288c0eba83c8dd9e38055fb2 src/admin/certificates.rs
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs 1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
@@ -184,13 +185,13 @@ f84cdf7dc40553b6c5086446bafd5c700496f048230ebd95a01839858adff071 src/admin/docu
d7aa68a78a4cb5a61342d92c1f3209af230d0f9ca3fea1115f67078372e6f37c src/admin/file_logs_io.rs d7aa68a78a4cb5a61342d92c1f3209af230d0f9ca3fea1115f67078372e6f37c src/admin/file_logs_io.rs
b3e2c11dbc11bb8930117afdac64779111acc2ca7b2857f6e359a5108fc9ced8 src/admin/logging_api.rs b3e2c11dbc11bb8930117afdac64779111acc2ca7b2857f6e359a5108fc9ced8 src/admin/logging_api.rs
58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs 58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs
13aa83f3a10b19205c63349c964b2e86322c1d0486c2d1d52429080ac9d9d401 src/admin/openapi.json 53347ce9fcb8ce297ff3565ac3d2af5060a56eb6fff51eeac4791c0bd9f5b296 src/admin/openapi.json
6c2b9a0ecb2f4172f110a363c22a6be32895dbcff9605fe7aa94efe91e0a536a src/admin/operations.rs 6c2b9a0ecb2f4172f110a363c22a6be32895dbcff9605fe7aa94efe91e0a536a src/admin/operations.rs
5287fff6b3ad436ac695e9b66feb1b0b5705b744a9f00cec7ea6a3e951e5eebf src/admin/preferences.rs 5287fff6b3ad436ac695e9b66feb1b0b5705b744a9f00cec7ea6a3e951e5eebf src/admin/preferences.rs
31bd90ee5f5ec2984920225ff6b57e07db3e0758092fb55d7c230e3590c1b015 src/admin/public.rs 31bd90ee5f5ec2984920225ff6b57e07db3e0758092fb55d7c230e3590c1b015 src/admin/public.rs
9edf51cfbeb51ec5062fa24803247f6117a45dfe9ac731215470c3d034296020 src/admin/router.rs 9edf51cfbeb51ec5062fa24803247f6117a45dfe9ac731215470c3d034296020 src/admin/router.rs
8708427263cd4caf8454b9115e89acb09172483a090c6b0bd43811dd9e52c7f6 src/admin/session.rs 8708427263cd4caf8454b9115e89acb09172483a090c6b0bd43811dd9e52c7f6 src/admin/session.rs
bebf42cd1808a99a0e669d483951a441ffa22c8d5a0a289c64491adf13c8841a src/admin/status.rs c7dd05f83efccb8df5a8e4ea22bda072643e9d9cd52610d4c8a68f072270903b src/admin/status.rs
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
5a8e2c4d0065c98f4686105c6f00362dd515f70f1ed662187f943ba4b76c54ad src/admin/tests.rs 5a8e2c4d0065c98f4686105c6f00362dd515f70f1ed662187f943ba4b76c54ad src/admin/tests.rs
5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs 5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs
@@ -203,7 +204,7 @@ cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail
54ed2130e85239ffa4576417dd68fbb9349e396210b75f5da862897137d7e876 src/admin/tests/part_08.rs 54ed2130e85239ffa4576417dd68fbb9349e396210b75f5da862897137d7e876 src/admin/tests/part_08.rs
2ae5bc857dd62882f16d1cfcee9691c9ba8afc149f4c0435addd48b564487517 src/admin/web/acme.js 2ae5bc857dd62882f16d1cfcee9691c9ba8afc149f4c0435addd48b564487517 src/admin/web/acme.js
446011c2c7afddf7734ceb268c75a3b4146dbe8e915b11986495e956b6c62c69 src/admin/web/app/config-history.js 446011c2c7afddf7734ceb268c75a3b4146dbe8e915b11986495e956b6c62c69 src/admin/web/app/config-history.js
ed063b8a414cf3b5e539c900e8722aa82d84c561a5cd54d875650793a25529e2 src/admin/web/app/core.js bd55186a8b02507cffe9fbd2dfb3cc5d3c54509a2a13853b5d93c5f6ec76e688 src/admin/web/app/core.js
cb5c4c2dd4964795b61393b05537cc1528c72d7c1390953f4d119a5e55c65c7c src/admin/web/app/preferences.js cb5c4c2dd4964795b61393b05537cc1528c72d7c1390953f4d119a5e55c65c7c src/admin/web/app/preferences.js
de17987bda89dfab54b6751007474ff9e7cc8da35f7e9c46a2eff922a50fb085 src/admin/web/app/runtime.js de17987bda89dfab54b6751007474ff9e7cc8da35f7e9c46a2eff922a50fb085 src/admin/web/app/runtime.js
887e34c7e4b3e197e54151f4bb48932e4f7b9f8e64e3d3e8c9c7ee0835fbe4b0 src/admin/web/app/settings.js 887e34c7e4b3e197e54151f4bb48932e4f7b9f8e64e3d3e8c9c7ee0835fbe4b0 src/admin/web/app/settings.js
@@ -223,16 +224,16 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
62fe27711ced37f0c4908c84b42ab29336ac2af207fecf5c7eb5557c30ce2868 src/admin/web/index.html 8a7f5ba3a7d5f0810351a540b829377b8a4a20fd40f6a955529e2d2839ffe328 src/admin/web/index.html
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js 24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
af0fbd94d9b88ce1cce28e39dfd2c35de031adcba412dd4a9a17d9b416c256dd src/admin/web/login.html af0fbd94d9b88ce1cce28e39dfd2c35de031adcba412dd4a9a17d9b416c256dd src/admin/web/login.html
99fe9b8ba7a8a24e5bbe9ae40999484fe13b0faf5eb1d7a9193701f667e51ca5 src/admin/web/login.js 99fe9b8ba7a8a24e5bbe9ae40999484fe13b0faf5eb1d7a9193701f667e51ca5 src/admin/web/login.js
810e7e5bf1af302b51d90771c3ec3c20227eca392f8c7d91d027e11a999bc236 src/admin/web/management.js 810e7e5bf1af302b51d90771c3ec3c20227eca392f8c7d91d027e11a999bc236 src/admin/web/management.js
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js 90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
7fc4c3661524f11448bc43ebe8f494ed44b8b0a414e63ce5bb8717f314c33808 src/admin/web/settings.js f5ba448163d462ab1f0d421686d1af7ae465b1e5bd78e38fc0899cf0f6f5d13f src/admin/web/settings.js
bb67e77af5514478485cfb69f339d63d7eb0cbadd093fa054814a416260ccc1d src/admin/web/streams.js bb67e77af5514478485cfb69f339d63d7eb0cbadd093fa054814a416260ccc1d src/admin/web/streams.js
db0ea03125967ea35c4fa6a4d9284fa5dcc5782cd655fcdbc4254ce3e24b7133 src/admin/web/style.css b12ce34bcd2617b52bb38f9dfe9be5e1f5c11304c6c18b77a7cb379fa1cafd88 src/admin/web/style.css
a8a73a4fa849ae49391fcd6143e900c515c8b577f71b55cbae9701fa9f5bbe47 src/admin/web/traffic-panel.js a8a73a4fa849ae49391fcd6143e900c515c8b577f71b55cbae9701fa9f5bbe47 src/admin/web/traffic-panel.js
e686d68fd6a368110a85dc4c5dc64e8093950d20d63e9aa44790c03dea6fc193 src/admin/web/transform-panel.js e686d68fd6a368110a85dc4c5dc64e8093950d20d63e9aa44790c03dea6fc193 src/admin/web/transform-panel.js
57a2b975716bdab32c33ec1bf9427b25998550e81ce191440ee66ffb7e2a249e src/admin/web/vhosts/core.js 57a2b975716bdab32c33ec1bf9427b25998550e81ce191440ee66ffb7e2a249e src/admin/web/vhosts/core.js
@@ -503,18 +504,20 @@ b04362ad082a32c9ccc4cf38c46f44d71347adecf7aba3224b3af3ea4872e3d0 tools/README.m
358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py 358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py
412255d383fcc8306c6086784fb416d9814e3ec790e440a0927431ca615f3411 tools/checks/backend_cache_0297_ui_test.mjs 412255d383fcc8306c6086784fb416d9814e3ec790e440a0927431ca615f3411 tools/checks/backend_cache_0297_ui_test.mjs
2a493dbce2991a23d807c01952f58608a364f541560836b4cb385e75e108e9e7 tools/checks/backend_health_0300_ui_test.mjs 2a493dbce2991a23d807c01952f58608a364f541560836b4cb385e75e108e9e7 tools/checks/backend_health_0300_ui_test.mjs
e36b2774f9e2510c760d885cb116bda1e9a14a13f477c96c250f3532258add8f tools/checks/check_source.py 84c090840926fb8fc5ed74aa779e7ca028bc0c684ca5ca15f5ae105e65a8dd3b tools/checks/check_source.py
297dc71c84b5ead2b6e7cef6469596854f5d5e21a83d6b9b3861ea45cf23f672 tools/checks/docs_check.py 5362f6c455cf697aacb59336d6664df0fd2a36956bedbbdcb090887ff20b7049 tools/checks/docs_check.py
e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py 4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
f8f87ba891338b565249d02c3cf5f02cec44e606b4205de99fcbea1eb2c24d57 tools/checks/memory_contracts.py f8f87ba891338b565249d02c3cf5f02cec44e606b4205de99fcbea1eb2c24d57 tools/checks/memory_contracts.py
456f92623672ec6a7e891e4a642cc6f0b75c38f930e698d9401e3fd906cb3bc6 tools/checks/preflight.py c2e9b21e6c4acdff5d846843fbf7a3babeeed10b9b59ed001c519feea815d90e tools/checks/preflight.py
2478e8dbb056830ddc6b39abc4bdf97978b6024973c59f21fa933992dfd8b86b tools/checks/release_smoke.py 32ae78d788aa841e2c01b792aab9c2cf3f075a1508f8abcde4499153c5f778df tools/checks/release_smoke.py
f69845bc26ec33160fc756953991cf32aa8ad6a7fb4a131caa2093db91e783ef tools/checks/rust_module_contracts.py f69845bc26ec33160fc756953991cf32aa8ad6a7fb4a131caa2093db91e783ef tools/checks/rust_module_contracts.py
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py
adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py
23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py 23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py
2e125a2a807553ac81d04428f337f5042eed968cb807244487114fdd8ea6b5d9 tools/checks/source_module_contracts.py 2e125a2a807553ac81d04428f337f5042eed968cb807244487114fdd8ea6b5d9 tools/checks/source_module_contracts.py
37dafd2db24beba50fa155c302299dc4a8a8171531f86ac5b4e53bb0e269c1f7 tools/checks/system_info_0301_ui_test.mjs
7505e10a00969ea705987dd68c6ba6f5c556e0b2d5bde9926ac4f41e72381700 tools/checks/system_info_no_subprocess_contracts.py
987b4c593b51a7c340d8f01f9a2eea1387043cdd997eeb932c8341bbb8c0bd64 tools/checks/version_contracts.py 987b4c593b51a7c340d8f01f9a2eea1387043cdd997eeb932c8341bbb8c0bd64 tools/checks/version_contracts.py
2020e171be1a044f7ca2ab83af7604a014c36fe4ff04791d0620ccbf1bf8e000 tools/checks/vhost_origin_errors_0297_ui_test.mjs 2020e171be1a044f7ca2ab83af7604a014c36fe4ff04791d0620ccbf1bf8e000 tools/checks/vhost_origin_errors_0297_ui_test.mjs
0c071c26fc316934bdfd2bfd19f5fed9521790d8c887760eaabda81a9be96703 tools/checks/vhost_www_ui_test.mjs 0c071c26fc316934bdfd2bfd19f5fed9521790d8c887760eaabda81a9be96703 tools/checks/vhost_www_ui_test.mjs
+2
View File
@@ -90,6 +90,7 @@ python3 tools/checks/preflight.py --repair-known
if [[ "$mode" == "repair" ]]; then printf 'Source preflight: OK\n'; exit 0; fi if [[ "$mode" == "repair" ]]; then printf 'Source preflight: OK\n'; exit 0; fi
if [[ "$mode" == "test" ]]; then if [[ "$mode" == "test" ]]; then
python3 tools/checks/rust_module_contracts.py python3 tools/checks/rust_module_contracts.py
python3 tools/checks/system_info_no_subprocess_contracts.py
python3 tools/checks/source_module_contracts.py python3 tools/checks/source_module_contracts.py
python3 tools/checks/version_contracts.py python3 tools/checks/version_contracts.py
python3 tools/checks/check_source.py python3 tools/checks/check_source.py
@@ -103,6 +104,7 @@ if [[ "$mode" == "test" ]]; then
node tools/checks/web_service_ui_test.mjs node tools/checks/web_service_ui_test.mjs
node tools/checks/backend_cache_0297_ui_test.mjs node tools/checks/backend_cache_0297_ui_test.mjs
node tools/checks/backend_health_0300_ui_test.mjs node tools/checks/backend_health_0300_ui_test.mjs
node tools/checks/system_info_0301_ui_test.mjs
node tools/checks/vhost_origin_errors_0297_ui_test.mjs node tools/checks/vhost_origin_errors_0297_ui_test.mjs
node tools/checks/www_redirect_ui_test.mjs node tools/checks/www_redirect_ui_test.mjs
node tools/checks/vhost_www_ui_test.mjs node tools/checks/vhost_www_ui_test.mjs
+6
View File
@@ -7,6 +7,12 @@
# Changelog # Changelog
## 0.30.1 same-version sysinfo compiler feature correction
- Fix four compiler `E0433` errors in the system information endpoint by enabling `sysinfo`'s `system` feature alongside `network` with `default-features = false` unchanged. No dependency version or API change.
- Remove unused Tokio `AsyncReadExt` imported by the ACME runtime, and add the explicit manifest feature check to the offline source regressions.
- The change addresses the supplied Rust compiler log. A full Rust build and multi-OS runtime check remain outstanding in the source packaging environment.
## 0.29.7 same-version `E0308` source correction ## 0.29.7 same-version `E0308` source correction
- Defer Reverse Proxy error interception from the upstream `Response<hyper::body::Incoming>` to the adapted `Response<axum::body::Body>` in `success_prepare.rs`. This preserves the H2 WebSocket tunnel's `Incoming` body type and ensures replacement still precedes response transformation and cache admission. Reapply response rules and enforce `no-store` for replaced application errors. - Defer Reverse Proxy error interception from the upstream `Response<hyper::body::Incoming>` to the adapted `Response<axum::body::Body>` in `success_prepare.rs`. This preserves the H2 WebSocket tunnel's `Incoming` body type and ensures replacement still precedes response transformation and cache admission. Reapply response rules and enforce `no-store` for replaced application errors.
+6
View File
@@ -12,6 +12,12 @@ The Settings page includes a local search field that filters category tabs using
Unfinished Web Server, Vhost, Redirect and Stream forms use a session-only draft dock. Closing a dirty editor minimizes its exact browser state to the bottom-right dock so another editor can be opened independently. The dock follows the square-corner panel styling of the dashboard and intentionally avoids rounded/glass presentation. Drafts are intentionally not written to SQLite because form state can contain credentials or other secrets; they are discarded on logout/session reset. Unfinished Web Server, Vhost, Redirect and Stream forms use a session-only draft dock. Closing a dirty editor minimizes its exact browser state to the bottom-right dock so another editor can be opened independently. The dock follows the square-corner panel styling of the dashboard and intentionally avoids rounded/glass presentation. Drafts are intentionally not written to SQLite because form state can contain credentials or other secrets; they are discarded on logout/session reset.
## System information (0.30.1)
**Settings -> System information** (`/settings?tab=system-info`) is a read-only view of the authenticated `/api/status` endpoint. It reports the OS name/kernel on Linux when available, executable architecture, logical CPU count, CPU and RAM telemetry, host/process uptime, RustProxy version, process RSS, thread and file-descriptor counts, and the existing statistics source and data/database locations. Values not supported by the host/platform display as unavailable, never as zero.
ACME diagnostics formerly shown in About and under Administration are now on this tab: mode/state, certificate environment path, and the active Python/pip/Certbot versions. Versions are probed by the ACME runtime setup with bounded subprocess output/timeouts and published from stored runtime status. They are not queried at every page refresh or status API request. Failed/unavailable probes leave the version empty. No new persistent settings or permissions are introduced.
## Configuration and update semantics ## Configuration and update semantics
GET `/api/config` returns a revisioned envelope. PUT the entire document with GET `/api/config` returns a revisioned envelope. PUT the entire document with
+22 -1
View File
@@ -1,4 +1,25 @@
# Verification report - RustProxy 0.30.0 # Verification report - RustProxy 0.30.1
## 0.30.1 sysinfo compiler-feature fix (2026-10-10)
- The operator's Rust compiler reported four `E0433` errors because the pinned `sysinfo = 0.38.4` dependency enabled only `network`, but the new OS/kernel status methods require the `system` feature. Enabled both `network` and `system` without changing dependency versions or status API behavior.
- Removed the reported unused Tokio `AsyncReadExt` import from `src/acme/runtime_env.rs`.
- Added a guard for both `sysinfo` features to `system_info_no_subprocess_contracts.py`, wired that source check into `./dev.sh test`, and synchronized the older manifest assertions in `check_source.py` and `release_smoke.py`. Cargo compilation on the operator's machine is still required to verify the full Rust build.
- Offline checks passed after the correction: system-info feature/source contracts, Rust and browser module contracts, version/source/release-smoke (SQLite/schema 9 and upgrade), memory, web server, security/dependency, simulated system-info UI, docs/translations, and Bash syntax. These static checks do not establish Rust compile success.
## 0.30.1 no-subprocess system information refinement (2026-10-10)
- Reused `sysinfo::System::{long_os_version, name, os_version, kernel_version}` for OS and kernel metadata on supported Linux/BSD/macOS/Windows targets; results remain cached. The status endpoint no longer reads Linux `/etc/os-release` or `/proc/sys/kernel/osrelease` directly.
- Removed the extra Python, pip and Certbot version subprocesses. At ACME startup, bounded local reads of the selected interpreter's `pyvenv.cfg` and the selected installation's `pip-*.dist-info/METADATA` and `certbot-*.dist-info/METADATA` provide versions when possible; missing/ambiguous values remain `null`. System Python full patch versions cannot be guaranteed without an executable probe, so they may be unavailable. ACME operational dependency checks and actual issuance still require subprocesses; those are **not** system-information collection.
- Added Windows `.exe` lookup for system Python/Certbot, metadata source unit regressions (not executed without Cargo) and compiler-independent source-contract regression `system_info_no_subprocess_contracts.py`. No new Rust dependencies, schema or API property changes.
- Compiler-independent checks passed: `system_info_no_subprocess_contracts.py`, `rust_module_contracts.py`, `source_module_contracts.py`, `version_contracts.py`, `check_source.py`, `release_smoke.py` (SQLite/schema 9 and upgrade), `memory_contracts.py`, `web_server_contracts.py`, `security_contracts.py`, `security_dependency_contracts.py`, `docs_check.py`, `langs_check.py`, all shipped Node UI regressions (including no-metadata fallback) and `high_traffic_memory_model.py` (simulated PURGE + 2,000 requests against a Python peer, **not RustProxy**). Rust/Cargo are absent, so `cargo check`, Rust unit tests and live multi-OS acceptance were not performed; production acceptance remains open.
## 0.30.1 system information and legend change (2026-10-10)
- Corrected H1/H2/H3 Overview legend markers to use exactly the protocol series theme variables (HTTP and WebSocket cards).
- Removed certificate runtime details from About, moved existing authenticated runtime metadata to Settings -> System information, added OS/version/kernel/architecture and existing process telemetry. Initial implementation used bounded ACME version subprocess probes for the selected binaries; the later refinement above replaces them with non-executing metadata reads, and leaves missing versions unavailable.
- Updated locale packs (English/Polish), authenticated OpenAPI status schema and release identity. SQLite schema 9, Cold Cache format 3 and persisted configuration have not changed.
- Passed offline: `rust_module_contracts.py`, `source_module_contracts.py`, `version_contracts.py`, `check_source.py`, `release_smoke.py` (including SQLite/schema 9 and source upgrade), `memory_contracts.py`, `high_traffic_memory_model.py` (simulated PURGE plus 2,000 requests against a Python peer, **not RustProxy**), `web_server_contracts.py`, `security_contracts.py`, `security_dependency_contracts.py`, all seven shipped Node UI regressions (including `system_info_0301_ui_test.mjs`), `docs_check.py` and `langs_check.py`. The full `./dev.sh test` pipeline exceeded this environment's execution limit after passing through the Web Server contracts; remaining checks were run individually. `./dev.sh check` exited 127 because Rust/Cargo are absent. No Rust compilation, Rust unit tests or real managed/system Certbot runtime checks were performed. Production acceptance remains open.
## 0.30.0 same-version sustained-memory audit (2026-10-10) ## 0.30.0 same-version sustained-memory audit (2026-10-10)
+20 -2
View File
@@ -202,7 +202,7 @@
"processMemory": "Process memory", "processMemory": "Process memory",
"systemLoad": "SYSTEM LOAD", "systemLoad": "SYSTEM LOAD",
"systemChart": "Recent system CPU usage", "systemChart": "Recent system CPU usage",
"systemMemory": "System RAM", "systemMemory": "RAM used / total",
"systemSwap": "System swap", "systemSwap": "System swap",
"systemUptime": "System uptime", "systemUptime": "System uptime",
"loadAverage": "Load 1 / 5 / 15 min", "loadAverage": "Load 1 / 5 / 15 min",
@@ -2286,6 +2286,24 @@
"vhostInterceptOriginErrors": "Replace application error bodies with RustProxy error pages", "vhostInterceptOriginErrors": "Replace application error bodies with RustProxy error pages",
"vhostInterceptOriginStatuses": "Intercept HTTP statuses (comma-separated)", "vhostInterceptOriginStatuses": "Intercept HTTP statuses (comma-separated)",
"vhostInterceptOriginErrorsHelp": "Example: 404, 500, 502, 503, 504. For selected codes, RustProxy keeps the HTTP error status and returns the vhost/global custom error page, or a built-in one. Application content/cookies are removed; the replacement is no-store. Off by default. Internal named-location fallbacks take precedence. Applies to reverse proxy and Web Server (including PHP).", "vhostInterceptOriginErrorsHelp": "Example: 404, 500, 502, 503, 504. For selected codes, RustProxy keeps the HTTP error status and returns the vhost/global custom error page, or a built-in one. Application content/cookies are removed; the replacement is no-store. Off by default. Internal named-location fallbacks take precedence. Applies to reverse proxy and Web Server (including PHP).",
"vhostInterceptOriginInvalid": "Select one or more unique supported HTTP codes: 400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504." "vhostInterceptOriginInvalid": "Select one or more unique supported HTTP codes: 400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504.",
"systemInformation": "System information",
"systemInformationHelp": "Live, read-only information from the authenticated status API. Unavailable metrics are not estimated.",
"systemHostHeading": "Operating system and hardware",
"systemOsVersion": "Operating system",
"systemKernel": "Kernel",
"systemArchitecture": "Architecture",
"systemLogicalCpus": "Logical CPUs",
"systemCpuLoad": "CPU usage",
"systemProcessHeading": "RustProxy process",
"systemAppVersion": "RustProxy version",
"systemProcessUptime": "Process uptime",
"systemProcessMemory": "Process RAM (RSS)",
"systemProcessThreads": "Process threads",
"systemOpenFds": "Open file descriptors",
"systemCertificateHeading": "ACME / Python environment",
"systemCertificateHelp": "Versions are detected from the interpreter and tools actually selected for certificate management.",
"systemAcmeStatus": "ACME status",
"systemPathsHeading": "Data locations"
} }
} }
+21 -3
View File
@@ -202,9 +202,9 @@
"processMemory": "Pamięć procesu", "processMemory": "Pamięć procesu",
"systemLoad": "OBCIĄŻENIE SYSTEMU", "systemLoad": "OBCIĄŻENIE SYSTEMU",
"systemChart": "Ostatnie próbki obciążenia CPU", "systemChart": "Ostatnie próbki obciążenia CPU",
"systemMemory": "RAM systemu", "systemMemory": "RAM użyta / łącznie",
"systemSwap": "Swap systemu", "systemSwap": "Swap systemu",
"systemUptime": "Czas działania systemu", "systemUptime": "Czas pracy systemu",
"loadAverage": "Load 1 / 5 / 15 min", "loadAverage": "Load 1 / 5 / 15 min",
"cacheOverview": "Cache RAM", "cacheOverview": "Cache RAM",
"cacheObjects": "Obiekty / pamięć", "cacheObjects": "Obiekty / pamięć",
@@ -2286,6 +2286,24 @@
"vhostInterceptOriginErrors": "Zastępuj treść błędów aplikacji stronami błędów RustProxy", "vhostInterceptOriginErrors": "Zastępuj treść błędów aplikacji stronami błędów RustProxy",
"vhostInterceptOriginStatuses": "Przechwytywane kody HTTP (oddziel przecinkami)", "vhostInterceptOriginStatuses": "Przechwytywane kody HTTP (oddziel przecinkami)",
"vhostInterceptOriginErrorsHelp": "Przykład: 404, 500, 502, 503, 504. Dla wybranych kodów RustProxy zachowa kod błędu, ale wyświetli stronę skonfigurowaną dla vhosta/globalnie albo wbudowaną. Treść i ciasteczka aplikacji zostaną usunięte, a odpowiedź otrzyma no-store. Domyślnie wyłączone. Wewnętrzne lokalizacje błędów mają pierwszeństwo. Działa dla Reverse Proxy i Web Server (także PHP).", "vhostInterceptOriginErrorsHelp": "Przykład: 404, 500, 502, 503, 504. Dla wybranych kodów RustProxy zachowa kod błędu, ale wyświetli stronę skonfigurowaną dla vhosta/globalnie albo wbudowaną. Treść i ciasteczka aplikacji zostaną usunięte, a odpowiedź otrzyma no-store. Domyślnie wyłączone. Wewnętrzne lokalizacje błędów mają pierwszeństwo. Działa dla Reverse Proxy i Web Server (także PHP).",
"vhostInterceptOriginInvalid": "Podaj co najmniej jeden unikalny obsługiwany kod HTTP: 400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504." "vhostInterceptOriginInvalid": "Podaj co najmniej jeden unikalny obsługiwany kod HTTP: 400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504.",
"systemInformation": "Informacje o systemie",
"systemInformationHelp": "Bieżące dane tylko do odczytu z uwierzytelnionego API statusu. Brakujących wartości nie szacujemy.",
"systemHostHeading": "System operacyjny i sprzęt",
"systemOsVersion": "System operacyjny",
"systemKernel": "Jądro systemu",
"systemArchitecture": "Architektura",
"systemLogicalCpus": "Procesory logiczne",
"systemCpuLoad": "Użycie CPU",
"systemProcessHeading": "Proces RustProxy",
"systemAppVersion": "Wersja RustProxy",
"systemProcessUptime": "Czas pracy procesu",
"systemProcessMemory": "Pamięć procesu (RSS)",
"systemProcessThreads": "Wątki procesu",
"systemOpenFds": "Otwarte deskryptory plików",
"systemCertificateHeading": "Środowisko ACME / Python",
"systemCertificateHelp": "Wersje są odczytywane z interpretera i narzędzi rzeczywiście używanych do obsługi certyfikatów.",
"systemAcmeStatus": "Stan ACME",
"systemPathsHeading": "Ścieżki danych"
} }
} }
+49 -5
View File
@@ -24,6 +24,10 @@ pub struct RuntimeStatus {
pub python: bool, pub python: bool,
pub pip: bool, pub pip: bool,
pub certbot: bool, pub certbot: bool,
/// Versions of the executables actually selected for ACME, never a guessed system default.
pub python_version: Option<String>,
pub pip_version: Option<String>,
pub certbot_version: Option<String>,
} }
impl Default for RuntimeStatus { impl Default for RuntimeStatus {
@@ -36,6 +40,9 @@ impl Default for RuntimeStatus {
python: false, python: false,
pip: false, pip: false,
certbot: false, certbot: false,
python_version: None,
pip_version: None,
certbot_version: None,
} }
} }
} }
@@ -68,11 +75,22 @@ fn find_on_path(name: &str) -> Option<PathBuf> {
if candidate.components().count() > 1 { if candidate.components().count() > 1 {
return candidate.is_file().then(|| candidate.to_owned()); return candidate.is_file().then(|| candidate.to_owned());
} }
env::var_os("PATH") for directory in env::var_os("PATH").into_iter().flat_map(|paths| env::split_paths(&paths).collect::<Vec<_>>()) {
.into_iter() let path = directory.join(name);
.flat_map(|value| env::split_paths(&value).collect::<Vec<_>>()) if path.is_file() {
.map(|directory| directory.join(name)) return Some(path);
.find(|path| path.is_file()) }
// Windows PATH lookup uses PATHEXT; Path::is_file alone does not.
// Python and Certbot are installed as .exe launchers on Windows.
#[cfg(windows)]
if path.extension().is_none() {
let executable = path.with_extension("exe");
if executable.is_file() {
return Some(executable);
}
}
}
None
} }
fn python_candidates() -> impl Iterator<Item = PathBuf> { fn python_candidates() -> impl Iterator<Item = PathBuf> {
@@ -137,6 +155,8 @@ async fn command_success(
} }
} }
include!("runtime_versions.rs");
async fn managed_environment_valid( async fn managed_environment_valid(
directory: &Path, directory: &Path,
shutdown: &tokio_util::sync::CancellationToken, shutdown: &tokio_util::sync::CancellationToken,
@@ -203,12 +223,14 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
python: false, python: false,
pip: false, pip: false,
certbot: false, certbot: false,
..RuntimeStatus::default()
}); });
return Err("acme.runtime_data_dir_missing".into()); return Err("acme.runtime_data_dir_missing".into());
}; };
let managed = data_dir.join("acme-python"); let managed = data_dir.join("acme-python");
let managed_directory = Some(managed.to_string_lossy().into_owned()); let managed_directory = Some(managed.to_string_lossy().into_owned());
if managed_environment_valid(&managed, &state.shutdown).await { if managed_environment_valid(&managed, &state.shutdown).await {
let versions = runtime_versions(&venv_python(&managed), true, Some(&venv_certbot(&managed)));
state.acme.set_runtime(RuntimeStatus { state.acme.set_runtime(RuntimeStatus {
status: "ready", status: "ready",
mode: "managed", mode: "managed",
@@ -217,6 +239,9 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
python: true, python: true,
pip: true, pip: true,
certbot: true, certbot: true,
python_version: versions.python,
pip_version: versions.pip,
certbot_version: versions.certbot,
}); });
return Ok(RuntimeLaunch { return Ok(RuntimeLaunch {
python: venv_python(&managed), python: venv_python(&managed),
@@ -241,10 +266,16 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
python: false, python: false,
pip: false, pip: false,
certbot: distro_certbot_available, certbot: distro_certbot_available,
..RuntimeStatus::default()
}); });
return Err("acme.runtime_python_missing".into()); return Err("acme.runtime_python_missing".into());
}; };
let system_pip = system_has_pip(&python, &state.shutdown).await; let system_pip = system_has_pip(&python, &state.shutdown).await;
let system_versions = runtime_versions(
&python,
system_pip,
distro_certbot.as_deref().filter(|_| distro_certbot_available),
);
state.acme.set_runtime(RuntimeStatus { state.acme.set_runtime(RuntimeStatus {
status: "initializing", status: "initializing",
mode: "managed", mode: "managed",
@@ -253,10 +284,14 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
python: true, python: true,
pip: system_pip, pip: system_pip,
certbot: distro_certbot_available, certbot: distro_certbot_available,
python_version: system_versions.python.clone(),
pip_version: system_versions.pip.clone(),
certbot_version: system_versions.certbot.clone(),
}); });
let build = build_managed_environment(&python, &managed, system_pip, &state.shutdown).await; let build = build_managed_environment(&python, &managed, system_pip, &state.shutdown).await;
if build.is_ok() { if build.is_ok() {
tracing::info!(path = %managed.display(), "managed ACME runtime ready"); tracing::info!(path = %managed.display(), "managed ACME runtime ready");
let versions = runtime_versions(&venv_python(&managed), true, Some(&venv_certbot(&managed)));
state.acme.set_runtime(RuntimeStatus { state.acme.set_runtime(RuntimeStatus {
status: "ready", status: "ready",
mode: "managed", mode: "managed",
@@ -265,6 +300,9 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
python: true, python: true,
pip: true, pip: true,
certbot: true, certbot: true,
python_version: versions.python,
pip_version: versions.pip,
certbot_version: versions.certbot,
}); });
return Ok(RuntimeLaunch { return Ok(RuntimeLaunch {
python: venv_python(&managed), python: venv_python(&managed),
@@ -282,6 +320,9 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
python: true, python: true,
pip: system_pip, pip: system_pip,
certbot: true, certbot: true,
python_version: system_versions.python,
pip_version: system_versions.pip,
certbot_version: system_versions.certbot,
}); });
return Ok(RuntimeLaunch { return Ok(RuntimeLaunch {
python, python,
@@ -312,6 +353,9 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
python: true, python: true,
pip: system_pip, pip: system_pip,
certbot: false, certbot: false,
python_version: system_versions.python,
pip_version: system_versions.pip,
certbot_version: None,
}); });
Err(message.into()) Err(message.into())
} }
+151
View File
@@ -0,0 +1,151 @@
// Version information is diagnostic, not a health check. Read package metadata
// for the *selected* runtime without launching Python, pip, Certbot or a shell.
// A missing/ambiguous version stays None; do not guess an interpreter patch
// version from an executable name or report a hard-coded Certbot pin.
use std::io::Read;
const MAX_VERSION_METADATA_BYTES: u64 = 16 * 1024;
const MAX_SITE_DIRECTORIES: usize = 64;
const MAX_PACKAGES_PER_SITE: usize = 4096;
fn metadata_text(path: &Path) -> Option<String> {
let mut bytes = Vec::new();
std::fs::File::open(path)
.ok()?
.take(MAX_VERSION_METADATA_BYTES)
.read_to_end(&mut bytes)
.ok()?;
String::from_utf8(bytes).ok()
}
fn safe_version(raw: &str) -> Option<String> {
let version = raw.trim();
(version.len() <= 48
&& version.bytes().next().is_some_and(|byte| byte.is_ascii_digit())
&& version
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || b".+-_".contains(&byte)))
.then(|| version.to_owned())
}
fn python_environment_root(executable: &Path) -> Option<std::path::PathBuf> {
let parent = executable.parent()?;
let name = parent.file_name()?.to_str()?;
let root = if name.eq_ignore_ascii_case("bin") || name.eq_ignore_ascii_case("scripts") {
parent.parent()?
} else {
parent
};
Some(root.to_owned())
}
fn interpreter_version(root: &Path) -> Option<String> {
let content = metadata_text(&root.join("pyvenv.cfg"))?;
// CPython venv writes `version = X.Y.Z`. `version_info` from virtualenv
// is a fallback when its standard `version` field is not present.
for key in ["version", "version_info"] {
for line in content.lines() {
if let Some((name, value)) = line.split_once('=') {
if name.trim().eq_ignore_ascii_case(key) {
if let Some(version) = safe_version(value) {
return Some(version);
}
}
}
}
}
None
}
fn python_minor(version: &str) -> Option<String> {
let mut parts = version.split('.');
let major = parts.next()?;
let minor = parts.next()?;
(major.bytes().all(|b| b.is_ascii_digit())
&& minor.bytes().all(|b| b.is_ascii_digit()))
.then(|| format!("python{major}.{minor}"))
}
fn package_sites(root: &Path, python_version: Option<&str>) -> Vec<std::path::PathBuf> {
let mut sites = vec![root.join("Lib/site-packages"), root.join("lib/python3/dist-packages")];
for base in [root.join("lib"), root.join("lib64")] {
if let Some(minor) = python_version.and_then(python_minor) {
sites.push(base.join(&minor).join("site-packages"));
sites.push(base.join(&minor).join("dist-packages"));
} else if let Ok(entries) = std::fs::read_dir(&base) {
// Python installations with no pyvenv.cfg may contain multiple
// interpreter versions. Never choose between conflicting results.
for entry in entries.take(MAX_SITE_DIRECTORIES).flatten() {
let path = entry.path();
if entry.file_name().to_string_lossy().starts_with("python3.") && path.is_dir() {
sites.push(path.join("site-packages"));
sites.push(path.join("dist-packages"));
}
}
}
}
sites
}
fn package_version(root: &Path, package: &str, python_version: Option<&str>) -> Option<String> {
let prefix = format!("{package}-");
let mut result: Option<String> = None;
for site in package_sites(root, python_version) {
let Ok(entries) = std::fs::read_dir(site) else { continue };
for entry in entries.take(MAX_PACKAGES_PER_SITE).flatten() {
let name = entry.file_name();
let Some(dir_name) = name.to_str() else { continue };
let Some(suffix) = dir_name.strip_prefix(prefix.as_str())
.and_then(|name| name.strip_suffix(".dist-info")) else { continue };
let Some(directory_version) = safe_version(suffix) else { continue };
let Some(content) = metadata_text(&entry.path().join("METADATA")) else { continue };
let mut reported_name = None;
let mut reported_version = None;
for line in content.lines() {
if let Some(value) = line.strip_prefix("Name: ") {
reported_name = Some(value.trim());
} else if let Some(value) = line.strip_prefix("Version: ") {
reported_version = safe_version(value);
}
if reported_name.is_some() && reported_version.is_some() { break; }
}
if !reported_name.is_some_and(|name| name.eq_ignore_ascii_case(package))
|| reported_version.as_deref() != Some(directory_version.as_str()) {
continue;
}
if result.as_ref().is_some_and(|previous| previous != &directory_version) {
return None;
}
result = Some(directory_version);
}
}
result
}
#[derive(Default)]
struct RuntimeVersions {
python: Option<String>,
pip: Option<String>,
certbot: Option<String>,
}
fn runtime_versions(python: &Path, has_pip: bool, certbot: Option<&Path>) -> RuntimeVersions {
let root = python_environment_root(python);
let python_version = root.as_deref().and_then(interpreter_version);
let pip_version = if has_pip {
root.as_deref().and_then(|root| package_version(root, "pip", python_version.as_deref()))
} else {
None
};
let certbot_version = certbot.and_then(|path| {
let certbot_root = python_environment_root(path)?;
let certbot_python_version = if root.as_deref() == Some(certbot_root.as_path()) {
python_version.as_deref()
} else {
None
};
package_version(&certbot_root, "certbot", certbot_python_version)
});
RuntimeVersions { python: python_version, pip: pip_version, certbot: certbot_version }
}
+62
View File
@@ -146,3 +146,65 @@ mod renewal_regressions {
stop(app).await; stop(app).await;
} }
} }
mod runtime_metadata_tests {
use super::*;
fn write_package(site: &Path, package: &str, version: &str, metadata: &str) {
let folder = site.join(format!("{package}-{version}.dist-info"));
std::fs::create_dir_all(&folder).unwrap();
std::fs::write(folder.join("METADATA"), metadata).unwrap();
}
#[test]
fn managed_python_pip_and_certbot_versions_use_metadata_only() {
let temp = tempfile::tempdir().unwrap();
let root = temp.path();
let python = venv_python(root);
let certbot = venv_certbot(root);
std::fs::create_dir_all(venv_bin(root)).unwrap();
std::fs::write(&python, "").unwrap();
std::fs::write(&certbot, "").unwrap();
std::fs::write(root.join("pyvenv.cfg"), "home = /usr/bin\nversion = 3.13.5\n").unwrap();
let site = if cfg!(windows) {
root.join("Lib/site-packages")
} else {
root.join("lib/python3.13/site-packages")
};
write_package(&site, "pip", "25.2", "Metadata-Version: 2.3\nName: pip\nVersion: 25.2\n");
write_package(&site, "certbot", "5.8.0", "Name: certbot\nVersion: 5.8.0\n");
let versions = runtime_versions(&python, true, Some(&certbot));
assert_eq!(versions.python.as_deref(), Some("3.13.5"));
assert_eq!(versions.pip.as_deref(), Some("25.2"));
assert_eq!(versions.certbot.as_deref(), Some("5.8.0"));
}
#[test]
fn absent_or_unverified_versions_are_not_invented() {
let temp = tempfile::tempdir().unwrap();
let root = temp.path();
let executable = root.join("bin/python3");
std::fs::create_dir_all(root.join("bin")).unwrap();
std::fs::write(&executable, "").unwrap();
let site = root.join("lib/python3/dist-packages");
write_package(&site, "pip", "99.0", "Name: other-package\nVersion: 99.0\n");
let versions = runtime_versions(&executable, true, None);
assert!(versions.python.is_none());
assert!(versions.pip.is_none());
assert!(versions.certbot.is_none());
}
#[test]
fn conflicting_site_distributions_do_not_return_stale_versions() {
let temp = tempfile::tempdir().unwrap();
let root = temp.path();
let executable = root.join("bin/python3");
std::fs::create_dir_all(root.join("bin")).unwrap();
std::fs::write(&executable, "").unwrap();
write_package(&root.join("lib/python3/dist-packages"), "pip", "23.0", "Name: pip\nVersion: 23.0\n");
write_package(&root.join("lib/python3.13/site-packages"), "pip", "25.0", "Name: pip\nVersion: 25.0\n");
assert!(runtime_versions(&executable, true, None).pip.is_none());
}
}
+52 -5
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3", "openapi": "3.0.3",
"info": { "info": {
"title": "RustProxy Administration API", "title": "RustProxy Administration API",
"version": "0.30.0", "version": "0.30.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." "description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
}, },
"servers": [ "servers": [
@@ -549,7 +549,19 @@
"description": "Off by default. For these origin response HTTP statuses, replace the application response with a RustProxy error page (no-store). Does not change RustProxy-generated routing/security errors.", "description": "Off by default. For these origin response HTTP statuses, replace the application response with a RustProxy error page (no-store). Does not change RustProxy-generated routing/security errors.",
"items": { "items": {
"type": "integer", "type": "integer",
"enum": [400, 401, 403, 404, 408, 413, 429, 500, 502, 503, 504] "enum": [
400,
401,
403,
404,
408,
413,
429,
500,
502,
503,
504
]
} }
} }
}, },
@@ -4737,7 +4749,7 @@
}, },
"certificate_runtime": { "certificate_runtime": {
"type": "object", "type": "object",
"description": "Python/pip/Certbot capability used by managed certificate automation.", "description": "ACME Python/pip/Certbot capability and versions read from local runtime/package metadata without executing diagnostic commands. Missing metadata returns null.",
"properties": { "properties": {
"status": { "status": {
"type": "string", "type": "string",
@@ -4771,6 +4783,21 @@
}, },
"certbot": { "certbot": {
"type": "boolean" "type": "boolean"
},
"python_version": {
"type": "string",
"nullable": true,
"description": "Version declared by the selected runtime (pyvenv.cfg for Python, dist-info/METADATA for pip and Certbot); null if missing or ambiguous."
},
"pip_version": {
"type": "string",
"nullable": true,
"description": "Version declared by the selected runtime (pyvenv.cfg for Python, dist-info/METADATA for pip and Certbot); null if missing or ambiguous."
},
"certbot_version": {
"type": "string",
"nullable": true,
"description": "Version declared by the selected runtime (pyvenv.cfg for Python, dist-info/METADATA for pip and Certbot); null if missing or ambiguous."
} }
}, },
"required": [ "required": [
@@ -4780,8 +4807,25 @@
"managed_directory", "managed_directory",
"python", "python",
"pip", "pip",
"certbot" "certbot",
"python_version",
"pip_version",
"certbot_version"
] ]
},
"architecture": {
"type": "string",
"description": "Architecture of the RustProxy executable."
},
"operating_system": {
"type": "string",
"nullable": true,
"description": "Host OS release name from the cross-platform sysinfo library, when supported."
},
"kernel_version": {
"type": "string",
"nullable": true,
"description": "Host kernel version from the cross-platform sysinfo library, when available."
} }
}, },
"required": [ "required": [
@@ -4789,7 +4833,10 @@
"statistics_source", "statistics_source",
"data_directory", "data_directory",
"database_path", "database_path",
"certificate_runtime" "certificate_runtime",
"architecture",
"operating_system",
"kernel_version"
] ]
}, },
"CertificateApiCredential": { "CertificateApiCredential": {
+33
View File
@@ -1,3 +1,33 @@
// Reuse sysinfo's native OS APIs on Linux, BSD, macOS and Windows. The
// diagnostics endpoint must never spawn system commands or parse platform files.
// Host identification is cached because it changes only after a reboot.
fn valid_host_text(value: String) -> Option<String> {
let value = value.trim();
(!value.is_empty() && value.len() <= 128 && !value.chars().any(char::is_control))
.then(|| value.to_owned())
}
fn os_release() -> Option<&'static str> {
static VALUE: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
VALUE.get_or_init(|| {
sysinfo::System::long_os_version()
.or_else(|| {
let name = sysinfo::System::name()?;
Some(match sysinfo::System::os_version() {
Some(version) if !version.is_empty() => format!("{name} {version}"),
_ => name,
})
})
.and_then(valid_host_text)
}).as_deref()
}
fn kernel_release() -> Option<&'static str> {
static VALUE: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
VALUE.get_or_init(|| sysinfo::System::kernel_version().and_then(valid_host_text))
.as_deref()
}
fn status_payload(state: &App) -> Value { fn status_payload(state: &App) -> Value {
let snapshot = state.config.load_full(); let snapshot = state.config.load_full();
let listeners = json!({ let listeners = json!({
@@ -180,6 +210,9 @@ fn status_payload(state: &App) -> Value {
}); });
payload["runtime"] = json!({ payload["runtime"] = json!({
"platform": system.platform, "platform": system.platform,
"operating_system": os_release(),
"kernel_version": kernel_release(),
"architecture": std::env::consts::ARCH,
"statistics_source": system.source, "statistics_source": system.source,
"data_directory": state.policy.data_dir.as_ref().map(|path| path.display().to_string()), "data_directory": state.policy.data_dir.as_ref().map(|path| path.display().to_string()),
"database_path": state.policy.data_dir.as_ref().map(|path| path.join("config.sqlite3").display().to_string()), "database_path": state.policy.data_dir.as_ref().map(|path| path.join("config.sqlite3").display().to_string()),
+2 -15
View File
@@ -192,21 +192,8 @@ function enhanceEnabledControl(input, hintKey = "entityEnableHint") {
} }
window.rustProxyEnableControl = enhanceEnabledControl; window.rustProxyEnableControl = enhanceEnabledControl;
function action(label, fn) { const b = el("button", label, "quiet"); b.type = "button"; b.addEventListener("click", fn); return b; } function action(label, fn) { const b = el("button", label, "quiet"); b.type = "button"; b.addEventListener("click", fn); return b; }
function renderAboutRuntime(status = latestStatus) {
const value = status?.runtime?.certificate_runtime || {};
const availability = flag => flag === true ? t("runtimeAvailable") : flag === false ? t("runtimeUnavailable") : t("unavailable");
const mode = value.status === "unavailable" ? t("certificateRuntimeUnavailable") : value.status === "ready" && value.mode === "managed" ? t("certificateRuntimeManaged") : value.status === "ready" && value.mode === "system" ? t("certificateRuntimeSystem") : t("certificateRuntimePreparing");
const modeNode = $("about-acme-mode"), runtimeNode = $("about-python-runtime"), details = $("about-python-details");
if (modeNode) modeNode.textContent = mode;
if (runtimeNode) runtimeNode.textContent = `${t("runtimePython")}: ${availability(value.python)} · ${t("runtimePip")}: ${availability(value.pip)} · ${t("runtimeCertbot")}: ${availability(value.certbot)}`;
if (details) {
const parts = [value.message ? t(value.message) : ""];
if (value.managed_directory) parts.push(`${t("certificateRuntimePath")}: ${value.managed_directory}`);
details.textContent = parts.filter(Boolean).join(" · ");
}
}
function renderStatus(s) { function renderStatus(s) {
const firstStatus = latestStatus === null; latestStatus = s; renderAboutRuntime(s); window.rustProxySettings?.render(); if (firstStatus) renderSettings(); const firstStatus = latestStatus === null; latestStatus = s; window.rustProxySettings?.render(); if (firstStatus) renderSettings();
connected = true; $("connection-label").textContent = t("connected"); $("signal").classList.remove("status-error"); const m = s.metrics; connected = true; $("connection-label").textContent = t("connected"); $("signal").classList.remove("status-error"); const m = s.metrics;
$("metric-ws").textContent = m.websocket_active || 0; $("metric-ws").textContent = m.websocket_active || 0;
$("metric-ws-h1").textContent = m.websocket_h1_active || 0; $("metric-ws-h1").textContent = m.websocket_h1_active || 0;
@@ -384,7 +371,7 @@ function stopTunnelRefresh() { if (tunnelRefreshTimer) clearInterval(tunnelRefre
function startTunnelRefresh() { stopTunnelRefresh(); tunnelRefreshTimer = setInterval(() => { if ($("tunnels-dialog")?.open) void loadTunnels(); }, 2000); } function startTunnelRefresh() { stopTunnelRefresh(); tunnelRefreshTimer = setInterval(() => { if ($("tunnels-dialog")?.open) void loadTunnels(); }, 2000); }
$("login-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; authMessage(""); try { await withFormSubmitState(form, event.submitter, async () => { const username = $("username").value.trim(); const password = $("password").value; if (setupRequired) { if (password !== $("password-confirm").value) throw fieldError("password-confirm", t("passwordMismatch")); await api("/api/auth/setup", { method: "POST", body: JSON.stringify({ token: $("setup-token").value.trim(), username, password }) }); setupRequired = false; syncAuthForm(); } const info = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password }) }); await activate(info); clearTimeout(toastTimer); hideToast(); }); } catch (e) { if (e.code === "auth.already_initialized") { setupRequired = false; syncAuthForm(); } $("password").value = ""; $("password-confirm").value = ""; authMessage(e.message); } }); $("login-form").addEventListener("submit", async event => { event.preventDefault(); const form = event.currentTarget; authMessage(""); try { await withFormSubmitState(form, event.submitter, async () => { const username = $("username").value.trim(); const password = $("password").value; if (setupRequired) { if (password !== $("password-confirm").value) throw fieldError("password-confirm", t("passwordMismatch")); await api("/api/auth/setup", { method: "POST", body: JSON.stringify({ token: $("setup-token").value.trim(), username, password }) }); setupRequired = false; syncAuthForm(); } const info = await api("/api/auth/login", { method: "POST", body: JSON.stringify({ username, password }) }); await activate(info); clearTimeout(toastTimer); hideToast(); }); } catch (e) { if (e.code === "auth.already_initialized") { setupRequired = false; syncAuthForm(); } $("password").value = ""; $("password-confirm").value = ""; authMessage(e.message); } });
$("logout").addEventListener("click", async () => { try { await api("/api/auth/logout", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { if (e.status === 401) lock(); toast(e.message, true); } }); $("logout").addEventListener("click", async () => { try { await api("/api/auth/logout", { method: "POST" }); lock(); toast(t("locked")); } catch (e) { if (e.status === 401) lock(); toast(e.message, true); } });
$("about-open").addEventListener("click", () => { renderAboutRuntime(); const dialog = $("about-dialog"); if (!dialog.open) dialog.showModal(); }); $("about-open").addEventListener("click", () => { const dialog = $("about-dialog"); if (!dialog.open) dialog.showModal(); });
$("about-close").addEventListener("click", () => $("about-dialog").close()); $("about-close").addEventListener("click", () => $("about-dialog").close());
const mobileMenuToggle = $("mobile-menu-toggle"), dashboardSidebar = document.querySelector(".sidebar"); const mobileMenuToggle = $("mobile-menu-toggle"), dashboardSidebar = document.querySelector(".sidebar");
function setMobileMenu(open) { function setMobileMenu(open) {
+45 -29
View File
@@ -1768,6 +1768,9 @@
aria-controls="settings-page-administration" aria-selected="false" aria-controls="settings-page-administration" aria-selected="false"
data-i18n="administration" data-settings-tab="administration" data-i18n="administration" data-settings-tab="administration"
id="settings-tab-administration" role="tab" tabindex="-1" type="button"></button><button id="settings-tab-administration" role="tab" tabindex="-1" type="button"></button><button
aria-controls="settings-page-system-info" aria-selected="false"
data-i18n="systemInformation" data-settings-tab="system-info"
id="settings-tab-system-info" role="tab" tabindex="-1" type="button"></button><button
aria-controls="settings-page-certificate-api" aria-selected="false" aria-controls="settings-page-certificate-api" aria-selected="false"
data-i18n="certificateApiTitle" data-settings-tab="certificate-api" data-i18n="certificateApiTitle" data-settings-tab="certificate-api"
id="settings-tab-certificate-api" role="tab" tabindex="-1" type="button"></button><button id="settings-tab-certificate-api" role="tab" tabindex="-1" type="button"></button><button
@@ -2698,30 +2701,52 @@
<div class="form-actions"><button class="primary" data-i18n="apply" <div class="form-actions"><button class="primary" data-i18n="apply"
type="submit">Validate, save and apply</button></div> type="submit">Validate, save and apply</button></div>
</form> </form>
</div>
<div aria-labelledby="settings-tab-system-info" class="settings-page"
data-settings-page="system-info" hidden="" id="settings-page-system-info" role="tabpanel">
<article class="card edit-form runtime-information-card"> <article class="card edit-form runtime-information-card">
<div class="card-heading"> <div class="card-heading">
<h2 data-i18n="runtimeInformation">Runtime information</h2><span <h2 data-i18n="systemInformation">System information</h2><span class="pill">API</span>
class="pill">API</span>
</div> </div>
<p class="fine" data-i18n="runtimeInformationHelp">Reported by the running RustProxy <p class="fine" data-i18n="systemInformationHelp">Live, read-only information from the authenticated RustProxy status API. Unavailable metrics are not estimated.</p>
process and available only from the authenticated administrator status API.</p> <h3 data-i18n="systemHostHeading">Operating system and hardware</h3>
<div class="runtime-information-grid"> <div class="runtime-information-grid">
<div><span class="small-label" data-i18n="runtimePlatform">Platform</span><code <div><span class="small-label" data-i18n="runtimePlatform">Platform</span><code id="runtime-platform">-</code></div>
id="runtime-platform">-</code></div> <div><span class="small-label" data-i18n="systemOsVersion">Operating system</span><code id="runtime-operating-system">-</code></div>
<div><span class="small-label" data-i18n="statisticsSource">Statistics <div><span class="small-label" data-i18n="systemKernel">Kernel</span><code id="runtime-kernel-version">-</code></div>
source</span><code id="runtime-statistics-source">-</code></div> <div><span class="small-label" data-i18n="systemArchitecture">Architecture</span><code id="runtime-architecture">-</code></div>
<div><span class="small-label" data-i18n="dataDirectory">Data directory</span><code <div><span class="small-label" data-i18n="systemLogicalCpus">Logical CPUs</span><code id="runtime-logical-cpus">-</code></div>
id="runtime-data-directory">-</code></div> <div><span class="small-label" data-i18n="systemCpuLoad">CPU usage</span><code id="runtime-cpu-usage">-</code></div>
<div><span class="small-label" data-i18n="databaseLocation">Database</span><code <div><span class="small-label" data-i18n="systemMemory">RAM used / total</span><code id="runtime-system-memory">-</code></div>
id="runtime-database-path">-</code></div> <div><span class="small-label" data-i18n="systemUptime">System uptime</span><code id="runtime-system-uptime">-</code></div>
<div><span class="small-label" data-i18n="runtimeAcmeMode">ACME mode</span><code </div>
id="runtime-acme-mode">-</code></div> <h3 data-i18n="systemProcessHeading">RustProxy process</h3>
<div><span class="small-label" data-i18n="runtimePython">Python</span><code <div class="runtime-information-grid">
id="runtime-python">-</code></div> <div><span class="small-label" data-i18n="systemAppVersion">RustProxy version</span><code id="runtime-version">-</code></div>
<div><span class="small-label" data-i18n="runtimePip">pip</span><code <div><span class="small-label" data-i18n="systemProcessUptime">Process uptime</span><code id="runtime-process-uptime">-</code></div>
id="runtime-pip">-</code></div> <div><span class="small-label" data-i18n="systemProcessMemory">Process RAM (RSS)</span><code id="runtime-process-memory">-</code></div>
<div><span class="small-label" data-i18n="runtimeCertbot">Certbot</span><code <div><span class="small-label" data-i18n="systemProcessThreads">Process threads</span><code id="runtime-process-threads">-</code></div>
id="runtime-certbot">-</code></div> <div><span class="small-label" data-i18n="systemOpenFds">Open file descriptors</span><code id="runtime-open-fds">-</code></div>
<div><span class="small-label" data-i18n="statisticsSource">Statistics source</span><code id="runtime-statistics-source">-</code></div>
</div>
</article>
<article class="card edit-form runtime-information-card">
<div class="card-heading"><h2 data-i18n="systemCertificateHeading">ACME / Python environment</h2></div>
<p class="fine" data-i18n="systemCertificateHelp">Versions refer to the interpreter and tools actually selected for certificate management, not an arbitrary Python installation.</p>
<div class="runtime-information-grid">
<div><span class="small-label" data-i18n="runtimeAcmeMode">ACME mode</span><code id="runtime-acme-mode">-</code></div>
<div><span class="small-label" data-i18n="systemAcmeStatus">ACME status</span><code id="runtime-acme-status">-</code></div>
<div><span class="small-label" data-i18n="runtimePython">Python</span><code id="runtime-python">-</code></div>
<div><span class="small-label" data-i18n="runtimePip">pip</span><code id="runtime-pip">-</code></div>
<div><span class="small-label" data-i18n="runtimeCertbot">Certbot</span><code id="runtime-certbot">-</code></div>
<div><span class="small-label" data-i18n="certificateRuntimePath">Managed environment</span><code id="runtime-acme-directory">-</code></div>
</div>
</article>
<article class="card edit-form runtime-information-card">
<div class="card-heading"><h2 data-i18n="systemPathsHeading">Data locations</h2></div>
<div class="runtime-information-grid">
<div><span class="small-label" data-i18n="dataDirectory">Data directory</span><code id="runtime-data-directory">-</code></div>
<div><span class="small-label" data-i18n="databaseLocation">Database</span><code id="runtime-database-path">-</code></div>
</div> </div>
</article> </article>
</div> </div>
@@ -3659,18 +3684,9 @@ log_errors = On</code></pre>
<dt data-i18n="aboutLicense">License</dt> <dt data-i18n="aboutLicense">License</dt>
<dd data-i18n="aboutLicenseValue">Open Source</dd> <dd data-i18n="aboutLicenseValue">Open Source</dd>
</div> </div>
<div>
<dt data-i18n="runtimeAcmeMode">ACME mode</dt>
<dd id="about-acme-mode">-</dd>
</div>
<div>
<dt data-i18n="aboutPythonRuntime">Certificate Python runtime</dt>
<dd id="about-python-runtime">-</dd>
</div>
</dl> </dl>
<p data-i18n="aboutOpenSourceText">RustProxy is an open-source project. Its source code is publicly <p data-i18n="aboutOpenSourceText">RustProxy is an open-source project. Its source code is publicly
available for inspection, development and community contribution.</p> available for inspection, development and community contribution.</p>
<p class="about-python-details" id="about-python-details"></p>
<a class="outline about-repository" href="https://git.linuxiarz.pl/gru/RustProxy" rel="noopener noreferrer" <a class="outline about-repository" href="https://git.linuxiarz.pl/gru/RustProxy" rel="noopener noreferrer"
target="_blank"><span data-i18n="aboutRepository">Official source target="_blank"><span data-i18n="aboutRepository">Official source
repository</span><span>git.linuxiarz.pl/gru/RustProxy</span></a> repository</span><span>git.linuxiarz.pl/gru/RustProxy</span></a>
+35 -4
View File
@@ -219,13 +219,44 @@ curl --fail --show-error --request POST \
const syncAdminTls = () => { $("admin-certificate").disabled = !$("admin-tls").checked; }; syncAdminTls(); const syncAdminTls = () => { $("admin-certificate").disabled = !$("admin-tls").checked; }; syncAdminTls();
$("admin-status").textContent = adminStatus?.restart_required ? t("restartPending") : adminStatus?.active?.tls ? `${t("administrationActiveHttps")}: ${adminStatus.active.listen}` : `${t("administrationActiveHttp")}: ${adminStatus?.active?.listen || latestStatus?.admin_listen || "-"}`; $("admin-status").textContent = adminStatus?.restart_required ? t("restartPending") : adminStatus?.active?.tls ? `${t("administrationActiveHttps")}: ${adminStatus.active.listen}` : `${t("administrationActiveHttp")}: ${adminStatus?.active?.listen || latestStatus?.admin_listen || "-"}`;
const runtime = latestStatus?.runtime || {}; const runtime = latestStatus?.runtime || {};
for (const [id, key] of [["runtime-platform", "platform"], ["runtime-statistics-source", "statistics_source"], ["runtime-data-directory", "data_directory"], ["runtime-database-path", "database_path"]]) { const system = latestStatus?.system || {};
const node = $(id); if (node) node.textContent = runtime[key] || t("unavailable"); const hasMetric = value => value !== null && value !== undefined && Number.isFinite(Number(value));
} const humanCount = value => hasMetric(value) ? Number(value).toLocaleString(locale()) : t("unavailable");
const humanBytes = value => hasMetric(value) ? bytes(Number(value)) : t("unavailable");
const humanUptime = value => hasMetric(value) ? uptime(Number(value)) : t("unavailable");
const percent = value => hasMetric(value) ? `${Number(value).toLocaleString(locale(), { maximumFractionDigits: 1 })}%` : t("unavailable");
const ram = hasMetric(system.memory_used_bytes) || hasMetric(system.memory_total_bytes)
? `${humanBytes(system.memory_used_bytes)} / ${humanBytes(system.memory_total_bytes)}` : t("unavailable");
for (const [id, value] of [
["runtime-version", latestStatus?.version],
["runtime-platform", runtime.platform],
["runtime-operating-system", runtime.operating_system],
["runtime-kernel-version", runtime.kernel_version],
["runtime-architecture", runtime.architecture],
["runtime-logical-cpus", humanCount(system.logical_cpus)],
["runtime-cpu-usage", percent(system.cpu_percent)],
["runtime-system-memory", ram],
["runtime-system-uptime", humanUptime(system.system_uptime_seconds)],
["runtime-process-uptime", humanUptime(latestStatus?.uptime_seconds)],
["runtime-process-memory", humanBytes(system.process_rss_bytes)],
["runtime-process-threads", humanCount(system.process_threads)],
["runtime-open-fds", humanCount(system.open_fds)],
["runtime-statistics-source", runtime.statistics_source],
["runtime-data-directory", runtime.data_directory],
["runtime-database-path", runtime.database_path]
]) { const node = $(id); if (node) node.textContent = value ?? t("unavailable"); }
const certificateRuntime = runtime.certificate_runtime || {}; const certificateRuntime = runtime.certificate_runtime || {};
const availability = flag => flag === true ? t("runtimeAvailable") : flag === false ? t("runtimeUnavailable") : t("unavailable"); const availability = flag => flag === true ? t("runtimeAvailable") : flag === false ? t("runtimeUnavailable") : t("unavailable");
const modeLabel = certificateRuntime.status === "unavailable" ? t("certificateRuntimeUnavailable") : certificateRuntime.status === "ready" && certificateRuntime.mode === "managed" ? t("certificateRuntimeManaged") : certificateRuntime.status === "ready" && certificateRuntime.mode === "system" ? t("certificateRuntimeSystem") : t("certificateRuntimePreparing"); const modeLabel = certificateRuntime.status === "unavailable" ? t("certificateRuntimeUnavailable") : certificateRuntime.status === "ready" && certificateRuntime.mode === "managed" ? t("certificateRuntimeManaged") : certificateRuntime.status === "ready" && certificateRuntime.mode === "system" ? t("certificateRuntimeSystem") : t("certificateRuntimePreparing");
for (const [id, text] of [["runtime-acme-mode", modeLabel], ["runtime-python", availability(certificateRuntime.python)], ["runtime-pip", availability(certificateRuntime.pip)], ["runtime-certbot", availability(certificateRuntime.certbot)]]) { const node = $(id); if (node) node.textContent = text; } const versioned = (flag, version) => version && flag === true ? `${availability(flag)} · ${version}` : availability(flag);
for (const [id, value] of [
["runtime-acme-mode", modeLabel],
["runtime-acme-status", certificateRuntime.message ? t(certificateRuntime.message) : t("unavailable")],
["runtime-python", versioned(certificateRuntime.python, certificateRuntime.python_version)],
["runtime-pip", versioned(certificateRuntime.pip, certificateRuntime.pip_version)],
["runtime-certbot", versioned(certificateRuntime.certbot, certificateRuntime.certbot_version)],
["runtime-acme-directory", certificateRuntime.managed_directory]
]) { const node = $(id); if (node) node.textContent = value || t("unavailable"); }
const policy = envelope.config.tls?.policy || { tls12: true, tls13: true, cipher_suites: [] }; const policy = envelope.config.tls?.policy || { tls12: true, tls13: true, cipher_suites: [] };
if (!settingsDirty.has("tls-policy-form")) { $("tls-policy-12").checked = policy.tls12 !== false; $("tls-policy-13").checked = policy.tls13 !== false; $("tls-policy-ciphers").value = (policy.cipher_suites || []).join("\n"); } if (!settingsDirty.has("tls-policy-form")) { $("tls-policy-12").checked = policy.tls12 !== false; $("tls-policy-13").checked = policy.tls13 !== false; $("tls-policy-ciphers").value = (policy.cipher_suites || []).join("\n"); }
+12 -1
View File
@@ -7606,6 +7606,11 @@ body[data-density="compact"] .page-footer {
background: currentColor; background: currentColor;
} }
/* The legend keys must use the same theme/preset palette as the SVG strokes. */
.stat-chart .stat-chart-legend .legend-h1 i { background: var(--traffic-h1); }
.stat-chart .stat-chart-legend .legend-h2 i { background: var(--traffic-h2); }
.stat-chart .stat-chart-legend .legend-h3 i { background: var(--traffic-h3); }
.stat-chart .stat-chart-legend b { .stat-chart .stat-chart-legend b {
font-family: var(--mono); font-family: var(--mono);
font-weight: 500; font-weight: 500;
@@ -7809,7 +7814,13 @@ body[data-density="compact"] .page-footer {
} }
.runtime-information-card { .runtime-information-card {
margin-top: 12px; margin-top: 0;
}
.runtime-information-card h3 {
margin: 16px 0 8px;
font-size: 12px;
font-weight: 600;
} }
.runtime-information-grid { .runtime-information-grid {
+12 -12
View File
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
preflight(root) preflight(root)
manifest = tomllib.loads((root / "Cargo.toml").read_text()) manifest = tomllib.loads((root / "Cargo.toml").read_text())
assert manifest["package"]["version"] == "0.30.0" assert manifest["package"]["version"] == "0.30.1"
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12" assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
h3dep = manifest["dependencies"]["h3"] h3dep = manifest["dependencies"]["h3"]
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"] 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" lock_path = root / "Cargo.lock"
if lock_path.exists(): if lock_path.exists():
lock = lock_path.read_text() lock = lock_path.read_text()
assert 'name = "rust-proxy"\nversion = "0.30.0"' in lock assert 'name = "rust-proxy"\nversion = "0.30.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" assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
for release_doc, marker in { for release_doc, marker in {
"README.md": "# RustProxy 0.30.0", "README.md": "# RustProxy 0.30.1",
"CHANGELOG.md": "## 0.30.0", "CHANGELOG.md": "## 0.30.1",
"ROADMAP.md": "## 0.30.0", "ROADMAP.md": "## 0.30.1",
"AGENTS.md": "Current source version: 0.30.0.", "AGENTS.md": "Current source version: 0.30.1.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.30.0", "docs/TEST_REPORT.md": "# Verification report - RustProxy 0.30.1",
}.items(): }.items():
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}" assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable" assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
@@ -285,7 +285,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
assert '"StreamMetrics"' in openapi and '"streams"' in openapi assert '"StreamMetrics"' in openapi and '"streams"' in openapi
for marker in ['"version": "0.30.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.30.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 assert marker in openapi, marker
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']: for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker assert marker in openapi, marker
@@ -585,7 +585,7 @@ h2_settings_controls = integrations_js.split('function http2SettingsControls', 1
assert 'listener-h2' not in h2_settings_controls, 'HTTP/2 enablement belongs to the listener editor, not the tuning page' assert 'listener-h2' not in h2_settings_controls, 'HTTP/2 enablement belongs to the listener editor, not the tuning page'
h3_settings_controls = integrations_js.split('function http3SettingsControls', 1)[1].split('function addRow', 1)[0] h3_settings_controls = integrations_js.split('function http3SettingsControls', 1)[1].split('function addRow', 1)[0]
assert 'h3Enable' not in h3_settings_controls, 'HTTP/3 enablement belongs to the listener editor, not the tuning page' assert 'h3Enable' not in h3_settings_controls, 'HTTP/3 enablement belongs to the listener editor, not the tuning page'
for marker in ['https://git.linuxiarz.pl/gru/RustProxy', 'https://rustproxy.linuxiarz.pl/', 'id="about-python-runtime"', 'id="runtime-python"', 'id="acme-runtime-components"']: for marker in ['https://git.linuxiarz.pl/gru/RustProxy', 'https://rustproxy.linuxiarz.pl/', 'data-settings-tab="system-info"', 'id="runtime-python"', 'id="acme-runtime-components"']:
assert marker in html, marker assert marker in html, marker
for language in ['langs/en_US.json', 'langs/pl_pl.json']: for language in ['langs/en_US.json', 'langs/pl_pl.json']:
language_text = (root / language).read_text(encoding='utf-8') language_text = (root / language).read_text(encoding='utf-8')
@@ -1363,7 +1363,7 @@ assert metrics_dep["version"] == "=2.4.3" and metrics_dep["default-features"] is
assert metrics_dep["features"] == ["dummy"] assert metrics_dep["features"] == ["dummy"]
sysinfo_dep = manifest["dependencies"]["sysinfo"] sysinfo_dep = manifest["dependencies"]["sysinfo"]
assert sysinfo_dep["version"] == "=0.38.4" and sysinfo_dep["default-features"] is False assert sysinfo_dep["version"] == "=0.38.4" and sysinfo_dep["default-features"] is False
assert sysinfo_dep["features"] == ["network"] assert set(sysinfo_dep["features"]) == {"network", "system"}
for marker_0241 in [ for marker_0241 in [
"systemstat::{Platform, System}", "metrics_process::collector::collect", "systemstat::{Platform, System}", "metrics_process::collector::collect",
"sysinfo::Networks::new_with_refreshed_list", "collect_network_systemstat", "merge_network_interfaces", "sysinfo::Networks::new_with_refreshed_list", "collect_network_systemstat", "merge_network_interfaces",
@@ -1396,7 +1396,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241 assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
assert 'location.pathname === "/logging"' in settings_js_0241 assert 'location.pathname === "/logging"' in settings_js_0241
assert '"RuntimeInformation"' in openapi and '"version": "0.30.0"' in openapi assert '"RuntimeInformation"' in openapi and '"version": "0.30.1"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions. # 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs") tls_0241 = rust_source(root, "src/tls.rs")
@@ -1635,5 +1635,5 @@ for marker_0282 in [
assert 'Zewnętrzne ścieżki plików' in (root / 'langs/pl_pl.json').read_text() assert 'Zewnętrzne ścieżki plików' in (root / 'langs/pl_pl.json').read_text()
assert 'setfacl -m u:rustproxy:--x /etc/ssl/' in (root / 'docs/V0.28.2_IMPLEMENTATION.md').read_text() assert 'setfacl -m u:rustproxy:--x /etc/ssl/' in (root / 'docs/V0.28.2_IMPLEMENTATION.md').read_text()
print("PASS: 0.30.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-v9 migrations.") print("PASS: 0.30.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-v9 migrations.")
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.") print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Rust-independent documentation/Reader consistency checks for the 0.30.0 release.""" """Rust-independent documentation/Reader consistency checks for the 0.30.1 release."""
from pathlib import Path from pathlib import Path
import re import re
from rust_source import rust_source from rust_source import rust_source
@@ -64,7 +64,7 @@ assert '"/docs/content.json"' in admin
readme = (ROOT / "README.md").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8")
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8") roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert readme.startswith("# RustProxy 0.30.0") assert readme.startswith("# RustProxy 0.30.1")
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive" assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
+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)}") raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]: for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/health.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]:
if not (root / path).is_file(): if not (root / path).is_file():
raise ValueError(f"Incomplete RustProxy 0.30.0 source tree: missing {path}") raise ValueError(f"Incomplete RustProxy 0.30.1 source tree: missing {path}")
text = (root / "src/lib.rs").read_text(encoding="utf-8") text = (root / "src/lib.rs").read_text(encoding="utf-8")
if '#[path = "auth/mod.rs"]' not in text: if '#[path = "auth/mod.rs"]' not in text:
raise ValueError("src/lib.rs is not the 0.30.0 entry point; extract the complete package, not individual files.") raise ValueError("src/lib.rs is not the 0.30.1 entry point; extract the complete package, not individual files.")
return messages return messages
+7 -7
View File
@@ -135,7 +135,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert backup.is_dir() assert backup.is_dir()
assert (backup / "src/obsolete.rs").is_file() assert (backup / "src/obsolete.rs").is_file()
assert not (target / "src/obsolete.rs").exists() assert not (target / "src/obsolete.rs").exists()
assert 'version = "0.30.0"' in (target / "Cargo.toml").read_text(encoding="utf-8") assert 'version = "0.30.1"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n" assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n" assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
assert (target / "langs/site.json").is_file() assert (target / "langs/site.json").is_file()
@@ -146,7 +146,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert (target / "docker-agent/certificate.go").is_file() assert (target / "docker-agent/certificate.go").is_file()
assert (target / "docker-agent/server.go").is_file() assert (target / "docker-agent/server.go").is_file()
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8")) manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
assert manifest["to_version"] == "0.30.0" assert manifest["to_version"] == "0.30.1"
assert manifest["removed_obsolete_runtime_state"] is True assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists() assert not legacy.exists()
assert not (backup / "legacy-data").exists() assert not (backup / "legacy-data").exists()
@@ -434,7 +434,7 @@ def ui_contract_smoke() -> None:
assert 'stream_list_sort' in list_sort_sources['streams'] assert 'stream_list_sort' in list_sort_sources['streams']
for marker in ['id="pool-search"', 'id="pool-sort"', 'data-pool-column-toggle="vhost"']: for marker in ['id="pool-search"', 'id="pool-sort"', 'data-pool-column-toggle="vhost"']:
assert marker in html, marker assert marker in html, marker
for marker in ['https://git.linuxiarz.pl/gru/RustProxy', 'https://rustproxy.linuxiarz.pl/', 'id="about-python-runtime"', 'id="runtime-python"', 'id="acme-runtime-components"']: for marker in ['https://git.linuxiarz.pl/gru/RustProxy', 'https://rustproxy.linuxiarz.pl/', 'data-settings-tab="system-info"', 'id="runtime-python"', 'id="acme-runtime-components"']:
assert marker in html, marker assert marker in html, marker
for language in ['langs/en_US.json', 'langs/pl_pl.json']: for language in ['langs/en_US.json', 'langs/pl_pl.json']:
text = (ROOT / language).read_text(encoding='utf-8') text = (ROOT / language).read_text(encoding='utf-8')
@@ -586,7 +586,7 @@ def source_contract_smoke() -> None:
sysinfo_network = manifest["dependencies"]["sysinfo"] sysinfo_network = manifest["dependencies"]["sysinfo"]
assert sysinfo_network["version"] == "=0.38.4" assert sysinfo_network["version"] == "=0.38.4"
assert sysinfo_network["default-features"] is False assert sysinfo_network["default-features"] is False
assert sysinfo_network["features"] == ["network"] assert set(sysinfo_network["features"]) == {"network", "system"}
target_key = 'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "macos", target_os = "windows"))' target_key = 'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "dragonfly", target_os = "macos", target_os = "windows"))'
assert manifest["target"][target_key]["dependencies"]["systemstat"] == "=0.2.7" assert manifest["target"][target_key]["dependencies"]["systemstat"] == "=0.2.7"
for marker in [ for marker in [
@@ -619,7 +619,7 @@ def source_contract_smoke() -> None:
assert marker in admin, marker assert marker in admin, marker
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0] anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
assert '"/api/status"' not in anonymous_block assert '"/api/status"' not in anonymous_block
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.30.0"' in openapi_0241 assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.30.1"' in openapi_0241
assert 'id="panel-logging"' not in html assert 'id="panel-logging"' not in html
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']: for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
assert marker in html, marker assert marker in html, marker
@@ -1109,7 +1109,7 @@ def source_contract_smoke() -> None:
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8") openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
assert '"/api/config/history/restore"' in openapi assert '"/api/config/history/restore"' in openapi
for marker in ['"version": "0.30.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.30.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 marker in openapi, marker
assert '"software_update"' in openapi and '"update_available"' in openapi assert '"software_update"' in openapi and '"update_available"' in openapi
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']: for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
@@ -1314,7 +1314,7 @@ def main() -> None:
source_contract_smoke() source_contract_smoke()
container_contract_smoke() container_contract_smoke()
grafana_contract_smoke() grafana_contract_smoke()
print("PASS: 0.30.0 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.") print("PASS: 0.30.1 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
if __name__ == "__main__": if __name__ == "__main__":
+127
View File
@@ -0,0 +1,127 @@
#!/usr/bin/env node
// Simulated status and tab DOM. No Rust process or visual browser is run here.
import assert from 'node:assert/strict';
import fs from 'node:fs';
import vm from 'node:vm';
const html = fs.readFileSync('src/admin/web/index.html', 'utf8');
const css = fs.readFileSync('src/admin/web/style.css', 'utf8');
const js = fs.readFileSync('src/admin/web/settings.js', 'utf8');
const core = fs.readFileSync('src/admin/web/app/core.js', 'utf8');
const api = JSON.parse(fs.readFileSync('src/admin/openapi.json', 'utf8'));
const has = (haystack, needle) => assert.ok(haystack.includes(needle), `Missing ${needle}`);
// The two mini charts share the exact H1/H2/H3 palette as their SVG lines.
for (const version of ['h1', 'h2', 'h3']) {
has(css, `.stat-chart .stat-chart-legend .legend-${version} i { background: var(--traffic-${version}); }`);
assert.match(css, new RegExp(`\\.proto-${version}-line\\s*\\{[^}]*stroke:\\s*var\\(--traffic-${version}\\);`));
has(html, `id="http-protocol-${version}"`);
has(html, `id="ws-protocol-${version}"`);
}
assert.equal((html.match(/class="stat-chart-legend"/g) || []).length, 2);
const about = html.split('<dialog aria-labelledby="about-title"')[1].split('</dialog>')[0];
for (const forbidden of ['about-acme-mode', 'about-python-runtime', 'about-python-details', 'runtimeAcmeMode']) {
assert.ok(!about.includes(forbidden), `ACME details left in About: ${forbidden}`);
}
assert.ok(!core.includes('renderAboutRuntime('), 'Obsolete About runtime renderer must be removed');
for (const marker of ['aboutAuthor', 'aboutLicense', 'aboutRepository', 'aboutPackagesRepository']) has(about, marker);
has(html, 'data-settings-tab="system-info"');
has(html, 'id="settings-page-system-info"');
const tab = html.split('id="settings-page-system-info"')[1].split('id="settings-page-certificate-api"')[0];
for (const field of ['runtime-operating-system', 'runtime-kernel-version', 'runtime-architecture',
'runtime-logical-cpus', 'runtime-system-memory', 'runtime-process-memory',
'runtime-acme-mode', 'runtime-python', 'runtime-pip', 'runtime-certbot',
'runtime-acme-directory', 'runtime-data-directory', 'runtime-database-path']) has(tab, `id="${field}"`);
assert.ok(!tab.includes('<form'), 'The System information page must be read-only');
const admin = html.split('id="settings-page-administration"')[1].split('id="settings-page-system-info"')[0];
assert.ok(!admin.includes('runtime-information-card'), 'Runtime data must be relocated, not duplicated');
// Execute the real status-to-settings rendering code against a synthetic status payload.
const code = js.slice(js.indexOf(' const runtime = latestStatus?.runtime || {};'), js.indexOf(' const policy = envelope.config.tls?', js.indexOf(' const runtime = latestStatus?.runtime || {};')));
const elements = new Map();
const dollar = id => { if (!elements.has(id)) elements.set(id, { textContent: '' }); return elements.get(id); };
const status = {
version: '0.30.1', uptime_seconds: 4201,
runtime: {
platform: 'Linux', operating_system: 'Debian GNU/Linux 13', kernel_version: '6.12.0', architecture: 'x86_64',
statistics_source: 'systemstat+sysinfo', data_directory: '/var/lib/rustproxy', database_path: '/var/lib/rustproxy/config.sqlite3',
certificate_runtime: { status: 'ready', mode: 'managed', message: 'acme.runtime_ready',
python: true, pip: true, certbot: true, python_version: '3.13.5', pip_version: '25.2',
certbot_version: '5.8.0', managed_directory: '/var/lib/rustproxy/acme-python' }
},
system: { logical_cpus: 8, cpu_percent: 17.2, memory_total_bytes: 16_384, memory_used_bytes: 8_192,
process_rss_bytes: 2_048, system_uptime_seconds: 100, process_threads: 9, open_fds: 67 }
};
const context = {
latestStatus: status, $: dollar, locale: () => 'en-US',
t: key => ({ runtimeAvailable: 'Available', runtimeUnavailable: 'Unavailable',
certificateRuntimeManaged: 'App-managed Python', 'acme.runtime_ready': 'Ready', unavailable: 'N/A' })[key] || key,
bytes: value => `${value} B`, uptime: value => `${value}s`,
};
vm.runInNewContext(code, context);
assert.equal(dollar('runtime-operating-system').textContent, 'Debian GNU/Linux 13');
assert.equal(dollar('runtime-kernel-version').textContent, '6.12.0');
assert.equal(dollar('runtime-version').textContent, '0.30.1');
assert.equal(dollar('runtime-logical-cpus').textContent, '8');
assert.equal(dollar('runtime-cpu-usage').textContent, '17.2%');
assert.equal(dollar('runtime-python').textContent, 'Available · 3.13.5');
assert.equal(dollar('runtime-pip').textContent, 'Available · 25.2');
assert.equal(dollar('runtime-certbot').textContent, 'Available · 5.8.0');
assert.equal(dollar('runtime-acme-directory').textContent, '/var/lib/rustproxy/acme-python');
// A working ACME runtime can have no trustworthy version metadata on some OSes.
elements.clear();
vm.runInNewContext(code, { ...context,
latestStatus: { ...status, runtime: { ...status.runtime, certificate_runtime: {
...status.runtime.certificate_runtime, python_version: null, pip_version: null, certbot_version: null
} } }
});
for (const field of ['runtime-python', 'runtime-pip', 'runtime-certbot']) {
assert.equal(dollar(field).textContent, 'Available');
}
// Status fields remain safely unavailable rather than displaying fake zeros.
elements.clear();
vm.runInNewContext(code, { ...context,
latestStatus: { runtime: { certificate_runtime: { status: 'unavailable', mode: 'none',
message: 'acme.runtime_python_missing', python: false, pip: false, certbot: false } }, system: {} }
});
for (const field of ['runtime-cpu-usage', 'runtime-logical-cpus', 'runtime-system-memory', 'runtime-python']) {
assert.ok(!/NaN|0 B/.test(dollar(field).textContent));
}
// Exercise real tab switching, deep-linking and search code without a browser.
const entry = js.slice(js.indexOf(' const buttons ='), js.indexOf(' const cacheTabs ='));
const buttons = ['administration', 'system-info', 'certificate-api'].map(name => ({
dataset: {settingsTab:name}, textContent:name, hidden:false, tabIndex:-1,
listeners:{}, setAttribute(name,value){this[name]=value;}, addEventListener(name,fn){this.listeners[name]=fn;}, focus(){} }));
const pages = buttons.map(b => ({dataset:{settingsPage:b.dataset.settingsTab}, hidden:true,textContent:b.dataset.settingsTab}));
const search={value:'',listeners:{},addEventListener(name,fn){this.listeners[name]=fn;}};
const searchStatus={textContent:''};
const loc = new URL('https://localhost/settings?tab=system-info');
let lastUrl='';
vm.runInNewContext(`(() => { ${entry} return true; })()`, {
document: {querySelectorAll: sel => sel === '[data-settings-tab]' ? buttons : pages,
querySelector: sel => pages.find(p => sel.includes(p.dataset.settingsPage))},
$: id => id === 'settings-search' ? search : searchStatus,
location: loc, history: {state:null,replaceState:(_state,_unused,url) => {lastUrl=url;}},
CSS:{escape:x=>x}, URL, URLSearchParams, t:x=>x,
});
buttons[1].listeners.click();
assert.equal(pages[1].hidden,false);
assert.equal(pages[0].hidden,true);
assert.equal(buttons[1]['aria-selected'],'true');
assert.equal(lastUrl,''); // Already at canonical deep link; no unnecessary history rewrite.
buttons[0].listeners.click();
assert.equal(pages[1].hidden,true);
assert.equal(lastUrl,'/settings?tab=administration');
const runtimeSchema = api.components.schemas.RuntimeInformation;
for (const key of ['operating_system','kernel_version','architecture']) assert.ok(runtimeSchema.properties[key]);
for (const key of ['python_version','pip_version','certbot_version']) {
assert.ok(runtimeSchema.properties.certificate_runtime.properties[key]);
assert.equal(runtimeSchema.properties.certificate_runtime.properties[key].nullable, true);
}
console.log('PASS: 0.30.1 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Offline source guard for subprocess-free system information; not a Rust runtime test."""
from pathlib import Path
import tomllib
ROOT = Path(__file__).resolve().parents[2]
manifest = tomllib.loads((ROOT / 'Cargo.toml').read_text())
for dependency in ('sysinfo', 'metrics-process'):
assert dependency in manifest['dependencies'], dependency
sysinfo_features = manifest['dependencies']['sysinfo']['features']
assert {'network', 'system'} <= set(sysinfo_features), (
'sysinfo::System and sysinfo::Networks require both system and network features'
)
assert any('systemstat' in target.get('dependencies', {}) for target in manifest.get('target', {}).values())
status = (ROOT / 'src/admin/status.rs').read_text()
assert 'sysinfo::System::long_os_version()' in status
assert 'sysinfo::System::kernel_version()' in status
assert 'std::sync::OnceLock' in status
for forbidden in ('Command::', 'std::process::Command', '/etc/os-release', '/proc/sys/kernel/osrelease'):
assert forbidden not in status, f'Status OS identification cannot use {forbidden}'
metadata = (ROOT / 'src/acme/runtime_versions.rs').read_text()
for marker in ('pyvenv.cfg', '.dist-info', 'METADATA', 'MAX_VERSION_METADATA_BYTES', 'MAX_PACKAGES_PER_SITE', 'fn runtime_versions('):
assert marker in metadata, marker
for forbidden in ('Command::', '.spawn(', 'version_token(', '"--version"', "'-c'", '.await'):
assert forbidden not in metadata, f'Diagnostic versions must not execute: {forbidden}'
runtime = (ROOT / 'src/acme/runtime_env.rs').read_text()
assert 'include!("runtime_versions.rs")' in runtime
assert 'runtime_versions(&venv_python(&managed), true, Some(&venv_certbot(&managed)))' in runtime
assert 'python_version: versions.python' in runtime and 'certbot_version: versions.certbot' in runtime
assert 'path.with_extension("exe")' in runtime, 'Windows executable discovery must include .exe'
regressions = (ROOT / 'src/acme/tests.rs').read_text()
for marker in ('managed_python_pip_and_certbot_versions_use_metadata_only',
'absent_or_unverified_versions_are_not_invented',
'conflicting_site_distributions_do_not_return_stale_versions'):
assert marker in regressions, marker
print('PASS: shared Rust libraries, portable OS detection, no diagnostic subprocesses, bounded metadata and source regressions.')