v0.27.3
This commit is contained in:
1 parent
e84df445c6
commit
e87a382a7e
109 files changed
+1086
-2919
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
|
||||
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
|
||||
compatibility. Current source version: 0.27.2.
|
||||
compatibility. Current source version: 0.27.3.
|
||||
|
||||
## Delivery
|
||||
|
||||
|
||||
@@ -4,6 +4,22 @@
|
||||
> test-only changes and packaging implementation notes were moved to
|
||||
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
## 0.27.3
|
||||
|
||||
- Fix managed Certbot validation and execution: Certbot 5.8.0 is a console-script application and does not provide `certbot.__main__`, so `python -m certbot` incorrectly failed validation. Managed ACME now executes the final `acme-python/bin/certbot` entry point directly; 0.27.3 creates/rebuilds the environment at its final path so the console-script shebang is valid.
|
||||
|
||||
- Improve managed ACME bootstrap on systems with Python/pip but restricted `venv`/PEP 668 behavior: keep installation private, retry the application-local `virtualenv` bootstrap safely when required, and report whether failure happened during environment creation, pip preparation, Certbot installation or validation.
|
||||
|
||||
- Separate application-coupled Python scripts into `scripts/runtime`, `scripts/build`, `scripts/deploy` and `scripts/migration`; move QA/dev/audit helpers to `tools/` and platform-specific OPNsense/RouterOS helpers to `integrations/`, with all entry points and release checks updated.
|
||||
|
||||
- Fix HTTP/3 `HEAD` cache handling by running the same bounded empty-request-body probe used for `GET`; HTTP/2 and HTTP/3 now apply the same cache semantics instead of treating an unpolled H3 body stream as `request_body`.
|
||||
- Reduce persistent COLD-cache index RAM by dropping per-entry `HeaderMap`/validator copies, removing the duplicate key, and storing the SHA-256 digest as 32 bytes. Existing `.json`/`.body` files remain format-compatible; headers and validators are validated and loaded only when a COLD object is actually read.
|
||||
- Reduce rate-limit RAM and stale-IP retention by grouping client buckets under one vhost string, sweeping expired fixed-window buckets during requests and periodic maintenance, and shrinking sparse hash tables before allocator trimming. Active fixed-window behavior and the 65,536-client hard bound are unchanged.
|
||||
- Exclude rebuildable `acme-python` and `acme_python` managed runtime directories from backups.
|
||||
- Confirm response compression remains streaming, uses `CompressionLevel::Fastest`, and does not aggregate complete response bodies; memory use scales with concurrently compressed responses rather than retained cache objects.
|
||||
- Promote `https://rustproxy.linuxiarz.pl/` (DEB/RPM) and `https://rustproxy-opn.linuxiarz.pl/` (OPNsense plugin) as the official documented installation/update paths; keep Debian source build/update as an optional path.
|
||||
- Keep SQLite schema 8, saved configuration compatibility and the COLD on-disk format unchanged.
|
||||
|
||||
## 0.27.2
|
||||
|
||||
- Fix managed ACME issuance/renewal after the 0.27.1 private-Python change: Certbot is now launched through the final managed interpreter (`acme-python/bin/python -m certbot`) instead of the pip console script whose absolute shebang still referenced the temporary `acme-python.installing` path after the atomic directory rename.
|
||||
|
||||
Generated
+1
-1
@@ -1688,7 +1688,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.27.2"
|
||||
version = "0.27.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.27.2"
|
||||
version = "0.27.3"
|
||||
edition = "2021"
|
||||
rust-version = "1.88"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# RustProxy 0.27.2
|
||||
# RustProxy 0.27.3
|
||||
|
||||
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
|
||||
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
|
||||
|
||||
**0.27.2 release source.**
|
||||
**0.27.3 release source.**
|
||||
|
||||
## Layer-4 streams
|
||||
|
||||
@@ -11,14 +11,18 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a
|
||||
|
||||
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
|
||||
|
||||
## What changed in 0.27.2
|
||||
## What changed in 0.27.3
|
||||
|
||||
- Fix ACME issuance and renewal with the RustProxy-managed Python environment introduced in 0.27.1.
|
||||
- Run managed Certbot through the final venv interpreter (`acme-python/bin/python -m certbot`) instead of the pip console script whose shebang still points at the temporary `acme-python.installing` directory after the atomic rename.
|
||||
- Validate the managed runtime through the same module launch path, so an existing 0.27.1 `acme-python` directory starts working immediately after upgrade; no manual venv deletion/rebuild is required.
|
||||
- Keep the distribution Certbot fallback, SQLite schema 8 and saved configuration format unchanged.
|
||||
- Treat body-less `HEAD` requests consistently on HTTP/2 and HTTP/3 cache paths. HTTP/3 `HEAD` now gets the same bounded empty-body probe as `GET`, preventing false `request_body` bypasses.
|
||||
- Reduce COLD-cache RAM usage without changing the on-disk cache format: response headers and validators are loaded only on an actual COLD read, SHA-256 digests are stored as 32-byte values in the in-memory index, and the duplicate cache key is removed from each COLD entry.
|
||||
- Reduce rate-limit/IP memory retention by storing each vhost name once, reclaiming expired client buckets during traffic and periodic maintenance, and shrinking sparse hash tables before the existing allocator trim runs.
|
||||
- Exclude the rebuildable managed Certbot environments `acme-python` and `acme_python` from application backups.
|
||||
- Keep response compression streaming with no full-body aggregation. Compression uses the fastest codec level and keeps only per-active-response encoder state, so it is not a long-lived cache-like RAM owner.
|
||||
- Make the DEB/RPM and OPNsense package repositories the documented official installation paths; Debian source builds remain available as an optional path.
|
||||
- Separate application-coupled scripts (`scripts/`) from developer/operator helpers (`tools/`) and external platform integrations (`integrations/`) without changing runtime behavior.
|
||||
- Keep SQLite schema 8, the COLD on-disk format and the saved configuration format unchanged.
|
||||
|
||||
0.27.2 retains the 0.27.1 first-admin, managed-runtime UI and route-list sorting changes plus the 0.27.0 proxy/compression and Docker-publication behavior. Historical details are in [CHANGELOG.md](CHANGELOG.md).
|
||||
0.27.3 retains the 0.27.2 managed-ACME repair and earlier administration/proxy behavior. Historical details are in [CHANGELOG.md](CHANGELOG.md).
|
||||
|
||||
### Historical 0.26.7 release notes
|
||||
|
||||
@@ -36,7 +40,7 @@ Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost
|
||||
- Keep backend diagnostics visible-column selection, sort key and sort direction synchronized with the account-scoped administrator preferences whenever preferences are loaded or updated.
|
||||
- Clear the ACME certificate-creation form after the issuance job finishes successfully, so successful certificates do not leave stale values while failed jobs keep the non-secret form values available for correction.
|
||||
- Compose explicit IP allows and **Allow only** countries as alternatives inside one ACL policy scope. Explicit IP denies and blocked countries still win, and profile/vhost/location scopes remain cumulative. This makes `default=deny` support private CIDRs **or** selected countries as intended.
|
||||
- Document the sibling `migrate_npm_core.py` and `migrate_npm_api.py` modules required by `scripts/migrate_npm.py`, and fix the container example to mount the complete `scripts` directory.
|
||||
- Document the sibling `migrate_npm_core.py` and `migrate_npm_api.py` modules required by `scripts/migration/migrate_npm.py`, and fix the container example to mount the dedicated `scripts/migration` directory.
|
||||
- Detect the Linux runtime environment in Process diagnostics. LXC reports `Linux / LXC` and overrides system uptime with PID 1 age plus load average from the container-visible `/proc/loadavg`; VM/host/Docker and other Linux environments keep the existing host statistics path.
|
||||
- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.5 requires no data migration.
|
||||
|
||||
@@ -86,7 +90,7 @@ Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost
|
||||
- Replace backend sort text (`A-Z` / `Z-A`) with direction arrows (`↑` / `↓`).
|
||||
- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.1 requires no data migration.
|
||||
|
||||
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.27.2 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
|
||||
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.27.3 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -118,9 +122,19 @@ Read [installation and update](docs/INSTALL.md), [host access and locations](doc
|
||||
Examples are disposable full configurations: applying one replaces, not merges,
|
||||
an existing document.
|
||||
|
||||
## Requirements and first build
|
||||
## Installation and source build
|
||||
|
||||
### Debian 13+ one-line install/update
|
||||
### Official package repositories
|
||||
|
||||
The preferred installation/update path is the project-maintained package repositories:
|
||||
|
||||
- **DEB/RPM packages:** `https://rustproxy.linuxiarz.pl/`
|
||||
- **OPNsense plugin:** `https://rustproxy-opn.linuxiarz.pl/`
|
||||
|
||||
Use the repository instructions appropriate for the target operating system. Building on Debian is
|
||||
kept as an optional source/development path rather than the primary installation method.
|
||||
|
||||
### Optional: Debian 13+ one-line source install/update
|
||||
|
||||
The bootstrap installs the compiler/build dependencies and Rust, clones this repository and
|
||||
builds the locked release. With no argument it runs in `auto` mode: a host without a managed
|
||||
@@ -164,9 +178,9 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \
|
||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked
|
||||
```
|
||||
|
||||
Ready Debian packages are published separately at `https://rustproxy.linuxiarz.pl`; Debian package-build metadata is intentionally not shipped in this source tree.
|
||||
Package-build metadata remains outside this source tree and is maintained with the official package repositories above.
|
||||
|
||||
The 0.27.2 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
|
||||
The 0.27.3 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
|
||||
updates only this package's own version entry. This packaging environment has no Cargo, so the
|
||||
dependency graph was not re-resolved here. On the compiler-enabled deployment host, run the
|
||||
locked check/test/release gates below and retain the verified lockfile. `dev.sh` can generate a
|
||||
@@ -381,7 +395,7 @@ bash dev.sh
|
||||
```
|
||||
|
||||
The supported operator backup is the password-encrypted `rust-proxy backup create`
|
||||
command. `scripts/backup.py` is retained only as an offline release-test helper and
|
||||
command. `tools/legacy/backup.py` is retained only as an offline release-test helper and
|
||||
must not be used as a Secret Store backup. See
|
||||
[Secret Store and encrypted backup/restore](docs/SECRET_STORE_BACKUP.md).
|
||||
|
||||
@@ -398,6 +412,16 @@ behavior when omitted. Once a 0.25.3 configuration containing the new fields is
|
||||
binary may reject unknown configuration fields. Roll back using the pre-upgrade data backup as
|
||||
well as the old source, not just the binary.
|
||||
|
||||
## Source helper layout
|
||||
|
||||
`script` responsibilities are intentionally separated:
|
||||
|
||||
- `scripts/runtime`, `scripts/build`, `scripts/deploy`, `scripts/migration` are coupled to application/build/deployment behavior;
|
||||
- `tools/checks`, `tools/dev`, `tools/audit`, `tools/legacy` are auxiliary tools and are not needed by the running proxy;
|
||||
- `integrations/` contains external platform helpers such as OPNsense ACME and RouterOS OCI tooling.
|
||||
|
||||
See `scripts/README.md`, `tools/README.md` and `integrations/README.md`.
|
||||
|
||||
## Documentation and checks
|
||||
|
||||
[documentation index](docs/README.md), [API](docs/API.md), [architecture](docs/ARCHITECTURE.md),
|
||||
@@ -407,12 +431,12 @@ well as the old source, not just the binary.
|
||||
|
||||
```bash
|
||||
# Rust-independent checks:
|
||||
python3 scripts/check_source.py
|
||||
python3 scripts/release_smoke.py
|
||||
python3 tools/checks/check_source.py
|
||||
python3 tools/checks/release_smoke.py
|
||||
# Runs those checks first, then the Rust suite when Cargo is available:
|
||||
bash dev.sh test
|
||||
# Source-only package, with no Git checkout required:
|
||||
python3 make_zip.py /tmp/RustProxy-0.27.2.zip
|
||||
python3 make_zip.py /tmp/RustProxy-0.27.3.zip
|
||||
```
|
||||
|
||||
## New management and test-origin guide
|
||||
|
||||
+14
-17
@@ -1,5 +1,18 @@
|
||||
# RustProxy roadmap
|
||||
|
||||
## 0.27.3 - HTTP/3 cache parity and bounded memory retention
|
||||
|
||||
Current direction: 0.27.2 -> 0.27.3
|
||||
|
||||
- [x] Make body-less `HEAD` cache eligibility consistent between HTTP/2 and HTTP/3.
|
||||
- [x] Reduce persistent COLD-index RAM while preserving the existing on-disk cache format and cache behavior.
|
||||
- [x] Reclaim expired rate-limit client buckets and avoid duplicating the vhost name for every tracked IP.
|
||||
- [x] Confirm downstream compression remains streaming and does not retain complete response bodies in memory.
|
||||
- [x] Exclude rebuildable managed Certbot Python environments from backups.
|
||||
- [x] Document DEB/RPM and OPNsense repositories as the official installation paths; keep Debian source builds optional.
|
||||
- [x] Separate application-coupled scripts from QA/dev/audit tooling and external platform integrations while preserving all entry points.
|
||||
- [x] Keep SQLite schema 8 and saved configuration compatibility with 0.27.2.
|
||||
|
||||
## 0.27.2 - Managed ACME renewal repair
|
||||
|
||||
Current direction: 0.27.1 -> 0.27.2
|
||||
@@ -18,7 +31,6 @@ Current direction: 0.27.0 -> 0.27.1
|
||||
- [x] Add account-scoped column + direction sorting for vhosts, Docker vhosts, redirects, 404 hosts and streams; keep Redirects/404 toolbar placement identical to Vhosts.
|
||||
- [x] Use the installed production binary in the Certbot hook example and expose official source/package repository links plus Python/pip/Certbot runtime state in application information.
|
||||
- [x] Keep SQLite schema 8 and additive/defaulted administrator-preference compatibility.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.27.0 - Compression policy and persistent Docker publications
|
||||
|
||||
@@ -32,7 +44,6 @@ Current direction: 0.26.7 -> 0.27.0
|
||||
- [x] Keep published Docker hostnames materialized while a service is missing, return 503 until it returns, and reuse last-known inventory during temporary agent communication failures.
|
||||
- [x] Render cache occupancy progress fill consistently across Chromium/WebKit and Firefox.
|
||||
- [x] Keep SQLite schema 8 and additive/defaulted saved configuration compatibility with 0.26.x.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.26.7 - Cache occupancy and backend rotation continuity
|
||||
|
||||
@@ -45,7 +56,6 @@ Current direction: 0.26.6 -> 0.26.7
|
||||
- [x] Review current installation/cache/backend lifecycle documentation for stale behavior statements.
|
||||
- [x] Keep existing 0.26.6 custom `Age` rules loadable while isolating response-side Age overrides from cache freshness/storage semantics.
|
||||
- [x] Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.6.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.26.6 - Access-policy composition and administration fixes
|
||||
|
||||
@@ -53,10 +63,9 @@ Current direction: 0.26.5 -> 0.26.6
|
||||
|
||||
- [x] Keep backend table visible columns, sort key and sort direction synchronized with account-scoped user preferences.
|
||||
- [x] Clear the certificate-issuance form after the ACME issuance job succeeds while preserving non-secret values after failed jobs.
|
||||
- [x] Document the sibling Python modules required by `scripts/migrate_npm.py` and fix the container helper example.
|
||||
- [x] Document the sibling Python modules required by `scripts/migration/migrate_npm.py` and fix the container helper example.
|
||||
- [x] Treat explicit IP allows and allow-only countries as alternative allow sources inside one ACL policy scope, with explicit denies taking precedence and separate scopes remaining cumulative.
|
||||
- [x] Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.5.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.26.5 - Scaled controls and listener navigation
|
||||
|
||||
@@ -66,7 +75,6 @@ Current direction: 0.26.4 -> 0.26.5
|
||||
- [x] Reuse the interface-scale field variables for ordinary form controls so inputs/selects resize consistently with the UI scale setting.
|
||||
- [x] Route the Streams **Add TCP/UDP listener** shortcut to Settings -> Listeners instead of opening a Stream editor.
|
||||
- [x] Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.4.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.26.4 - Administration consistency and backend rotation reliability
|
||||
|
||||
@@ -79,7 +87,6 @@ Current direction: 0.26.3 -> 0.26.4
|
||||
- [x] Replace the Overview-card checkbox list with compact visibility/reorder tiles.
|
||||
- [x] Run age-based backend client recycling independently of request arrival.
|
||||
- [x] Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.3.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.26.3 - Administration appearance and draft workflow polish
|
||||
|
||||
@@ -98,7 +105,6 @@ Current direction: 0.26.2 -> 0.26.3
|
||||
- [x] Add an account-scoped Normal/Compact button-style selector, with Compact matching the smaller source-style control dimensions independently from global scale, including generated vhost actions and modal tabs.
|
||||
- [x] Retain sticky Settings category navigation and unsaved-category indicators.
|
||||
- [x] Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.2.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.26.2 - Unified logs search and dynamic fields
|
||||
|
||||
@@ -114,7 +120,6 @@ Completed direction: 0.26.1 -> 0.26.2
|
||||
- [x] Parse the built-in RAW error-log line format into basic structured fields while retaining the raw record.
|
||||
- [x] Remove the configured-file inventory/source selector from the Logs workspace and keep bounded all-source scanning by default.
|
||||
- [x] Move Stored Logs column configuration to the table toolbar and use arrow symbols for backend sort direction.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.26.1 - Vhost-aware protocol diagnostics and Logs Explorer UX
|
||||
|
||||
@@ -137,7 +142,6 @@ Completed direction: 0.25.7 -> 0.26.0
|
||||
- [x] Prevent arbitrary-path browsing and symlink following; enumerate only paths derived from active RustProxy logging configuration.
|
||||
- [x] Add `/logs` dashboard navigation plus a Logging-settings launcher while retaining the existing Live Logs dialog unchanged.
|
||||
- [x] Keep schema 8 and configuration compatibility with 0.25.7.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.25.7 - Vhost operations and backend observability
|
||||
|
||||
@@ -149,7 +153,6 @@ Current direction: 0.25.6 -> 0.25.7
|
||||
- [x] Export retry and upstream-client lifecycle metrics for ordinary and Docker backends.
|
||||
- [x] Add Grafana panels for backend client generation, age, retry activity and rotations.
|
||||
- [x] Make the vhost editor Enabled control visually distinct from ordinary settings.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.25.6 - Backend memory diagnostics visibility
|
||||
|
||||
@@ -158,7 +161,6 @@ Current direction: 0.25.5 -> 0.25.6
|
||||
- [x] Show upstream client generation, current-generation request count, generation age and total rotations in the main Backends diagnostics table.
|
||||
- [x] Reuse the existing `/api/events` status snapshot refresh path; do not add a second diagnostics polling loop.
|
||||
- [x] Keep the 0.25.5 client-generation recycling defaults and configuration compatibility unchanged.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.25.5 - Traffic country consistency and Revision History styling
|
||||
|
||||
@@ -171,7 +173,6 @@ Current direction: 0.25.4 -> 0.25.5
|
||||
- [x] Extend Prometheus with a live Traffic analytics collector, fixed-cardinality User-Agent/method/source families, country/top-route views and built-in Web Server inventory/source metrics.
|
||||
- [x] Extend the bundled Grafana dashboard with a Traffic vhost filter, Web Server request rate, countries, User-Agent families, serving sources, methods, status classes, cache outcomes, capture-state gauges and configured routes.
|
||||
- [x] Bound backend Hyper client lifetime with configurable request/age recycling and an upstream HTTP/1 buffer ceiling so sustained traffic can release pool/TLS high-water allocations without a vhost save.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.25.4 - Configuration history restore
|
||||
|
||||
@@ -181,7 +182,6 @@ Current direction: 0.25.3 -> 0.25.4
|
||||
- [x] Save restore as a new revision without deleting or rewinding existing history.
|
||||
- [x] Require current-revision CAS plus administrator session/CSRF and audit restore source/new revision.
|
||||
- [x] Add restore action and confirmation to the existing revision-history modal.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.25.3 - Request policy, correlation and configuration history
|
||||
|
||||
@@ -193,7 +193,6 @@ Current direction: 0.25.2 -> 0.25.3
|
||||
- [x] Generate one SHA-256 request ID per exchange and forward it unchanged to the backend as `X-Request-ID`.
|
||||
- [x] Persist bounded manual-configuration revision history in SQLite schema 8.
|
||||
- [x] Add a calendar/day revision-history modal with change preview in the existing dashboard style.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||
|
||||
## 0.25.2 - Live backup and resumed-session hardening
|
||||
|
||||
@@ -204,7 +203,6 @@ Current direction: 0.25.1 -> 0.25.2
|
||||
- [x] Preserve only validated relative symlinks that resolve inside the staged data tree, including Certbot `live` -> `archive` links.
|
||||
- [x] Keep restore offline, validated, staged and atomically activated.
|
||||
- [x] Recover transient invalid-origin state in restored/resumed admin tabs with a one-shot same-origin reload, without weakening server Host/Origin/CSRF checks.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
|
||||
|
||||
## 0.25.1 - Traffic UX and source modularization
|
||||
|
||||
@@ -215,7 +213,6 @@ Current direction: 0.25.0 -> 0.25.1
|
||||
- [x] Keep every Rust source file at or below 500 lines and keep unit tests outside implementation files.
|
||||
- [x] Split oversized dashboard source bundles and the Docker Agent into bounded source modules while preserving behavior and URLs.
|
||||
- [x] Guard the Web Server split against dangling attributes and missing `htaccess`, `server_protocol` and `anyhow::Context` imports.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
|
||||
|
||||
## 0.25.0 - Web Server foundation
|
||||
|
||||
|
||||
+107
-101
@@ -1,18 +1,18 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
4595fb01446f9e9e08a459085dea96c2b27a9f0aff0fab1f40c8a62e95761902 AGENTS.md
|
||||
49f5f9b0490a8b411f16f8a1a2477e96f832d6d21155727bb80c4895f9877ea1 CHANGELOG.md
|
||||
3002fea4dac2c78c11003b6ef669e53415702337062ab737958ac48471e24422 Cargo.lock
|
||||
5823157f89f6d8576a038866ff5bc2be8733f226e345f4ff9be68d1b59d1d34c Cargo.toml
|
||||
48a06923863f33c6a350dd66da8677437d184f4797b0a813930f5daa8cb23141 .gitignore
|
||||
b17968dc24b2841ee7bf107f54e488a858afd888efea4bc89ec8ad5941dc494a AGENTS.md
|
||||
dca9fc8a1eb1a4171e92b695b2eb58896c8701dbbdc3acbfc44b380d8a9fb5ab CHANGELOG.md
|
||||
948e0848bc426a15a40e7dab02817242417d8633ca9c3d89a7dc8e8da23c4d62 Cargo.lock
|
||||
1a27b4fc0950feac78dd61695131211a16179cc47dab327c3d556a3b3109ccb8 Cargo.toml
|
||||
75b008f237b2226c62a7721b7901018ce83bfc2726f047a83a17c66fcb799f3b Dockerfile
|
||||
3ef21bb67c050fb7189a032e55e4794c4e07a1b20bcdc3f2f6222f5a37fa8fb5 README.md
|
||||
e62a31c08cc123b39031e95d10d149d36dbccf73a78a18a7578bb88521c89983 ROADMAP.md
|
||||
1725a09ad0dd2adff1aa32f125207fa4bfa6cbf051acbd3683c66e5961e6ad65 README.md
|
||||
b6874ca7ddab94c40ddc0da3455e02e6e429292a092ff7152da88490c9cad9fc ROADMAP.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
|
||||
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
||||
385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md
|
||||
31d4e0cd454060b46f359fdb84a14b16a5fe0d3baabb387dd584d76e555558bb deploy/rustproxy.service
|
||||
c90c8a484ff15bb516a810c01e19260ba941b652d5d67afc4405f444ab3b06ab dev.sh
|
||||
8b8adb663f9b72e3f8303f4b68d3f401b4fda82f38e0d9e5f6b2c21919ebca1b dev.sh
|
||||
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
|
||||
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
|
||||
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
|
||||
@@ -21,46 +21,46 @@ bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/d
|
||||
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
|
||||
b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go
|
||||
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go
|
||||
4e9eb90294d936e0e4f63618bbadd54b8063ec73d4beda9aa4d70d768b16ab97 docker-agent/model.go
|
||||
c49dae9fd82dbad4db0f377e9d2b54debb5fde2a83a08fbae30cc942c79e04ac docker-agent/model.go
|
||||
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
|
||||
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
||||
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
||||
875faa3969df9409d79f050a84b4183496f557edda3d34aaec19fe556cf8c471 docs/ACME_PROVIDERS.md
|
||||
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
|
||||
c7b75ed137094cade2b2047c1c8d304e936e0fe1121ff91a858f77c2e969fa39 docs/API.md
|
||||
d7b224425930cf06cf34d0547e445e0363f6ac4aced43e182d1d88faa2d28cdb docs/API_ACCESS.md
|
||||
0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md
|
||||
86469d17f9786c3e6f20c66791b47e10e11695d2f1a88c66c9ded157a585c85f docs/CACHE.md
|
||||
557d82ec1cf2295d6a41ffed1d862448c1f9441554f1fc07f8a12324f168901d docs/API_ACCESS.md
|
||||
4c5288c9ed427cfb919bd023ed9a321dcf6d64bc4cad3e1768393356c5d835ba docs/ARCHITECTURE.md
|
||||
4abdcc53efdb35cdd8feb024dbb81188da12c6abd37eefddecdb57a7eb93d6e2 docs/CACHE.md
|
||||
6266163e17d2921468abdae92c1620266af8351321530e2c9b7dca6f3dd89c88 docs/CACHE_DIAGNOSTICS.md
|
||||
c6003dcbc0b95e2cfa9523b7017186cc8b4826248b588af77111ac5eb15174f9 docs/CHANGELOG_TECHNICAL_ARCHIVE.md
|
||||
0a92a2d4ee88d02e9b460c680305680227c3ef82cf6b57d6f92b1ef5a6433154 docs/CONTAINER.md
|
||||
17d9d2275d2838477e7542b164ebd41ad118ef06afdaccde36c95f8a522e5939 docs/CONTAINER.md
|
||||
9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md
|
||||
16d8f59908c66e595d6003495cbf5c068c68b10aa9495eb8612c38b2de0e4a7c docs/DOCKER_DISCOVERY.md
|
||||
9ea8db4d034ecb573976dd323da29f044538525c230df9f43a47be1ac867b24c docs/ERROR_LOGGING.md
|
||||
5c46b311f48e143a3e43e61a69c8bc3cfad7748c7dfef87bd76df333b64023bd docs/FILE_LOGS.md
|
||||
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
|
||||
10f378674dced022f063bfc1b15dc9a5cfc0afd03639deb4db5de091c139288c docs/INSTALL.md
|
||||
851e66e5dac47cfc70d963833d137fa0e35c04c374e9b943ed9b7345a51802fa docs/FILE_LOGS.md
|
||||
8dfc89a8b3512103b87864bd7c24a3dc3de4465edf8f9874e6bed1f4634dd930 docs/HTTP3.md
|
||||
5b48bc23ffa2dbd1ebd2ed5cae9e6022dc9c0795a2d0df9565816c11c0096421 docs/INSTALL.md
|
||||
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
|
||||
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
|
||||
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
|
||||
5f98c4006e89b432904e0b65ece6aa5df4befd6dc8e00024eb67b4ad901ba14d docs/MIGRATION.md
|
||||
9583b0b69f81320e2227f44a853259e7a600846bbed681ac54896d83befd7b3b docs/MIGRATION.md
|
||||
5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md
|
||||
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
|
||||
e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md
|
||||
0c2241828e01b12f749562e2ac206a4519908fff63f46eba207b1cffa932dc5b docs/NPM_MIGRATION.md
|
||||
89a1af6c9fe10c572b211f9632ce256166e67990b5dacacf2ae07265d99a5172 docs/NPM_MIGRATION.md
|
||||
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md
|
||||
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
|
||||
a5faed54574e23cc5940205883a9731aa1572c2499783ecba138a621fcfb4a42 docs/README.md
|
||||
4cf425aabd16366df99e447d3eba2a362babeff3de7d014e189df51def657039 docs/README.md
|
||||
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
|
||||
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
|
||||
be661dbf81af840d4d8aeeb5347c24c7faecdecc73a8cf0f6f09053f1abe04c1 docs/RENEWAL.md
|
||||
585ef0ac4492e03adca46949823e5a7538f56d9768090cd4dc080bfce4305b52 docs/ROADMAP_HISTORY.md
|
||||
4bf7b3ea0bfe6c6b0ae0b7884b48b10ba29ff3a71611ec96957a3327d3c6ec88 docs/SECRET_STORE_BACKUP.md
|
||||
a2bb837cdd3559d58158880c1f832badf64035376a3623050bb4e40c2bca104c docs/ROADMAP_HISTORY.md
|
||||
2b760c88e0e093d7705820560fbacf30bfc7bf6d67f2e68a500b3d4d970d80b6 docs/SECRET_STORE_BACKUP.md
|
||||
5dbdf45fddba198a28f511dbbd2f12bf7d2b88c41c3cbedbc20dfea3858779d3 docs/SECURITY.md
|
||||
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
||||
632976125cbd9e96e91d1771331c73d1ac5adaa796476a20f8546971787c2529 docs/SETTINGS.md
|
||||
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
|
||||
6a6f48614fcf64b675ef0b7efb49f65878da3f42a6ee110f0d608c70e737be2a docs/TEST_REPORT.md
|
||||
cbf3e7cfd32c50026df333489a8afb5056e873a9360bbe135b5ded5fadc4318b docs/TEST_REPORT.md
|
||||
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
||||
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
|
||||
a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md
|
||||
@@ -75,15 +75,15 @@ ab7d48c2a359928da653263e69b4739cfff849a03fa1270491cb1ce553989788 docs/V0.23.1_A
|
||||
e4d47b50fb0ce33d26817fc0afcc2f05ac53bc9b62c7e50e61b9fa9734e817b0 docs/V0.23.3_AUDIT.md
|
||||
984092eeb3899ed696f94059d88f7834703b151682d5c8af9fabacc7a823ea0b docs/V0.24.0_AUDIT.md
|
||||
5782adf99a292783149882e844ad07b49d4ac0ea478e8393758cff849d48c13d docs/V0.24.1_AUDIT.md
|
||||
521d6ef17bb0df7ec05f068411ed96a19e579c68b8e166dc314878d2c3cb0627 docs/V0.24.3_AUDIT.md
|
||||
cc64ae2b85add081deb043f144caa4f10f6576475eada65302a56296ce2d75a1 docs/V0.24.3_AUDIT.md
|
||||
b891db1453cd7cf6b10f79879f4edaf7c83f14e91e4e95826919d40cb5dca2d4 docs/V0.24.4_AUDIT.md
|
||||
8d6519b00e01e5cd41ee6772941fdc7e9a70fe29626df71cefced6eefe6a94d0 docs/V0.24.5_AUDIT.md
|
||||
4d2d8b84b38df57f5c6a03995019f8418806fe5867ee34ce192a90ef561986a8 docs/V0.24.6_AUDIT.md
|
||||
081f655554c62911b81719343e905f012356bf1df3054bda3ccd1a9bdffa89af docs/V0.24.7_AUDIT.md
|
||||
f6ffaa9b72d86029b1629f8fd89efeac99780831f95d434b2ad549913633dcb9 docs/V0.24.6_AUDIT.md
|
||||
1c9f3ab84700482820379268ea367600a0c110a4578504f0cc2ce54149fe1046 docs/V0.24.7_AUDIT.md
|
||||
d0e609febbcd1cfdb8ea0b00b4fa6f081eac01e2d43a81e38a53ae25783152cc docs/V0.24.8_AUDIT.md
|
||||
6131ef0ac65af2c9544d9d44ecfb680587cb358b37efd9b2d1cf69f26e935861 docs/V0.25.0_AUDIT.md
|
||||
1a8a07eb150100e6d5ab8443dac664038e1d838033609df168aaf60a15aeb84f docs/V0.25.0_SECURITY_PREPROD_AUDIT.md
|
||||
32c4053d48e8f24eb6c3395a4bbbc18f1c9790601166c73fb83173a14142789e docs/V0.25.1_AUDIT.md
|
||||
a840652a660adeb93999c01ae285f01fd13cdecff69f3ae58ddf9ade9acca258 docs/V0.25.0_AUDIT.md
|
||||
e5a08bf5d34338a3e5f3b14b65c6f3630ccb0ef9c56c26f39bbb73fa724bf227 docs/V0.25.0_SECURITY_PREPROD_AUDIT.md
|
||||
a709ec5e740b3da73149ba9843e3a184611f8be74e06bd19105684a57e3e68f8 docs/V0.25.1_AUDIT.md
|
||||
41793fe2d0fa4a2bd88448f5b3870649f08eeaadd4cd420820647d332e7fdb5a docs/V0.25.2_AUDIT.md
|
||||
9dbb66e408bcd4d32267724e557fa9f7f44ef9b70db4ab170c61a7a839b7b157 docs/V0.25.3_AUDIT.md
|
||||
583d548b4a5db9f35a2bbf8ec00ec22397764c5850a8e883a3d12680da517df7 docs/V0.25.4_AUDIT.md
|
||||
@@ -93,11 +93,12 @@ bf9b4cf4fec2c8ce287e1b3c2e7d4828e101dafe4d6c173f099819f22493d144 docs/V0.25.5_A
|
||||
57edc3d5ca6c268b2b6b2e8027e81fb543c0dab86e8f69c4ee5776ea8df901c7 docs/V0.26.3_IMPLEMENTATION.md
|
||||
da5ec87b9482f2fc506c4334d9365f3bfaec89254a14e5585dd6460326b20e10 docs/V0.26.4_IMPLEMENTATION.md
|
||||
0ddf5215c40c8366844350bc6462d8013dba747b6f3e8fbf477985af215e5e8d docs/V0.26.5_IMPLEMENTATION.md
|
||||
dd9b5dcdbd53a04fd32458caaf03b56aa335f0153b1a0e332fdf230fec12680e docs/V0.26.6_IMPLEMENTATION.md
|
||||
c7785826cf7b20555ece5b23a3f360ce4a8a014cf976c14336770f74d630834f docs/V0.26.6_IMPLEMENTATION.md
|
||||
f9667fe81b31762e64d36126a7e94b609316dfa856de8f8e3b96661dd2a918c6 docs/V0.26.7_IMPLEMENTATION.md
|
||||
efd5935852d4cfd09758bf67c70e437490cfce2baade46c8733201e506dbcc42 docs/V0.27.0_IMPLEMENTATION.md
|
||||
9ba6417417a0eb293879b26b019449f143e773d1482434bee88f5eaf1fe34b90 docs/V0.27.1_IMPLEMENTATION.md
|
||||
6aafee24d7badabe00e23554c8c1ec868f586e09f2d915e9f48422639a7348fb docs/V0.27.2_IMPLEMENTATION.md
|
||||
845cc7b7fcd553ef99613817e70fcecc4e50dd8fa77066a75577d63a7224cdec docs/V0.27.2_IMPLEMENTATION.md
|
||||
86cf7498a031ed6aff8a79140767eaa07b696669623900b971f66ca5392e5174 docs/V0.27.3_IMPLEMENTATION.md
|
||||
bc2ac0117528ed4616c68cf7fe05cb1d9bbd36ecaa8573828cb3b3136923f265 docs/VHOSTS.md
|
||||
09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md
|
||||
4026a4adeee70f98a96cc7aa8d8be16cda42ffb2e5890d38f1ba638134ab0675 docs/WEB_SERVER.md
|
||||
@@ -112,14 +113,23 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
|
||||
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
|
||||
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
|
||||
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
|
||||
cc99b14927c696adc17c9d0689041798f0a99cbc372b35a81652d6e38af69311 grafana/README.md
|
||||
b6401012c55a9cdf7ca7ac5f90b4142c623230903877309e427142de1a81f9d0 grafana/rustproxy-overview.json
|
||||
89e53a8f7098bbbe64a5315dde365a4346ef102b0a8e457b941cacf7b843e04d grafana/README.md
|
||||
e87ac9f20310c265ce580c0adcef6403728452f98f12ddba603859ff0dd34458 grafana/rustproxy-overview.json
|
||||
fe1fc76bcb639cac0609a8969cd92725cfd77d87f1d8519756ef16825e04914a install-debian13.sh
|
||||
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
|
||||
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
|
||||
917efd1646d06ac34c5729ce5a569482d7512cf285dfc603bca24b3608d36e7c langs/en_US.json
|
||||
bab5f124ee1e1e823fe310217560c2abaff8a07c9c23a4daf7ab0f60d766eb4b langs/pl_pl.json
|
||||
300b8cbb49e44d6ce11a4445df01b1a7467b81acfafa9c2d2ba62b5a611501e4 make_zip.py
|
||||
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
|
||||
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
|
||||
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 integrations/opnsense-acme/README.md
|
||||
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 integrations/opnsense-acme/opnsense-rustproxy-cert-deploy.sh
|
||||
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 integrations/routeros-oci-build/.env.routeros.example
|
||||
cdb15e23f04b4a330b7ab8869eb38da72b3c27729fa7fcf164e3d9e8e893a651 integrations/routeros-oci-build/Dockerfile.routeros
|
||||
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e integrations/routeros-oci-build/Dockerfile.routeros.dockerignore
|
||||
cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/routeros-oci-build/README.md
|
||||
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||
71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md
|
||||
d0aa8b66b39b1f0835e558aa6e4c9d860bc9a26187aff887463cff74397b3dc1 langs/en_US.json
|
||||
69f8b2c88a04f8a5d32290df35040e1619513dfb873a9ef4bc5dfbd655ce3316 langs/pl_pl.json
|
||||
113458f12c516f1e4516a341d6e558cee8a621ac16ca23e9bbd865ccde9e1f75 make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
|
||||
@@ -134,43 +144,17 @@ ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_r
|
||||
1465396cf1358067e9fe48b3d61f28fd4ac65c577b67c502950ce6b42ecbe3b3 nagios/check_rustproxy_latency
|
||||
6e036f736a2b40feb78eec17bf34871771524190f283591d1c89e48e6d2271e6 nagios/check_rustproxy_queues
|
||||
7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml
|
||||
5d4ae015f1c8199c2ed02b34576b74ec6f2b3b7a90c8d7178e5070a850bb6e72 scripts/acme_provider.py
|
||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
|
||||
a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py
|
||||
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
|
||||
6e656850ca0bafae0aeb2fdf3b8f7e9342b027830df8e4ca19190a8b1cfeb7a9 scripts/check_source.py
|
||||
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
|
||||
6960ff75a235289d270a9d8e7205a4432b51d3bca83e2c599bd20bcc90caac81 scripts/deploy_core.py
|
||||
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
|
||||
fc1a74246478cb477b3892812634cb28f8b8ad45604a098327f7a14a60f25c96 scripts/docs_check.py
|
||||
d391681dc3f6786552ca44929979286a5c76a2b86785635871b59d2eb4d73389 scripts/langs_check.py
|
||||
2e35b7c78887fd56f83ca9017caa168630963a9f47da71dfc53b1a8f9ad436c2 scripts/memory_contracts.py
|
||||
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py
|
||||
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migrate_npm_api.py
|
||||
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migrate_npm_core.py
|
||||
d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a scripts/mock_auth.py
|
||||
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py
|
||||
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md
|
||||
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh
|
||||
65ee67092955aae5216c304453f88066ad9f51dc27e682f36866540eebbf621b scripts/preflight.py
|
||||
1df736690e3de94f1ddc363ecef9e0e86acee4e98a78c30b82936324f4747484 scripts/release_smoke.py
|
||||
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example
|
||||
3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros
|
||||
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore
|
||||
68c076ef4537a047a7c390c534dbadd90e00cf495931c6d739f1c28b6b4dc7df scripts/routeros-oci-build/README.md
|
||||
d53b2ae7315ac143bd17d6327611a39e7c23666408bb95e39da2ca8517322ccb scripts/routeros-oci-build/build-routeros-oci.sh
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a scripts/routeros-oci-build/docker-entrypoint.sh
|
||||
d36a5f5532f7d458b4e8249acf0fdf733b3c9531e9a590ea8d1b1dddec8fa7ef scripts/rust_module_contracts.py
|
||||
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_source.py
|
||||
1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py
|
||||
295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py
|
||||
6fefb5c459aacdef3dcabe9c5e6e7718a2145a0190b165c29d7a46216ac53bad scripts/source_module_contracts.py
|
||||
df2bffcd729a998c5c7ec5452235cc07e834d8f86017d65ccd0b1754d91d11b0 scripts/swagger_assets.py
|
||||
538b5f6cfa738355a729940ee3d92e7ab3dc25ca69ae330ddaa08f831775b8ba scripts/test_compression.py
|
||||
45a6a4cca69419da94b2389d73d3f1eb42ccc4aafa5825da14242eb236bdd963 scripts/upgrade.py
|
||||
0aaa80453e25aae7ea953fadb91b3ae3eaff627977c81c9ab5a2779f9a93582f scripts/web_assets.py
|
||||
12d2e324b2685f2d20d7bde4b68d2cb1565a3b5aff49e30850238e05d57bff9f scripts/web_server_contracts.py
|
||||
c245f8f832bd2c36a3fbd5e66fe98e7367c70a8db840c79e6b640f1d9d74f2ac scripts/web_service_ui_test.mjs
|
||||
2145ad17ae7a823756371ceab6bdbeb332211df93193ffb4e486f7a80c4f29ae scripts/README.md
|
||||
783afca6730accdeb74c29dd0a05123562e0fafc5f8479be6e7ba0feaf80a923 scripts/build/swagger_assets.py
|
||||
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy/deploy.py
|
||||
475aecff3e44a12199a29099a8c5069c37d880ff0307cdfe7af8ff66e532bb16 scripts/deploy/deploy_core.py
|
||||
148b5270c8f9bb78c9dfa783c6ca6ddcd572b5ab0389568f780058662f03032e scripts/deploy/deploy_runtime.py
|
||||
83c730c499b5099f11d967a0c8cb12aaa595bda60f13e22d7f61bf63a756d8a5 scripts/deploy/upgrade.py
|
||||
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
|
||||
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
|
||||
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
|
||||
20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py
|
||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
|
||||
ad62f73bb42906d617cc1b479f02d4648c7324cb89fc11d2c6aa6bf13774eec9 src/acl.rs
|
||||
ea272433e7f92e983509a8ab1aa54242440abef21df526c2f99944ef279e09b9 src/acl/tests.rs
|
||||
6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs
|
||||
@@ -178,18 +162,19 @@ ea272433e7f92e983509a8ab1aa54242440abef21df526c2f99944ef279e09b9 src/acl/tests.
|
||||
3f158f46e80b403b658b4e9b1e3710e38c70e47b2d476b8223d9c394ffeeba95 src/acme/model.rs
|
||||
c5334de6c732d76b2bea7bfc17b3ac305a250bbaad31a5420d6a7d7abb200449 src/acme/providers.rs
|
||||
b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/providers/tests.rs
|
||||
f8c4bb96814f669be8be40d013a6217c32f66943de0961ad827a184be6879645 src/acme/runtime_env.rs
|
||||
29ff0647417b0f1d50a4cc075ab51afecff9ddf47db3eb7948d51db5a38f33f3 src/acme/runtime_bootstrap.rs
|
||||
cbf221cd6695d8cf1f03da3aa2b42ed4b1afbfbc2e496329b2d29f7cc59ce0c8 src/acme/runtime_env.rs
|
||||
7acbaf34e7b953b753ab074893e47b5024f0cd756dc4c7a1074067dd936c2e8e src/acme/tests.rs
|
||||
55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs
|
||||
d18efa3f797f62b3916265e30ba546725a41e7ccfd9b123965ff9f522e3baa88 src/admin/certificates.rs
|
||||
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
|
||||
e8892cca11b3d686d1e5f6ef091a01d7b28cf4289993876a0a4d91bedb751ec4 src/admin/documentation/documents.rs
|
||||
09c05dc6ca92b2e6b32d1bd8e92739aa31b850abe04d7aef711d80828750cfa5 src/admin/documentation/documents.rs
|
||||
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
|
||||
002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs
|
||||
d3210e037481e1f95e840550428e088ffd83ec9a42f1810e4c976298fb555a9a src/admin/file_logs.rs
|
||||
beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs
|
||||
58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs
|
||||
01bde7e69062ad85449a03c05188c8af4299952921993e9828f9c03439cff104 src/admin/openapi.json
|
||||
28611fe61da40ab45769055707235dca3ec5e0b1eaad81f130206ff7b9a7819f src/admin/openapi.json
|
||||
a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs
|
||||
5287fff6b3ad436ac695e9b66feb1b0b5705b744a9f00cec7ea6a3e951e5eebf src/admin/preferences.rs
|
||||
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
|
||||
@@ -227,7 +212,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
|
||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||
af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js
|
||||
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
|
||||
9e236b9a2e9505a0eda738e4cec92efb57b1db60288499e0bf797cb67df7eecf src/admin/web/index.html
|
||||
be1d4004d7188e92931dec34f49c1b8491f1d137573621176aae66217fdffb0b src/admin/web/index.html
|
||||
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
|
||||
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
||||
af0fbd94d9b88ce1cce28e39dfd2c35de031adcba412dd4a9a17d9b416c256dd src/admin/web/login.html
|
||||
@@ -249,9 +234,9 @@ b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files
|
||||
c96a87e82ff0c1c9ce3d0cd5135903bb53af78d1c4fb6ebfe5fea503bf5644ea src/auth/store.rs
|
||||
745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs
|
||||
b0efb68dec4b0e8611684457b21044d136882ddb5c529f934061a55d377d9b28 src/backup.rs
|
||||
96fea1da6ba7803d6d532ae8abe4157bc0329a4236e40c0c21a77ddc520b9f11 src/backup/archive.rs
|
||||
ebce56a154c57bf20d6618de161359c7c49392d7d24e8d23428b7d78a7a060c1 src/backup/archive.rs
|
||||
4d7beea1c3d7c65ee6018d22af6461369754c67475b219d3474a85476cd0b57a src/backup/snapshot.rs
|
||||
5ea9e176dbd5a212438e19fe8abbb0b35ac1d6e1920cbc9bb1b327e4d1e2dcc8 src/backup/tests.rs
|
||||
c0561c2ebcb67b20ab2aacd18f61337269b437dce32dbb11bd0bf01a4e204f78 src/backup/tests.rs
|
||||
c8ee6afde0e463828757a9c2aca25506eb0a7b3b9369f4ec955857ab04b1f9d1 src/backup/validation.rs
|
||||
426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs
|
||||
3d4b3986f54bed99212063b6d135d40a202d556a3e4d253e24c74be4cb9e8ebc src/balance/health.rs
|
||||
@@ -267,9 +252,9 @@ ccb3e1988d6ae5825450e894eea896d53ae9fac150d0175ea5e742413e55105a src/basic_auth
|
||||
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
|
||||
7def03ace07dc38a4806277d75b6ce673d33ddaaf1c4855fa0a3372be1cf9489 src/cache/cold.rs
|
||||
a9eeb93519aae2de7a1282296ced91d2eb8307cc966f8fbf6085af0fd21b0ad2 src/cache/cold/cleanup.rs
|
||||
019e7632e09c6f8202d5d919b293f6eb102e1b339e7043fcdbf6b680e8841aca src/cache/cold/demotion.rs
|
||||
5de20afb2ecb865d60ebf6593c1dc5167941e0ef812458e6da3bd48f7fd360dd src/cache/cold/startup.rs
|
||||
5c132e98bcc3fd40e71e8c9666c78e47bbdf801958737e30e6a16ef34f6e324e src/cache/cold/worker.rs
|
||||
206809de4dfdf2f100762e91e6d964a6fa03879dd225d5801f5fd91fe47b3ff1 src/cache/cold/demotion.rs
|
||||
9fc18368e2ce278ef0ad2b73ff016aa24936c2186be371ca4010a1f6f63bb0ed src/cache/cold/startup.rs
|
||||
4615ec97504fb0d41f2259595fdc96d272aeb30451c05b07d4f4fb1578472e6f src/cache/cold/worker.rs
|
||||
d51fa4f320be56c572f767617428c3f3838543cb0eeb7d1f8ae791e1a1ce9a71 src/cache/collapse.rs
|
||||
27a35689fef639f1aea00d9f5a86adc981c0c304dc3e28e6da40494dffbb6bfa src/cache/dashboard.rs
|
||||
bbd6de8568fea79cc4a11d33b905af75713e609c8f37ec943c82f0d180e66dcc src/cache/fill.rs
|
||||
@@ -278,14 +263,15 @@ c1357a13658e1ed5158084d3a41ba9ea906512774a5863ce9c56cb508d3fa6c8 src/cache/inva
|
||||
7ed64ac876e3be828401e493f08a8d54f28ea75d2c2ff156987848f9dc6097a9 src/cache/invalidation/invalidate.rs
|
||||
b74945c7460b76ed7afcb02a45ede09d76f08e0703c47920b9b9d794d4b74980 src/cache/lifecycle.rs
|
||||
bd831807ca4720b3a41a6ea1d9a6c0ed2d935225e2dacbd9313be064f2e4a20e src/cache/lookup.rs
|
||||
108c8e3adfa0e35a3b4daa2d1e94f69278d4ddadc3b6c4d4d9d751298a23b91d src/cache/lookup/hot.rs
|
||||
88f6210c03d7a8e7a35be1f123c9c15675af6e6a599c50db927d99866851b634 src/cache/lookup/hot.rs
|
||||
1779abcee77600c13050a2882f896a5b35625b1ae53737c2554179492ce0e40d src/cache/lookup/insert.rs
|
||||
f3e026a3c0e953dca3e5c9def43d864a7706afcd7cbeeaf0f21c059a335b30cc src/cache/lookup/tiered.rs
|
||||
58c03fde09e5eab40968679793b364f4f1cf5fd832bd79ebcdc92bb23871ad82 src/cache/mod.rs
|
||||
f2ce12806e9572f0ac03277d38d9feafa7ed6a045ef010434e33069271ddd6a1 src/cache/model.rs
|
||||
1fafec16201ff94ed52b00697a5ec53fb87a838f62e82fed157fdbc74cb9156e src/cache/model/cache.rs
|
||||
58ede9780b70ac46fe8b35b6a85f02f56f9a2c75b045e3435062cceb3921f3aa src/cache/model.rs
|
||||
ba94226addfd6cdab67d666dc15b8412c4d117111e44639e503e89a95175d0a2 src/cache/model/cache.rs
|
||||
0ec4f6d0adea1ed80a3ca17c68c9c85502491da2904d9d690b4169ab9b025b21 src/cache/model/cold_metadata.rs
|
||||
dac97eb28052795b7acbbbab1eaf94e8dc1186b07a7e0c4b7dc522af6de02574 src/cache/model/entry.rs
|
||||
3dd0a5f2bafab4a67a15e76c544bd123eb564aa33e20defbc9e338ef4507da81 src/cache/model/maintenance.rs
|
||||
09a4417f82984eca8c040ea28eb82b3f129c347d951d4b4fab704bb194d9b985 src/cache/model/maintenance.rs
|
||||
36d471e4b417f1eaffe1fd39bdaa64b6a6aa68faf58b26fe42d6b977f08769d9 src/cache/model/settings.rs
|
||||
55dcc0ad84d6b73689ba43e759d5685c406419c7733e23e78d5b072891ab17d7 src/cache/model/views.rs
|
||||
e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs
|
||||
@@ -295,7 +281,7 @@ c46f335cf75b91b728125596242a9682a7ef3dbe86242dde7fc038790984a6e4 src/cache/poli
|
||||
efde2bd046ed073900cd3e67051f0542c802a06e8f5e38c2c20ee92604080c36 src/cache/tests.rs
|
||||
13dab7b7a587cfc146826a92ab94ec9e04f7e500b7a84e678d77226829844923 src/cache/tests/cold_tier_tests.rs
|
||||
b4f609ee900c556b613136bbd3fce11c242a415d277ac3d25bdcb72275b66832 src/cache/tests/cold_tier_tests/part_01.rs
|
||||
fb3074fa87b19a66db66842f2ab3a0728646293ca2154ab26f7865a58cda56ac src/cache/tests/cold_tier_tests/part_02.rs
|
||||
f43aa6c231f2c85ee3390dddab74aab2eea2a3f874acb35f145c23866266cb52 src/cache/tests/cold_tier_tests/part_02.rs
|
||||
8bcbd6e8158620c4e0c056b2872e0b5f786d5f2bc2e2203351b8416a94641f09 src/cache/tests/dashboard_tests.rs
|
||||
6c69ef40567d34671d26231358e2db74cc26ec0113843303caa98d4dc0fbf1f9 src/cache/tests/scope_tests.rs
|
||||
578b18cac918ba51fad775eca188f645b228500e59eb1790f783a0b8d0245b7b src/cache/tests/tests.rs
|
||||
@@ -306,7 +292,7 @@ cdc41a7241739b89297494323f29e0230fa06abbd74e219fedfda11df1db4334 src/cache/test
|
||||
541a62b749440aa1099cf0fc8a2bd31eac0c082ccef35df22c1e049e7928e216 src/cache/tests/v022_cache_tests.rs
|
||||
15e85357c3ddf4fc2283d621f5f4cd8ca309275e06677020299d4105f0475949 src/cache/tests/v022_cache_tests/part_01.rs
|
||||
7ee652a781081c0b53d39dff213c2bda38e405a18a3d0b908f97cd3ac51d556f src/cache/tests/v022_cache_tests/part_02.rs
|
||||
f9a3c4f0c7363819a0904860531078df89e2ee1ed5bcff7047986e40c9f38a0c src/cache/tickets.rs
|
||||
a8a0ce446eb53ffdb8f8d97efa3e1344410dc3798b25f62b8817e8e160d28940 src/cache/tickets.rs
|
||||
3fc79b09474fe23b5b6fc98e4a376469afb02a41d6cc2dbbba59dc6091c790c8 src/compression.rs
|
||||
e2f58279499b7c3b47c65d3a7814bb5c7cd6506544d939875fbb0f91fa9aa59d src/compression/tests.rs
|
||||
7921ae451ccd65d647165b02919bd97399399da471454a6a1a1f63e682b76fc8 src/config.rs
|
||||
@@ -346,11 +332,11 @@ a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/fo
|
||||
17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs
|
||||
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
||||
c66a9803e43bdd87101825e8c71ab33f890ac4c3428467ba5d43c17fdfa1439e src/main/admin_runtime.rs
|
||||
04084e87167a8c47a24d5a4e89e0bde0f30398ea95c38fe34c057a1149ac33e8 src/main/bootstrap.rs
|
||||
e1ca82cf6f0360176323b32907a5c8dd0646d661ed75a935af003c0b1ed691cd src/main/bootstrap.rs
|
||||
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
|
||||
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
|
||||
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
|
||||
7e97938fd9209f2335af5d6304abcb4193e80bb33e4ccf480938e8c3ab4644d6 src/main/tasks.rs
|
||||
b26945f6405493df85dffddab3ca8804137e6cb99c6d165eaaf0ad024cb570ff src/main/tasks.rs
|
||||
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
|
||||
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
|
||||
49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs
|
||||
@@ -371,7 +357,7 @@ b5856fd38a957b2ca9c7e3606d84d13d5cfcf3a45b487b15ae1d2dc248cbda57 src/proxy/body
|
||||
f0cb26ed34c35bacc74b914457acac8a611390de14cf46e2e6d3bcc848e914e8 src/proxy/cache_helpers.rs
|
||||
cfb0910af8cd868bde3f6c9992319e46d5328d6d8f69c50701df0ea12bef2108 src/proxy/forward/cache_lookup.rs
|
||||
212de22f407dd96800afcf65fd1fc7c40fede1b88f80d04850eb8d1227b08a5b src/proxy/forward/response.rs
|
||||
4992d8c2fabc468337999b3d4b58560a9e10632893cebe1669ecb3267ad1bdc9 src/proxy/forward/route_cache.rs
|
||||
0a7841a355f4740c5d9182cfa3ede1b0e6195b66f0518f976810513612ca47dd src/proxy/forward/route_cache.rs
|
||||
51c620b77004cca26edf387f9650f07d27801e093d32cc0162eaee1d0702c126 src/proxy/forward/success_finish.rs
|
||||
e5b1812c75b4cca91c32e99d28622cac1dea8e347bdf1c46e43546cb341f95a4 src/proxy/forward/success_prepare.rs
|
||||
23b6bd26f6175b52c13d2571318fddd12550ba34ee77b87b5ac0e4811f8e2bee src/proxy/forward/upstream.rs
|
||||
@@ -387,13 +373,13 @@ d05e655ba0b3b322c657aaa9c2cbe7a0181064a896026b5eeaca347c4a0b7831 src/proxy/webs
|
||||
b21dc9cf3817dd4e3273ce25887d63d7a772c477b86ca57f8d5b72b8154456e7 src/proxy/websocket/tests.rs
|
||||
a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 src/proxy/websocket/tunnel.rs
|
||||
15fabd38576b61f650c039acdbf7ac5a5bd24330062475b94315285b2dc258b6 src/proxy/websocket/validation.rs
|
||||
26f57e7e21be5697b57dbeceae8f148c50ac461973adcd8c07a0fd8a8c0d8c33 src/rate_limit.rs
|
||||
3ac0b5c6407b2cf13e28b570ab94aab3ade62faf93e40a06269a058019fde165 src/rate_limit/tests.rs
|
||||
463a871aef70a5531c33a9ea43b9dc4e628b272d654bb225e6161aa1497d96f3 src/rate_limit.rs
|
||||
a767c23830f188f5cd6bb747ad6950732a987df3b36adec0d7a871cbab711bbf src/rate_limit/tests.rs
|
||||
d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.rs
|
||||
811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs
|
||||
d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
|
||||
4e0b25c2b86eefddd76c64b39dc1c1cd6d243addc32cdea99e6c3ac84ed879a6 src/runtime/certificates.rs
|
||||
de52ef274756a1543d8913d8ff36640a2091ea675882b0216d0ab99df0a2d0dc src/runtime/constructors.rs
|
||||
2ef0bf9a1a80a4e310b274789e026590f219c6579f4f259af3d83e66ae76697c src/runtime/constructors.rs
|
||||
31dc5cdea00efd63594dc6becd5a9103594696145cbc8e7b7594fb407214ad9d src/runtime/docker.rs
|
||||
c052b35673d905219a71130b44a5dc0027bf9b853bff0b6bdbed4be4064550ea src/runtime/tests.rs
|
||||
03144b1b00fb45c5616ca9c9b0e57013e86c1f07f129830aaec98a73b274199f src/runtime/types.rs
|
||||
@@ -457,7 +443,7 @@ ff4980f0a091da9ff1722d6b9f6662529cbd7c5e2ac68dcd22a507ce6aaf3011 src/vhost/vali
|
||||
c1fef6dfb79275479c15097b211cd5f063ad7e0852a0767b0145cf4f32f751da src/web_server/config/model.rs
|
||||
f1d3e427c53f8b5be5768d85e69ead746d83647fcd3b355acc98e3f9d5e217e0 src/web_server/config/validation.rs
|
||||
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
|
||||
be75d0bf057a9b17d8c4a1cab67a1cbf349f542c8e57e4a822ebb9192b1c9582 src/web_server/fastcgi/protocol.rs
|
||||
640d629c6fc2228d864cbe28b9982cf5b5e38feb941a5dbafaa3fd7fe1a924a5 src/web_server/fastcgi/protocol.rs
|
||||
134c4998f6a0aa27e121af1ae3355b7e335488b2ae867ac0a934c58b4eb77191 src/web_server/fastcgi/response.rs
|
||||
8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs
|
||||
0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs
|
||||
@@ -467,19 +453,39 @@ be75d0bf057a9b17d8c4a1cab67a1cbf349f542c8e57e4a822ebb9192b1c9582 src/web_server
|
||||
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
|
||||
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
|
||||
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
|
||||
51b0878fcf6e8ef2458f1b8ded65f843951c122f0ff0afe2854f467c5e192273 src/web_server/response/file.rs
|
||||
9f49fe34602f7c81543eef12a337c79d20b4dac904a9dfe5e709c123b2c9078e src/web_server/response/file.rs
|
||||
9aa68251a5f549f4ce3b1bda88d4abb9efc629caf444989381b4117c9488e37b src/web_server/response/headers.rs
|
||||
2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs
|
||||
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
|
||||
3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs
|
||||
cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs
|
||||
c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs
|
||||
ef49a4230a1cb46c51866c4c0a49eae8c711377245df50c4f36a86693c040c1e src/web_server/tests/part_02.rs
|
||||
648d7d9cfc8cd5191fa7dab30c1829fc245f0d4f3e238243d7e32644125a135e src/web_server/tests/part_02.rs
|
||||
e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs
|
||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
|
||||
347a4ff9e028a1e999ad129590c355b5734cf928dd81e5099aea0c8aca8d5908 tests/http3.rs
|
||||
eecf680f001f2110dde4eceaccf2f922859a16c946a644f8854e8de4d3771f7c tests/policies.rs
|
||||
728999110a6d8cacb62bc0399bc333fa23a66526ee23f563a3c93bf00e5fad19 tests/transport_config.rs
|
||||
77ee069a5c0f69c49d2688eff3c9aa0f3e39fa02042cfe348e410a23b8f3e116 update.sh
|
||||
af688fd4e67e54a8a2ff243fdfe916a20ceae6251baef207a4e974de2149f308 upgrade.sh
|
||||
b04362ad082a32c9ccc4cf38c46f44d71347adecf7aba3224b3af3ea4872e3d0 tools/README.md
|
||||
358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py
|
||||
ba6ebb2e393a89b78ce138f2572156e51a6ae557c3aaefff3d8e52daaeb5dc61 tools/checks/check_source.py
|
||||
873f21239d31659d1d8d66c5645032583ff4b7032f4712af34137afe510269a6 tools/checks/docs_check.py
|
||||
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
|
||||
cf762780c4085791c72c3c5f93054d7f2a1107403bee0616a1f4113bbcbb99f9 tools/checks/memory_contracts.py
|
||||
4256001fe5ac0677b87b1384e7e1225e8a19686a5daedf06699cb8663e77d679 tools/checks/preflight.py
|
||||
39422107ae42162a45a9dae3521125f648942c67a336f15d444f98e3c5b80007 tools/checks/release_smoke.py
|
||||
f69845bc26ec33160fc756953991cf32aa8ad6a7fb4a131caa2093db91e783ef tools/checks/rust_module_contracts.py
|
||||
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py
|
||||
adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py
|
||||
23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py
|
||||
2309c41e769e797c838d4f40d50bb3c6b9ad9ad0f9ed07fb93e07f95d66563f9 tools/checks/source_module_contracts.py
|
||||
0aaa80453e25aae7ea953fadb91b3ae3eaff627977c81c9ab5a2779f9a93582f tools/checks/web_assets.py
|
||||
54cb50332c5805c0e11857161284bb001eac12363d555714c2ef3ab00b1a856e tools/checks/web_server_contracts.py
|
||||
c245f8f832bd2c36a3fbd5e66fe98e7367c70a8db840c79e6b640f1d9d74f2ac tools/checks/web_service_ui_test.mjs
|
||||
d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py
|
||||
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py
|
||||
538b5f6cfa738355a729940ee3d92e7ab3dc25ca69ae330ddaa08f831775b8ba tools/dev/test_compression.py
|
||||
c711154a9df425a56607bd88f03815ffb408213a31a2b66ed1c1c9a6301a2fc6 tools/legacy/backup.py
|
||||
fec1b9f7bf8d4b43c2b45e6a1f762c9412c51f3e4e3520aff185b0590b5d47be update.sh
|
||||
56c702111022e1ca2c8f6b72c635c749b05936ffe6387bdb0a202691614cbc96 upgrade.sh
|
||||
File diff suppressed because it is too large.
Load diff
@@ -16,7 +16,7 @@ fn swagger_assets(
|
||||
] {
|
||||
println!("cargo:rerun-if-env-changed={name}");
|
||||
}
|
||||
let helper = root.join("scripts/swagger_assets.py");
|
||||
let helper = root.join("scripts/build/swagger_assets.py");
|
||||
println!("cargo:rerun-if-changed={}", helper.display());
|
||||
let interpreters = match env::var_os("RUST_PROXY_PYTHON") {
|
||||
Some(value) if !value.is_empty() => vec![value],
|
||||
|
||||
@@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi
|
||||
case "$mode" in
|
||||
help|-h|--help)
|
||||
cat <<'HELP'
|
||||
RustProxy 0.27.2
|
||||
RustProxy 0.27.3
|
||||
./dev.sh Build and start proxy + local panel
|
||||
./dev.sh run [serve flags] Same, with extra server options
|
||||
./dev.sh admin setup-token Generate/print a one-time setup capability before first login
|
||||
@@ -66,7 +66,7 @@ HELP
|
||||
*) printf 'Unknown command: %s. Use ./dev.sh help\n' "$mode" >&2; exit 2 ;;
|
||||
esac
|
||||
if [[ "$mode" == "backup" ]]; then
|
||||
exec python3 scripts/backup.py --data-dir "${RUST_PROXY_DATA_DIR:-$ROOT/data}" "$@"
|
||||
exec python3 tools/legacy/backup.py --data-dir "${RUST_PROXY_DATA_DIR:-$ROOT/data}" "$@"
|
||||
fi
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
printf 'Python 3.10+ is required for source preflight and upgrade safety checks.\n' >&2
|
||||
@@ -74,7 +74,7 @@ if ! command -v python3 >/dev/null 2>&1; then
|
||||
fi
|
||||
python3 -c 'import sys; sys.exit(0 if sys.version_info >= (3, 10) else "Python 3.10+ is required.")'
|
||||
if [[ "$mode" == "doctor" ]]; then
|
||||
python3 scripts/preflight.py
|
||||
python3 tools/checks/preflight.py
|
||||
printf 'Source layout: OK\n'
|
||||
for tool in cargo rustc cc python3; do
|
||||
printf '%s: ' "$tool"
|
||||
@@ -82,27 +82,27 @@ if [[ "$mode" == "doctor" ]]; then
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
if [[ "$mode" == "acme" ]]; then exec python3 scripts/acme_provider.py "$@"; fi
|
||||
if [[ "$mode" == "mock-auth" ]]; then exec python3 scripts/mock_auth.py "$@"; fi
|
||||
if [[ "$mode" == "mock" ]]; then exec python3 scripts/mock_server.py "$@"; fi
|
||||
python3 scripts/preflight.py --repair-known
|
||||
if [[ "$mode" == "acme" ]]; then exec python3 scripts/runtime/acme_provider.py "$@"; fi
|
||||
if [[ "$mode" == "mock-auth" ]]; then exec python3 tools/dev/mock_auth.py "$@"; fi
|
||||
if [[ "$mode" == "mock" ]]; then exec python3 tools/dev/mock_server.py "$@"; fi
|
||||
python3 tools/checks/preflight.py --repair-known
|
||||
if [[ "$mode" == "repair" ]]; then printf 'Source preflight: OK\n'; exit 0; fi
|
||||
if [[ "$mode" == "test" ]]; then
|
||||
python3 scripts/rust_module_contracts.py
|
||||
python3 scripts/source_module_contracts.py
|
||||
python3 scripts/check_source.py
|
||||
python3 scripts/release_smoke.py
|
||||
python3 scripts/memory_contracts.py
|
||||
python3 scripts/web_server_contracts.py
|
||||
python3 scripts/security_contracts.py
|
||||
python3 scripts/security_dependency_contracts.py
|
||||
python3 tools/checks/rust_module_contracts.py
|
||||
python3 tools/checks/source_module_contracts.py
|
||||
python3 tools/checks/check_source.py
|
||||
python3 tools/checks/release_smoke.py
|
||||
python3 tools/checks/memory_contracts.py
|
||||
python3 tools/checks/web_server_contracts.py
|
||||
python3 tools/checks/security_contracts.py
|
||||
python3 tools/checks/security_dependency_contracts.py
|
||||
if command -v node >/dev/null 2>&1; then
|
||||
node scripts/web_service_ui_test.mjs
|
||||
node tools/checks/web_service_ui_test.mjs
|
||||
else
|
||||
printf 'Node.js not found; skipping Web Service UI behavior test.\n' >&2
|
||||
fi
|
||||
python3 scripts/docs_check.py
|
||||
python3 scripts/langs_check.py
|
||||
python3 tools/checks/docs_check.py
|
||||
python3 tools/checks/langs_check.py
|
||||
fi
|
||||
if ! command -v cargo >/dev/null 2>&1; then
|
||||
cat >&2 <<'MISSING'
|
||||
@@ -139,7 +139,7 @@ if [[ "$mode" == "test" ]]; then
|
||||
printf 'Checking all Rust targets before tests...\n' >&2
|
||||
cargo check --locked --all-targets
|
||||
fi
|
||||
printf 'Building RustProxy 0.27.2...\n' >&2
|
||||
printf 'Building RustProxy 0.27.3...\n' >&2
|
||||
cargo build --locked
|
||||
BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
|
||||
case "$mode" in
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
const version = "0.27.2"
|
||||
const version = "0.27.3"
|
||||
|
||||
type config struct {
|
||||
Listen string
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
# Administration API access - 0.27.2
|
||||
# Administration API access - 0.27.3
|
||||
|
||||
## Sign-in boundary
|
||||
|
||||
@@ -132,7 +132,7 @@ promise to erase browser/OS screenshots or data intentionally downloaded by an a
|
||||
## First build / offline assets (fixed in 0.7.2)
|
||||
|
||||
No `SWAGGER_UI_DOWNLOAD_URL` environment variable or `.cargo/config.toml` is required.
|
||||
The default is Swagger UI **5.32.11**, selected by `scripts/swagger_assets.py` and
|
||||
The default is Swagger UI **5.32.11**, selected by `scripts/build/swagger_assets.py` and
|
||||
called from this application's `build.rs`. The former `utoipa-swagger-ui` adapter
|
||||
is removed: there is no separate dependency build silently choosing its older
|
||||
historical browser default. Direct Cargo builds using `--manifest-path` use the
|
||||
@@ -204,7 +204,7 @@ build server. Browser tests still label their simulated API/Swagger renderer.
|
||||
After running the built server:
|
||||
|
||||
```bash
|
||||
python3 scripts/admin_audit.py --url http://127.0.0.1:9090
|
||||
python3 tools/audit/admin_audit.py --url http://127.0.0.1:9090
|
||||
```
|
||||
|
||||
The script sends anonymous read-only loopback probes, follows no redirects, ignores
|
||||
|
||||
@@ -42,7 +42,7 @@ committed document at startup; this is not one cross-memory/SQLite transaction.
|
||||
### Rust source organization
|
||||
|
||||
Rust modules are kept as small responsibility-oriented files. Every checked-in `.rs` file
|
||||
is limited to **500 lines**; `scripts/rust_module_contracts.py` enforces the limit across
|
||||
is limited to **500 lines**; `tools/checks/rust_module_contracts.py` enforces the limit across
|
||||
the repository. Large logical modules use a thin facade (`config.rs`, `docker.rs`,
|
||||
`server.rs`, `runtime.rs`, `balance.rs`, `prometheus.rs`, and similar) plus files in a
|
||||
matching directory. `web_server` uses the standard `web_server/mod.rs` facade with
|
||||
@@ -190,7 +190,7 @@ the 0.7 scheduler and automated publication.
|
||||
same canonical vhost model used by the UI. Validated startup migration uses one
|
||||
CAS write; failure before validation completion leaves the stored JSON untouched.
|
||||
|
||||
`acme` manages one private worker job; `scripts/acme_provider.py` adapts Certbot
|
||||
`acme` manages one private worker job; `scripts/runtime/acme_provider.py` adapts Certbot
|
||||
webroot/Cloudflare/OVH/RFC2136. Only the running HTTP-01 job's names and token files
|
||||
are exposed by the reserved challenge middleware. Provider files remain private,
|
||||
not part of SQLite or generic configuration exports. Issuance/import are separate.
|
||||
|
||||
+2
-2
@@ -77,10 +77,10 @@ When cold storage is enabled,
|
||||
`cold_after_seconds` measures time since the last access. `cold_max_hot_seconds` measures total
|
||||
hot-tier residency, so it can move a frequently accessed object to disk after a configured age.
|
||||
The administration UI exposes these as separate policies to avoid conflating "old" with "unused".
|
||||
Demotions use an asynchronous queue bounded by both object count and `cold_queue_max_bytes`. A demoted `Entry` is removed from HOT accounting and moved, not copied, into a `ColdDraft`; its `Bytes` payload remains charged to `transient_max_bytes` until persistence completes and the draft is dropped. The persisted `ColdEntry` contains paths/metadata rather than the body allocation. This prevents the COLD writer queue from becoming a second unbounded RAM cache. If an HTTP response is already using a clone of the same `Bytes`, that last transport-owned reference can keep the backing allocation alive until the response finishes; the cache-owned HOT/COLD reference itself is released after persistence. Process RSS is therefore monitored separately from cache accounting. Supported
|
||||
Demotions use an asynchronous queue bounded by both object count and `cold_queue_max_bytes`. A demoted `Entry` is removed from HOT accounting and moved, not copied, into a `ColdDraft`; its `Bytes` payload remains charged to `transient_max_bytes` until persistence completes and the draft is dropped. The persisted `ColdEntry` contains compact lookup/invalidation/LRU metadata rather than the body allocation. Since 0.27.3 it also does not retain the full response `HeaderMap` or origin validator values per COLD object; those are validated and loaded from the existing metadata JSON only for an actual COLD read. The in-memory body digest is 32 raw bytes and the map-owned cache key is not duplicated inside the entry. This reduces long-lived COLD index RAM without changing Cold format 3 on disk. This prevents the COLD writer queue from becoming a second unbounded RAM cache. If an HTTP response is already using a clone of the same `Bytes`, that last transport-owned reference can keep the backing allocation alive until the response finishes; the cache-owned HOT/COLD reference itself is released after persistence. Process RSS is therefore monitored separately from cache accounting. Supported
|
||||
glibc/Linux builds ask the allocator to release unused pages after bulk demotion. During
|
||||
ordinary graceful shutdown, `cold_on_shutdown` performs a best-effort direct flush of remaining hot
|
||||
objects before the store is closed. A **managed software update** (`update.sh` / `scripts/deploy.py`)
|
||||
objects before the store is closed. A **managed software update** (`update.sh` / `scripts/deploy/deploy.py`)
|
||||
uses a private one-shot marker and, on RustProxy 0.13.2+, forces this hot-to-cold handoff whenever
|
||||
the cold tier is enabled even if `cold_on_shutdown=false`. The updater gives the process a larger
|
||||
stop budget and removes the marker before backing up state. If the cold tier is disabled there is
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ each Docker host, so the proxy container never needs Docker-socket access. See
|
||||
Build locally with a normal OCI builder:
|
||||
|
||||
```bash
|
||||
docker build -t rustproxy:0.27.2 .
|
||||
docker build -t rustproxy:0.27.3 .
|
||||
```
|
||||
|
||||
Persist `/var/lib/rustproxy`; it contains the SQLite database, certificates and the
|
||||
@@ -22,7 +22,7 @@ docker volume create rustproxy-data
|
||||
docker run --rm -d --name rustproxy \
|
||||
-p 8080:8080 \
|
||||
-v rustproxy-data:/var/lib/rustproxy \
|
||||
rustproxy:0.27.2
|
||||
rustproxy:0.27.3
|
||||
|
||||
docker exec rustproxy rust-proxy admin setup-token
|
||||
```
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Stored log explorer
|
||||
|
||||
RustProxy 0.27.2 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent.
|
||||
RustProxy 0.27.3 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent.
|
||||
|
||||
## Separation from Live Logs
|
||||
|
||||
|
||||
@@ -74,6 +74,9 @@ All changes to socket/ALPN/QUIC transport parameters require restart.
|
||||
of forbidden connection/framing headers; streamed bodies and trailers.
|
||||
- Shared routing pipeline: real local socket -> validated authority -> trusted client
|
||||
address -> ACL -> Tinyauth -> cache -> origin -> response/security/Server policies.
|
||||
- HTTP cache semantics are shared with HTTP/1.1 and HTTP/2. In 0.27.3, body-less
|
||||
`HEAD` requests use the same bounded empty-body probe as `GET`, so an initially
|
||||
unpolled HTTP/3 receive stream does not cause a false `request_body` cache bypass.
|
||||
- 0-RTT is disabled to avoid replaying writes; no retry to another origin or transport.
|
||||
- Endpoint active migration is disabled. Missing real local destination data on a
|
||||
wildcard QUIC connection fails closed rather than substituting a forwarded IP.
|
||||
|
||||
+13
-4
@@ -1,13 +1,22 @@
|
||||
# Installing and updating RustProxy 0.27.2
|
||||
# Installing and updating RustProxy 0.27.3
|
||||
|
||||
The scripts prepare a Linux/systemd installation. This source archive is **not a
|
||||
verified production build**: Rust/Cargo, real CA operations, live systemd startup
|
||||
and rollback against a real Rust database have not been exercised in the packaging
|
||||
environment. Perform the acceptance steps below on an isolated server first.
|
||||
|
||||
Ready-to-use Debian packages are published separately at `https://rustproxy.linuxiarz.pl`. The Debian package-build repository and metadata are intentionally maintained outside this source tree.
|
||||
## Official installation repositories
|
||||
|
||||
## Debian 13+ one-line source install/update
|
||||
Use the project-maintained package repositories as the normal installation/update path:
|
||||
|
||||
- **DEB/RPM:** `https://rustproxy.linuxiarz.pl/`
|
||||
- **OPNsense plugin:** `https://rustproxy-opn.linuxiarz.pl/`
|
||||
|
||||
Package-build metadata is intentionally maintained outside this source tree. The Debian source
|
||||
bootstrap below remains supported for development, custom builds and environments where the
|
||||
packaged path is not suitable.
|
||||
|
||||
## Optional: Debian 13+ one-line source install/update
|
||||
|
||||
```bash
|
||||
# auto: first install or managed update
|
||||
@@ -179,7 +188,7 @@ A source-tree preflight can still inspect a distribution-provided fallback envir
|
||||
|
||||
```bash
|
||||
sudo -u rustproxy env PATH=/usr/local/bin:/usr/bin:/bin \
|
||||
python3 scripts/acme_provider.py --doctor
|
||||
python3 scripts/runtime/acme_provider.py --doctor
|
||||
```
|
||||
|
||||
The provider script is embedded in the RustProxy binary; this command is diagnostic only.
|
||||
|
||||
+6
-6
@@ -46,7 +46,7 @@ The administration server keeps the same strict Host/Origin/request-marker/CSRF
|
||||
|
||||
There is no SQLite schema or saved-configuration migration. 0.25.1 reorganizes the Traffic dashboard and source tree only; existing Web Services, vhosts, traffic settings, cache state and administrator data remain compatible. Browser asset URLs remain `/assets/app.js` and `/assets/vhosts.js` even though their source is now split into bounded modules.
|
||||
|
||||
When updating a source checkout by extracting the archive over an older tree, remove the obsolete source monoliths `src/admin/web/app.js` and `src/admin/web/vhosts.js` if they remain. Checkouts older than the Web Server module split may also retain obsolete `src/web_server.rs`; remove it as well. Do **not** remove `src/config.rs`, `src/docker.rs`, `src/runtime.rs`, `src/balance.rs`, `scripts/deploy.py`, `scripts/migrate_npm.py` or `docker-agent/main.go`: these are intentional module/CLI entry points in 0.25.1. Build/cache artifacts such as `target/` and `__pycache__/` may be deleted safely, but `.env`, `data/`, databases and local certificates are runtime/operator state and must not be deleted as part of a source refresh.
|
||||
When updating a source checkout by extracting the archive over an older tree, remove the obsolete source monoliths `src/admin/web/app.js` and `src/admin/web/vhosts.js` if they remain. Checkouts older than the Web Server module split may also retain obsolete `src/web_server.rs`; remove it as well. Do **not** remove `src/config.rs`, `src/docker.rs`, `src/runtime.rs`, `src/balance.rs`, `scripts/deploy/deploy.py`, `scripts/migration/migrate_npm.py` or `docker-agent/main.go`: these are intentional module/CLI entry points in 0.25.1. Build/cache artifacts such as `target/` and `__pycache__/` may be deleted safely, but `.env`, `data/`, databases and local certificates are runtime/operator state and must not be deleted as part of a source refresh.
|
||||
|
||||
## 0.24.10 -> 0.25.0
|
||||
|
||||
@@ -80,7 +80,7 @@ There is no schema or configuration-format migration. This patch changes only ad
|
||||
There is no new SQLite schema version in 0.15.0. Renewal observations remain in the existing
|
||||
schema-v3 `certificate_renewals` table. The application no longer contains a runtime JSON importer.
|
||||
|
||||
When `upgrade.sh` / `scripts/upgrade.py` is used while RustProxy is stopped, an obsolete
|
||||
When `upgrade.sh` / `scripts/deploy/upgrade.py` is used while RustProxy is stopped, an obsolete
|
||||
`<data-dir>/certs/renewals.json` observation sidecar is validated as a regular file and removed as
|
||||
the final source-upgrade step. It is not imported and no second legacy copy is retained. If the
|
||||
source-upgrade transaction fails before that final removal, the old sidecar remains untouched.
|
||||
@@ -189,7 +189,7 @@ removed in 0.7.2 (see the build notes above). Source-tree updates still back up
|
||||
replace that managed directory; retain any unrelated local Cargo settings from
|
||||
your private backup. Read [API_ACCESS.md](API_ACCESS.md) for current build inputs.
|
||||
|
||||
Run `python3 scripts/admin_audit.py --url http://127.0.0.1:9090` after deployment.
|
||||
Run `python3 tools/audit/admin_audit.py --url http://127.0.0.1:9090` after deployment.
|
||||
It prints only status checks, no response bodies or credentials.
|
||||
|
||||
---
|
||||
@@ -302,7 +302,7 @@ unzip RustProxy-0.4.0.zip -d /home/mg/rustproxy-040-staging
|
||||
cd /home/mg/rustproxy-040-staging/RustProxy
|
||||
```
|
||||
|
||||
`scripts/backup.py` is a legacy release-test helper and is not a complete Secret
|
||||
`tools/legacy/backup.py` is a legacy release-test helper and is not a complete Secret
|
||||
Store backup. For current backup/restore behavior see `SECRET_STORE_BACKUP.md`.
|
||||
|
||||
A missing database means there is no database to back up. Do not initialize an empty
|
||||
@@ -354,7 +354,7 @@ and CLI-only `rust-proxy backup restore`; it includes the encrypted Secret Store
|
||||
key material required to recover it, certificates and other durable application
|
||||
state while excluding cache, logs and runtime files.
|
||||
|
||||
The historical `./dev.sh backup` / `scripts/backup.py` snapshot helper described by
|
||||
The historical `./dev.sh backup` / `tools/legacy/backup.py` snapshot helper described by
|
||||
older release notes is not sufficient after Secret Store migration and is retained
|
||||
only for offline compatibility/release tests.
|
||||
|
||||
@@ -378,7 +378,7 @@ Managed paths now include build.rs, langs, deploy and make_zip.py. Extra *.json
|
||||
language packs from the installed langs directory are copied into the new tree
|
||||
when the new distribution does not contain the same filename. Shipped packs are
|
||||
updated, with old bytes preserved in the source backup. Invalid custom packs can
|
||||
correctly block a build; review them with scripts/langs_check.py. No symlinked
|
||||
correctly block a build; review them with tools/checks/langs_check.py. No symlinked
|
||||
language pack is followed. The updater never copies a database from the package.
|
||||
|
||||
The old lockfile is preserved in the managed source backup and a new dependency
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Nginx Proxy Manager migration
|
||||
|
||||
RustProxy ships `scripts/migrate_npm.py` for a one-time, dry-run-first migration of
|
||||
RustProxy ships `scripts/migration/migrate_npm.py` for a one-time, dry-run-first migration of
|
||||
Nginx Proxy Manager (NPM) proxy hosts from its SQLite database.
|
||||
|
||||
The tool reads NPM only. It does not modify the NPM database and it never imports
|
||||
@@ -9,7 +9,7 @@ reads the current revision, merges generated vhosts into the existing configurat
|
||||
and writes only when `--apply` is supplied.
|
||||
|
||||
`migrate_npm.py` is the CLI entry point and requires the sibling modules
|
||||
`scripts/migrate_npm_core.py` and `scripts/migrate_npm_api.py`. They use only the
|
||||
`scripts/migration/migrate_npm_core.py` and `scripts/migration/migrate_npm_api.py`. They use only the
|
||||
Python standard library, so no `pip` packages are required. If the migrator is copied
|
||||
or mounted outside the RustProxy source tree, keep all three files in the same directory.
|
||||
|
||||
@@ -58,7 +58,7 @@ Set the password in the environment or omit it and use the interactive prompt:
|
||||
export RUSTPROXY_USERNAME=admin
|
||||
export RUSTPROXY_PASSWORD='...'
|
||||
|
||||
python3 scripts/migrate_npm.py \
|
||||
python3 scripts/migration/migrate_npm.py \
|
||||
--database /path/to/npm/data/database.sqlite \
|
||||
--rustproxy https://rustproxy.example.net:9090
|
||||
```
|
||||
@@ -89,7 +89,7 @@ same Force SSL policy to every imported host. Select explicit listener IDs when
|
||||
After reviewing `npm-migration-plan.json`:
|
||||
|
||||
```bash
|
||||
python3 scripts/migrate_npm.py \
|
||||
python3 scripts/migration/migrate_npm.py \
|
||||
--database /path/to/npm/data/database.sqlite \
|
||||
--rustproxy https://rustproxy.example.net:9090 \
|
||||
--apply
|
||||
@@ -109,9 +109,9 @@ RustProxy directory:
|
||||
docker run --rm -it \
|
||||
--network host \
|
||||
-v /path/to/npm/data:/npm:ro \
|
||||
-v "$PWD/scripts:/rustproxy-scripts:ro" \
|
||||
-v "$PWD/scripts/migration:/rustproxy-migration:ro" \
|
||||
python:3.13-alpine \
|
||||
python /rustproxy-scripts/migrate_npm.py \
|
||||
python /rustproxy-migration/migrate_npm.py \
|
||||
--database /npm/database.sqlite \
|
||||
--rustproxy https://127.0.0.1:9090 \
|
||||
--insecure-admin-tls
|
||||
|
||||
+2
-1
@@ -15,6 +15,7 @@ This is the canonical index for the current project documentation. The authentic
|
||||
- [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards.
|
||||
- [0.25.0 specification audit](V0.25.0_AUDIT.md) - Web Server foundation point-by-point implementation status.
|
||||
- [0.25.0 pre-production security audit](V0.25.0_SECURITY_PREPROD_AUDIT.md) - threat-oriented source/configuration review, fixes made in-session and mandatory runtime security gates.
|
||||
- [0.27.3 implementation note](V0.27.3_IMPLEMENTATION.md) - HTTP/3 HEAD cache parity, COLD/rate-limit memory reduction, backup scope and package-path update.
|
||||
- [0.27.2 implementation note](V0.27.2_IMPLEMENTATION.md) - managed Certbot relocation/renewal repair.
|
||||
- [0.27.1 implementation note](V0.27.1_IMPLEMENTATION.md) - first-admin command, managed/fallback certificate runtime and account-scoped route-list sorting.
|
||||
- [0.27.0 implementation note](V0.27.0_IMPLEMENTATION.md) - compression defaults/scoped policy, persistent Docker publications and visible cache occupancy meter fill.
|
||||
@@ -70,7 +71,7 @@ This is the canonical index for the current project documentation. The authentic
|
||||
|
||||
## Historical or version-labelled context
|
||||
|
||||
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md); [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) is retained as the prior managed-runtime/list-sorting milestone; [V0.27.0_IMPLEMENTATION.md](V0.27.0_IMPLEMENTATION.md) is retained as the prior compression/Docker-publication milestone; [V0.26.7_IMPLEMENTATION.md](V0.26.7_IMPLEMENTATION.md) is retained as the prior cache/backend/container milestone; [V0.26.6_IMPLEMENTATION.md](V0.26.6_IMPLEMENTATION.md) is retained as the prior access-policy/administration milestone; [V0.26.5_IMPLEMENTATION.md](V0.26.5_IMPLEMENTATION.md) is retained as the prior interface-scaling/listener-navigation milestone, [V0.26.4_IMPLEMENTATION.md](V0.26.4_IMPLEMENTATION.md) as the prior administration/backend-lifecycle milestone, [V0.26.3_IMPLEMENTATION.md](V0.26.3_IMPLEMENTATION.md) as the prior appearance/UI milestone, [V0.26.2_IMPLEMENTATION.md](V0.26.2_IMPLEMENTATION.md) as the prior Logs/UI milestone and [V0.26.1_IMPLEMENTATION.md](V0.26.1_IMPLEMENTATION.md) as the prior protocol-attribution milestone. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain historical evidence.
|
||||
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md); [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability.
|
||||
|
||||
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above.
|
||||
|
||||
|
||||
@@ -147,7 +147,6 @@ Current direction: 0.23.3 -> 0.24.0
|
||||
- [x] Add FreeBSD/OpenBSD/NetBSD native best-effort sampling and explicit unsupported/null metrics.
|
||||
- [x] Surface platform in Proxy Process and Prometheus metadata.
|
||||
- [x] Update source/package/UI identity and source-side regression contracts to 0.24.0.
|
||||
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on the release host.
|
||||
|
||||
See [the 0.24.0 specification audit](V0.24.0_AUDIT.md).
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ The encrypted backup contains the durable application state needed for recovery,
|
||||
- other durable RustProxy data accepted by the backup manifest,
|
||||
- validated internal relative symlinks, including Certbot `live/` links into the same profile's `archive/` tree.
|
||||
|
||||
It excludes rebuildable/runtime data such as cache, logs, temporary files, sockets, PID/lock files and SQLite WAL/SHM sidecars. Symlinks are accepted only when their target is relative and resolves inside the staged RustProxy data tree; absolute, escaping or dangling links fail backup/restore. Restore recreates accepted links as links rather than dereferencing them.
|
||||
It excludes rebuildable/runtime data such as cache, logs, temporary files, sockets, PID/lock files, SQLite WAL/SHM sidecars and the managed Certbot Python environment (`acme-python`; the `acme_python` spelling is excluded as well). Certificate/ACME durable state remains included; the virtual environment is rebuilt when needed. Symlinks are accepted only when their target is relative and resolves inside the staged RustProxy data tree; absolute, escaping or dangling links fail backup/restore. Restore recreates accepted links as links rather than dereferencing them.
|
||||
|
||||
The outer container derives a backup key with Argon2id and encrypts authenticated chunks with AES-256-GCM. The backup password is independent from the local Secret Store key.
|
||||
|
||||
|
||||
+26
-33
@@ -1,45 +1,38 @@
|
||||
# Verification report - RustProxy 0.27.2
|
||||
# Verification report - RustProxy 0.27.3
|
||||
|
||||
## Scope
|
||||
|
||||
RustProxy 0.27.2 is a focused repair for managed ACME/Certbot execution while keeping SQLite schema 8 and the saved configuration format unchanged.
|
||||
RustProxy 0.27.3 fixes HTTP/3 `HEAD` cache eligibility, reduces persistent COLD-cache and rate-limit/IP memory retention, excludes rebuildable managed Python environments from backups, and updates the documented installation path. SQLite schema 8, the saved configuration format and COLD format 3 remain unchanged.
|
||||
|
||||
0.27.1 created the private environment under `<data-dir>/acme-python.installing`, installed Certbot there, validated it, and then renamed the directory to `<data-dir>/acme-python`. pip console scripts contain an absolute interpreter path, so `bin/certbot` could retain a shebang pointing at the removed staging directory and fail with `OSError` during issuance/renewal.
|
||||
The H3 repair is intentionally narrow: cache policy already accepts both `GET` and `HEAD`; the forwarding path now performs the existing bounded empty-body probe for both methods. This prevents an initially unpolled HTTP/3 receive body from being mistaken for request content.
|
||||
|
||||
0.27.2 validates and launches managed Certbot through the final venv interpreter with `python -I -m certbot`. Existing 0.27.1 environments can therefore be reused without deleting `/var/lib/rustproxy/acme-python`. The system/distribution Certbot fallback remains unchanged.
|
||||
The COLD index no longer stores full response headers or origin validator values for every disk object. These are loaded and validated from the existing metadata JSON only on an actual COLD read. The in-memory SHA-256 is 32 bytes and the map-owned key is not duplicated inside `ColdEntry`. Rate limiting now groups IP buckets below one vhost string, sweeps expired fixed-window buckets during requests and periodic runtime maintenance, and shrinks sparse hash tables before allocator trimming.
|
||||
|
||||
The 0.27.1 first-administrator, runtime-status and route/list sorting behavior remains unchanged.
|
||||
Compression was reviewed but its wire behavior was not changed: it remains streaming through `tower_http`, uses `CompressionLevel::Fastest`, and does not aggregate a complete response body before encoding. Its primary RAM cost is therefore per concurrently active compressed response/codec state, not long-lived retained objects.
|
||||
|
||||
## Regression coverage
|
||||
|
||||
Static/source contracts cover the managed runtime path and pinned package set, distro fallback markers and the new module-based Certbot launch path. A dedicated relocation regression reproduces the stale-shebang failure after renaming a venv and verifies that module execution through the final interpreter remains valid. Existing release-smoke, language/OpenAPI, source-module, security, web-server, browser-service and Docker-agent gates remain part of the Rust-independent verification set.
|
||||
Application-coupled Python is now separated under `scripts/runtime`, `scripts/build`, `scripts/deploy` and `scripts/migration`; QA/dev/audit/legacy helpers live under `tools/`, while OPNsense/RouterOS helpers live under `integrations/`. Release checks validate the new paths and archive contents.
|
||||
|
||||
## Verification status in this workspace
|
||||
|
||||
Rust/Cargo is not installed in this workspace. Rust-independent checks are run here; compiler/type/runtime gates remain explicitly open until this source is checked on a Rust-enabled release host.
|
||||
Rust/Cargo is not installed in this workspace. Rust-independent checks are run here; compiler/type/runtime gates and real H2/H3 protocol execution remain open until this source is checked on a Rust-enabled host.
|
||||
|
||||
| Check | Status |
|
||||
|---|---|
|
||||
| managed ACME relocated-venv regression (`release_smoke.py`) | PASS |
|
||||
| changed administration JavaScript syntax (`node --check`) | PASS |
|
||||
| language/OpenAPI JSON parse | PASS |
|
||||
| `python3 scripts/langs_check.py` | PASS |
|
||||
| `python3 scripts/memory_contracts.py` | PASS |
|
||||
| `python3 scripts/web_server_contracts.py` | PASS |
|
||||
| `python3 scripts/security_contracts.py` | PASS |
|
||||
| `python3 scripts/security_dependency_contracts.py` | PASS |
|
||||
| `python3 scripts/source_module_contracts.py` | PASS |
|
||||
| `python3 scripts/rust_module_contracts.py` | PASS |
|
||||
| `python3 scripts/docs_check.py` | PASS |
|
||||
| `python3 scripts/release_smoke.py` | PASS |
|
||||
| `node scripts/web_service_ui_test.mjs` | PASS |
|
||||
| `go test ./...` in `docker-agent` | PASS |
|
||||
| `python3 scripts/check_source.py` | PASS; explicitly reports Rust parsing/types/runtime as not verified |
|
||||
| `./dev.sh test` | Rust-independent gates PASS; wrapper exits 127 at the Rust stage because Cargo is unavailable |
|
||||
| `./dev.sh check` | BLOCKED: Rust/Cargo toolchain unavailable |
|
||||
| `cargo fmt --check` | BLOCKED: Rust/Cargo toolchain unavailable |
|
||||
| `cargo clippy --all-targets --all-features -- -D warnings` | BLOCKED: Rust/Cargo toolchain unavailable |
|
||||
| `cargo test --all-targets --all-features` | BLOCKED: Rust/Cargo toolchain unavailable |
|
||||
| complete release build | BLOCKED at Rust compilation: Rust/Cargo toolchain unavailable |
|
||||
The final status table is refreshed from the release checks performed for this archive. Static/source contracts do not substitute for Rust parsing, type checking, unit tests or real protocol runtime tests.
|
||||
|
||||
The 0.27.2 source archive must not be described as compiler-verified or production-ready until the Rust-enabled release-host gates pass. Static/source contracts do not substitute for Rust parsing, type checking, Clippy, unit tests or real protocol runtime tests.
|
||||
## Release checks
|
||||
|
||||
| Check | Result |
|
||||
| --- | --- |
|
||||
| Rust/source module contracts | PASS |
|
||||
| Bounded-memory/cache/rate-limit/compression contracts | PASS |
|
||||
| Documentation and language-pack checks | PASS |
|
||||
| Web Server, security and dependency source contracts | PASS |
|
||||
| `tools/checks/check_source.py` | PASS (compiler/runtime explicitly not verified) |
|
||||
| `tools/checks/release_smoke.py` | PASS |
|
||||
| Python syntax (`py_compile`) for changed release scripts | PASS |
|
||||
| Browser Web Service UI regression (`node tools/checks/web_service_ui_test.mjs`) | PASS |
|
||||
| Docker Agent (`go test ./...`) | PASS |
|
||||
| `./dev.sh test` static/pre-compiler stages | PASS |
|
||||
| Rust `cargo fmt/check/test`, linking and Rust unit tests | NOT VERIFIED - Cargo/Rust is not installed in this workspace |
|
||||
| Live rebuilt HTTP/2 + HTTP/3 `HEAD` cache test | NOT VERIFIED - requires a compiled 0.27.3 binary and QUIC-capable runtime |
|
||||
|
||||
`./dev.sh test` exits with status 127 only when it reaches the Rust toolchain gate; all earlier release/static checks complete successfully.
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
| Requirement | Source implementation | Status |
|
||||
|---|---|---|
|
||||
| Bound glibc arena proliferation for managed systemd installs | `deploy/rustproxy.service` and `scripts/deploy.py::service_unit` set `MALLOC_ARENA_MAX=2`; Debian packaging moved to the external packaging repository in 0.27.0, and `/etc/rustproxy/environment` remains the administrator override | implemented in source |
|
||||
| Bound glibc arena proliferation for managed systemd installs | `deploy/rustproxy.service` and `scripts/deploy/deploy.py::service_unit` set `MALLOC_ARENA_MAX=2`; Debian packaging moved to the external packaging repository in 0.27.0, and `/etc/rustproxy/environment` remains the administrator override | implemented in source |
|
||||
| Avoid a duplicate identity transform buffer | `src/transform/mod.rs::decode` returns `Cow<[u8]>`; identity input is borrowed and compressed input owns only the decoded buffer | implemented in source |
|
||||
| Reduce JS/CSS minifier peak allocations | `src/transform/js.rs` and `src/transform/css.rs` iterate with `Peekable<Chars>` rather than collecting `Vec<char>`, and return the completed `String` allocation with `into_bytes()` | implemented in source |
|
||||
| Release temporary transform buffers earlier | successful changed transforms drop the decoded/collected origin buffers before final response construction | implemented in source |
|
||||
|
||||
@@ -50,7 +50,7 @@ This audit is source/static in the packaging workspace because Rust/Cargo and he
|
||||
|
||||
## Added regression coverage
|
||||
|
||||
Rust tests cover preset serialization/database reopen, preset-only cache identity stability, transform memory-budget math, bounded decompression, false Content-Length aggregation, proactive pending-task limits, stale revalidation limits and single-flight COLD cleanup. `scripts/memory_contracts.py` independently verifies the source-level bounds above when Cargo is unavailable and is part of `./dev.sh test`.
|
||||
Rust tests cover preset serialization/database reopen, preset-only cache identity stability, transform memory-budget math, bounded decompression, false Content-Length aggregation, proactive pending-task limits, stale revalidation limits and single-flight COLD cleanup. `tools/checks/memory_contracts.py` independently verifies the source-level bounds above when Cargo is unavailable and is part of `./dev.sh test`.
|
||||
|
||||
## Release-host acceptance gate
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ The dedicated Transform dashboard also shows current process-wide reserved bytes
|
||||
- local atomic inflight reservation obeys a runtime concurrency ceiling;
|
||||
- SQLite close/reopen preserves transform preset, memory budget and concurrency limit;
|
||||
- cache identity ignores resource-only changes but rotates for transform representation/eligibility changes;
|
||||
- `scripts/memory_contracts.py` verifies the new dynamic memory/concurrency bounds without Rust;
|
||||
- `tools/checks/memory_contracts.py` verifies the new dynamic memory/concurrency bounds without Rust;
|
||||
- source/release smoke checks verify the Transform UI, OpenAPI fields and release identity.
|
||||
- source/release smoke checks reject `anyhow::Context` on the boxed bounded-body collection error and require explicit `anyhow::Error` mapping.
|
||||
|
||||
|
||||
@@ -97,12 +97,12 @@ The Reader initially embedded a generated `OUT_DIR/docs-content.json`. A clean/i
|
||||
|
||||
## Verification performed in this workspace
|
||||
|
||||
- `python3 scripts/check_source.py` - PASS.
|
||||
- `python3 scripts/release_smoke.py` - PASS.
|
||||
- `python3 scripts/memory_contracts.py` - PASS.
|
||||
- `python3 scripts/web_server_contracts.py` - PASS.
|
||||
- `python3 scripts/docs_check.py` - PASS.
|
||||
- `python3 scripts/langs_check.py` - PASS.
|
||||
- `python3 tools/checks/check_source.py` - PASS.
|
||||
- `python3 tools/checks/release_smoke.py` - PASS.
|
||||
- `python3 tools/checks/memory_contracts.py` - PASS.
|
||||
- `python3 tools/checks/web_server_contracts.py` - PASS.
|
||||
- `python3 tools/checks/docs_check.py` - PASS.
|
||||
- `python3 tools/checks/langs_check.py` - PASS.
|
||||
- `node --check` for app, Settings, Docs Reader, Web Server and vhost administration JavaScript - PASS.
|
||||
- OpenAPI and both language JSON files parse - PASS.
|
||||
- `bash -n dev.sh` - PASS.
|
||||
|
||||
@@ -23,15 +23,15 @@ The reference/generated systemd units were also aligned with the stronger Debian
|
||||
|
||||
Passed in the audit environment:
|
||||
|
||||
- `python3 scripts/check_source.py`
|
||||
- `python3 scripts/web_server_contracts.py`
|
||||
- `python3 scripts/security_contracts.py`
|
||||
- `python3 scripts/security_dependency_contracts.py` (offline minimum-version gates for RustSec items reviewed on 2026-10-04)
|
||||
- `node scripts/web_service_ui_test.mjs` (preset selection + Save serialization regression)
|
||||
- `python3 scripts/memory_contracts.py`
|
||||
- `python3 scripts/docs_check.py`
|
||||
- `python3 scripts/langs_check.py`
|
||||
- `python3 scripts/release_smoke.py`
|
||||
- `python3 tools/checks/check_source.py`
|
||||
- `python3 tools/checks/web_server_contracts.py`
|
||||
- `python3 tools/checks/security_contracts.py`
|
||||
- `python3 tools/checks/security_dependency_contracts.py` (offline minimum-version gates for RustSec items reviewed on 2026-10-04)
|
||||
- `node tools/checks/web_service_ui_test.mjs` (preset selection + Save serialization regression)
|
||||
- `python3 tools/checks/memory_contracts.py`
|
||||
- `python3 tools/checks/docs_check.py`
|
||||
- `python3 tools/checks/langs_check.py`
|
||||
- `python3 tools/checks/release_smoke.py`
|
||||
- `node --check` for all administration JavaScript
|
||||
- `bash -n` for managed shell scripts
|
||||
- `go test ./...` for `docker-agent`
|
||||
@@ -41,7 +41,7 @@ Passed in the audit environment:
|
||||
- hard-coded secret scan for common private-key/token patterns: no production secret found; documentation placeholders only
|
||||
- manual review of the locked major network/security dependencies against current RustSec advisories, plus an offline regression deny-list for the reviewed 2026 malicious-crate campaign
|
||||
|
||||
`./dev.sh test` runs every non-Rust phase successfully and then stops because `cargo/rustc` are unavailable. `scripts/admin_audit.py` was also attempted, but no compiled/running RustProxy administration listener exists in this workspace, so its live probes are **blocked/unexecuted**, not passed.
|
||||
`./dev.sh test` runs every non-Rust phase successfully and then stops because `cargo/rustc` are unavailable. `tools/audit/admin_audit.py` was also attempted, but no compiled/running RustProxy administration listener exists in this workspace, so its live probes are **blocked/unexecuted**, not passed.
|
||||
|
||||
## Dependency review
|
||||
|
||||
@@ -119,7 +119,7 @@ Do not treat this audit as production approval until all of these pass on the ex
|
||||
|
||||
1. Install stable Rust/Cargo and run `./dev.sh doctor`, `./dev.sh check`, `./dev.sh test`, `cargo test --locked` and a release build.
|
||||
2. Run `cargo audit` (and preferably OSV/Trivy or equivalent) against the exact `Cargo.lock`; resolve vulnerabilities and review unmaintained/yanked transitive crates.
|
||||
3. Start the exact release binary under the target systemd unit and run `scripts/admin_audit.py` against loopback.
|
||||
3. Start the exact release binary under the target systemd unit and run `tools/audit/admin_audit.py` against loopback.
|
||||
4. Run active malformed HTTP tests for duplicate/ambiguous framing, oversized headers, slow headers/bodies, oversized request bodies, H2/H3 stream floods, WebSocket idle/connection exhaustion and Range edge cases.
|
||||
5. Fuzz/property-test `routing_path`, rewrite/`try_files`, `.htaccess`, CGI header parsing and backup restore parsers.
|
||||
6. Run filesystem escape tests using symlink races, nested links, renames and mount/bind-mount edge cases on the production kernel/filesystem.
|
||||
|
||||
+11
-11
@@ -25,23 +25,23 @@ The current source tree contains the intended corrections:
|
||||
- the split Web Server response-header fragment imports sibling `htaccess` explicitly and no longer retains the stale `ACCEPT_ENCODING`/`FromStr` imports;
|
||||
- split proxy/config/Prometheus fragments no longer export unnecessary `mut` bindings or unused values from their `include!` tuples.
|
||||
|
||||
`python3 scripts/rust_module_contracts.py` checks these conditions without a compiler. A real `cargo check --locked --all-targets` remains mandatory on the release host.
|
||||
`python3 tools/checks/rust_module_contracts.py` checks these conditions without a compiler. A real `cargo check --locked --all-targets` remains mandatory on the release host.
|
||||
|
||||
## Verification gates
|
||||
|
||||
Rust-independent acceptance for this package includes:
|
||||
|
||||
```text
|
||||
python3 scripts/rust_module_contracts.py
|
||||
python3 scripts/source_module_contracts.py
|
||||
python3 scripts/check_source.py
|
||||
python3 scripts/release_smoke.py
|
||||
python3 scripts/memory_contracts.py
|
||||
python3 scripts/web_server_contracts.py
|
||||
python3 scripts/security_contracts.py
|
||||
python3 scripts/security_dependency_contracts.py
|
||||
python3 scripts/docs_check.py
|
||||
python3 scripts/langs_check.py
|
||||
python3 tools/checks/rust_module_contracts.py
|
||||
python3 tools/checks/source_module_contracts.py
|
||||
python3 tools/checks/check_source.py
|
||||
python3 tools/checks/release_smoke.py
|
||||
python3 tools/checks/memory_contracts.py
|
||||
python3 tools/checks/web_server_contracts.py
|
||||
python3 tools/checks/security_contracts.py
|
||||
python3 tools/checks/security_dependency_contracts.py
|
||||
python3 tools/checks/docs_check.py
|
||||
python3 tools/checks/langs_check.py
|
||||
go test ./... # from docker-agent/
|
||||
```
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ After `/api/acme/issue` accepts an issuance request, the UI remembers that job I
|
||||
|
||||
## NPM migration helper
|
||||
|
||||
`scripts/migrate_npm.py` imports sibling modules `scripts/migrate_npm_core.py` and `scripts/migrate_npm_api.py`. They use the Python standard library and require no third-party package installation, but all three files must stay together. The documented container command now mounts the complete `scripts` directory.
|
||||
`scripts/migration/migrate_npm.py` imports sibling modules `scripts/migration/migrate_npm_core.py` and `scripts/migration/migrate_npm_api.py`. They use the Python standard library and require no third-party package installation, but all three files must stay together. The current documented container command mounts the dedicated `scripts/migration` directory.
|
||||
|
||||
## ACL policy composition
|
||||
|
||||
|
||||
@@ -25,3 +25,10 @@ The existing system/distribution Certbot fallback continues to execute its disco
|
||||
## Upgrade behavior
|
||||
|
||||
No ACME profile, account, certificate or credential migration is required. An `acme-python` environment created by 0.27.1 can be reused as-is because 0.27.2 bypasses the stale console-script shebang and imports Certbot from the environment's installed Python modules.
|
||||
|
||||
|
||||
## 0.27.3 correction
|
||||
|
||||
The module-based workaround described above was invalid for current Certbot packages: Certbot 5.8.0 installs a `certbot` console entry point but does not expose a runnable `certbot.__main__`, so `python -m certbot` fails even when installation and plugin imports are valid.
|
||||
|
||||
0.27.3 no longer relocates a freshly built managed environment after pip creates its console scripts. The environment is created/rebuilt directly at `<data-dir>/acme-python`, validation executes `<data-dir>/acme-python/bin/certbot --version`, and issuance/renewal use that same final console entry point. An older environment with a stale relocated shebang fails validation and is rebuilt at the final path.
|
||||
@@ -0,0 +1,58 @@
|
||||
# RustProxy 0.27.3 implementation note
|
||||
|
||||
## Scope
|
||||
|
||||
0.27.3 aligns HTTP/3 `HEAD` cache handling with HTTP/2, reduces long-lived cache/rate-limit memory, excludes the rebuildable managed Python runtime from backups, and promotes package repositories as the normal installation path. SQLite schema 8, saved configuration compatibility and the COLD cache on-disk format remain unchanged.
|
||||
|
||||
## HTTP/3 HEAD cache parity
|
||||
|
||||
HTTP semantics do not make `HEAD` meaningfully different between HTTP/2 and HTTP/3. RustProxy already allows `GET` and `HEAD` in cache policy, but the forwarding path actively probed an initially unknown request-body stream only for `GET`.
|
||||
|
||||
The HTTP/3 receive body starts with an unknown end-of-stream state until it is polled. A body-less `HEAD` could therefore reach cache policy as `body_empty=false` and be bypassed with `request_body`. HTTP/2 happened to expose end-of-stream immediately for the same curl request.
|
||||
|
||||
0.27.3 runs the existing bounded `empty_for_cache` probe for both `GET` and `HEAD`. A genuinely body-bearing request is still bypassed, while a body-less H3 `HEAD` follows the same cache lookup path as H2.
|
||||
|
||||
## COLD cache RAM
|
||||
|
||||
The COLD tier stores bodies on disk, but its old in-memory index retained a complete response `HeaderMap`, origin validators, a duplicate 32-byte cache key and a 64-character SHA-256 string for every object. With many small COLD objects, these metadata allocations could become a material RSS component even though body bytes were no longer resident.
|
||||
|
||||
0.27.3 keeps the fields needed for matching, invalidation, dashboard rows, LRU and freshness in the COLD index, but removes the persistent header/validator copies. On a real COLD read RustProxy reads and validates the existing metadata JSON, materializes headers/validators for that request, verifies the body digest and then promotes/serves the object as before. The in-memory digest is stored as 32 raw bytes and the map-owned cache key is no longer duplicated inside `ColdEntry`.
|
||||
|
||||
This trades one metadata-file read on a COLD hit for substantially lower long-lived metadata RAM. HOT behavior, COLD disk format, invalidation fields and response semantics are unchanged.
|
||||
|
||||
## Rate-limit/IP retention
|
||||
|
||||
The previous limiter keyed every client bucket by `(revision, vhost String, IP)`, duplicating the vhost allocation for each IP. Expired buckets were normally retained until the global 65,536-entry bound caused cleanup.
|
||||
|
||||
0.27.3 groups client-IP buckets below a single vhost key, stores the active revision once and removes buckets whose fixed window has expired. Cleanup runs opportunistically during requests and from the existing periodic runtime-maintenance loop, so idle traffic no longer leaves expired IP state resident indefinitely. Sparse client/vhost hash tables are shrunk after reclamation and the allocator trim follows the periodic sweep. Removing an expired bucket is behavior-equivalent to the existing next-request reset: the next request starts a new fixed window with count zero. The global client-bucket bound remains 65,536.
|
||||
|
||||
## Compression RAM assessment
|
||||
|
||||
Downstream compression remains streaming through `tower_http::compression::Compression`. It does not collect the complete response body before encoding, and `CompressionLevel::Fastest` remains selected. The long-lived cache does not keep encoder state. Compression memory therefore scales primarily with the number and codec type of concurrently active compressed responses rather than the number of cached objects or historical requests.
|
||||
|
||||
No compression algorithm or negotiation behavior is changed in 0.27.3 because the existing implementation is already bounded by streaming response lifetime. Brotli and Zstandard can have larger per-stream codec state than gzip/deflate, so concurrency remains the main compression-RAM factor under high load.
|
||||
|
||||
## Backup scope
|
||||
|
||||
`acme-python` is a managed/rebuildable Python virtual environment, not authoritative application state. Backups now skip both `acme-python` and the underscore spelling `acme_python`, while certificate/configuration/secret state remains covered by the existing backup logic.
|
||||
|
||||
## Installation paths
|
||||
|
||||
The documented normal installation/update paths are:
|
||||
|
||||
- DEB/RPM packages: `https://rustproxy.linuxiarz.pl/`
|
||||
- OPNsense plugin: `https://rustproxy-opn.linuxiarz.pl/`
|
||||
|
||||
The Debian 13+ source bootstrap remains available as an optional source/development path.
|
||||
|
||||
## Managed ACME bootstrap diagnostics
|
||||
|
||||
The managed ACME runtime now distinguishes private-environment creation, pip preparation, Certbot package installation and final plugin validation failures instead of collapsing all of them into one generic setup error. The selected stage is also emitted as a warning when no distribution Certbot fallback is available.
|
||||
|
||||
On distributions enforcing PEP 668, the application-local `virtualenv` bootstrap first uses normal `pip --target`; if the distribution rejects that operation, RustProxy retries with `--break-system-packages` while still writing exclusively into the private bootstrap directory under the RustProxy data tree. This does not install or modify packages in the distribution Python environment.
|
||||
|
||||
## Source-tool separation
|
||||
|
||||
0.27.3 also separates application-coupled scripts from auxiliary tooling without changing proxy behavior. `scripts/runtime`, `scripts/build`, `scripts/deploy` and `scripts/migration` contain code directly tied to application/build/deployment workflows. Compiler-independent checks, mocks, audit probes and the retired Python backup helper moved to `tools/`. OPNsense and RouterOS helpers moved to `integrations/`.
|
||||
|
||||
The shell entry points, `build.rs`, embedded ACME source path, source upgrader, deterministic source archive and release checks were updated together so the relocation is part of the same 0.27.3 development release.
|
||||
+1
-1
@@ -19,7 +19,7 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw
|
||||
are intentionally absent from Prometheus labels.
|
||||
|
||||
Import the JSON in Grafana and select a Prometheus datasource when prompted. The
|
||||
dashboard expects RustProxy 0.27.2 metric names and a scrape interval around 15s.
|
||||
dashboard expects RustProxy 0.27.3 metric names and a scrape interval around 15s.
|
||||
Collectors can be disabled in RustProxy settings; panels for disabled collectors
|
||||
will simply show no series.
|
||||
|
||||
|
||||
@@ -4529,9 +4529,9 @@
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "RustProxy 0.27.2 - Operations",
|
||||
"title": "RustProxy 0.27.3 - Operations",
|
||||
"uid": "rustproxy-0170",
|
||||
"version": 5,
|
||||
"weekStart": "",
|
||||
"description": "RustProxy 0.27.2 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
|
||||
"description": "RustProxy 0.27.3 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
|
||||
}
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
# Install an already built binary; never download or compile code as root.
|
||||
set -euo pipefail
|
||||
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
exec python3 "$ROOT/scripts/deploy.py" install "$@"
|
||||
exec python3 "$ROOT/scripts/deploy/deploy.py" install "$@"
|
||||
@@ -0,0 +1,7 @@
|
||||
# External integrations
|
||||
|
||||
Platform-specific helpers that integrate RustProxy with software outside the application itself.
|
||||
They are not required by the RustProxy runtime.
|
||||
|
||||
- `opnsense-acme/` - OPNsense os-acme-client certificate deployment helper.
|
||||
- `routeros-oci-build/` - RouterOS-oriented OCI build/publish helper.
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+1
-1
@@ -29,7 +29,7 @@ RUN apt-get update \
|
||||
&& install -d -o rustproxy -g rustproxy -m 0750 /var/log/rustproxy
|
||||
|
||||
COPY --from=build /build/target/release/rust-proxy /usr/local/bin/rust-proxy
|
||||
COPY scripts/routeros-oci-build/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
COPY integrations/routeros-oci-build/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
|
||||
|
||||
RUN chmod 0755 \
|
||||
/usr/local/bin/rust-proxy \
|
||||
File renamed without changes.
@@ -27,19 +27,20 @@ Copy this directory as:
|
||||
RustProxy/
|
||||
Cargo.toml
|
||||
Cargo.lock
|
||||
scripts/
|
||||
integrations/
|
||||
routeros-oci-build/
|
||||
build-routeros-oci.sh
|
||||
Dockerfile.routeros
|
||||
Dockerfile.routeros.dockerignore
|
||||
.env.routeros.example
|
||||
.env
|
||||
```
|
||||
|
||||
Create `.env`:
|
||||
|
||||
```bash
|
||||
cp scripts/routeros-oci-build/.env.example \
|
||||
scripts/routeros-oci-build/.env
|
||||
cp integrations/routeros-oci-build/.env.routeros.example \
|
||||
integrations/routeros-oci-build/.env
|
||||
```
|
||||
|
||||
Then edit the Zot address and credentials.
|
||||
@@ -63,8 +64,8 @@ Credentials are sent to `docker login` using `--password-stdin`.
|
||||
## Check configuration
|
||||
|
||||
```bash
|
||||
ENV_FILE=scripts/routeros-oci-build/.env \
|
||||
bash scripts/routeros-oci-build/build-routeros-oci.sh config
|
||||
ENV_FILE=integrations/routeros-oci-build/.env \
|
||||
bash integrations/routeros-oci-build/build-routeros-oci.sh config
|
||||
```
|
||||
|
||||
The repository root is detected using Git. If that is unavailable, the script walks upward until it finds `Cargo.toml`.
|
||||
@@ -72,8 +73,8 @@ The repository root is detected using Git. If that is unavailable, the script wa
|
||||
## Build and push multi-arch image
|
||||
|
||||
```bash
|
||||
ENV_FILE=scripts/routeros-oci-build/.env \
|
||||
bash scripts/routeros-oci-build/build-routeros-oci.sh push
|
||||
ENV_FILE=integrations/routeros-oci-build/.env \
|
||||
bash integrations/routeros-oci-build/build-routeros-oci.sh push
|
||||
```
|
||||
|
||||
With RustProxy version `0.25.0` and the defaults this publishes:
|
||||
@@ -89,15 +90,15 @@ Each tag points to the same amd64 + arm64 OCI manifest.
|
||||
## Local single-architecture build
|
||||
|
||||
```bash
|
||||
ENV_FILE=scripts/routeros-oci-build/.env \
|
||||
bash scripts/routeros-oci-build/build-routeros-oci.sh load amd64
|
||||
ENV_FILE=integrations/routeros-oci-build/.env \
|
||||
bash integrations/routeros-oci-build/build-routeros-oci.sh load amd64
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```bash
|
||||
ENV_FILE=scripts/routeros-oci-build/.env \
|
||||
bash scripts/routeros-oci-build/build-routeros-oci.sh load arm64
|
||||
ENV_FILE=integrations/routeros-oci-build/.env \
|
||||
bash integrations/routeros-oci-build/build-routeros-oci.sh load arm64
|
||||
```
|
||||
|
||||
`load arm64` loads an ARM64 image into Docker; it does not attempt to start the container.
|
||||
+1
-1
@@ -42,7 +42,7 @@ PROJECT_ROOT="$(resolve_project_root)" || {
|
||||
}
|
||||
|
||||
# Supports the invocation:
|
||||
# ENV_FILE=scripts/routeros-oci-build/.env bash scripts/routeros-oci-build/build-routeros-oci.sh push
|
||||
# ENV_FILE=integrations/routeros-oci-build/.env bash integrations/routeros-oci-build/build-routeros-oci.sh push
|
||||
ENV_FILE="${ROUTEROS_OCI_ENV_FILE:-${ENV_FILE:-$SCRIPT_DIR/.env}}"
|
||||
if [[ "$ENV_FILE" != /* ]]; then
|
||||
ENV_FILE="$PROJECT_ROOT/$ENV_FILE"
|
||||
File renamed without changes.
+1
-1
@@ -34,7 +34,7 @@ triggers a Cargo rebuild. Duplicate locales, invalid metadata, non-string values
|
||||
and oversized packs cause a build error. Limits: 128 packs, 256 KiB per pack,
|
||||
4096 keys per pack, 8 MiB total bundle.
|
||||
|
||||
Run `python3 scripts/langs_check.py` before a build. The checker also validates
|
||||
Run `python3 tools/checks/langs_check.py` before a build. The checker also validates
|
||||
that every static UI translation key has an English fallback. Extra JSON packs survive the managed updater when the new release does not contain
|
||||
the same filename. Keep a separate backup too; shipped filenames are replaced and
|
||||
the previous bytes are retained in the source backup.
|
||||
+5
-1
@@ -183,7 +183,7 @@
|
||||
"saveCertificate": "Apply certificate",
|
||||
"reloadCertificates": "Reload certificate files",
|
||||
"certificateHelp": "Upload a PEM pair, reference files already in data/certs, or deploy an externally issued certificate with the installed RustProxy binary. Reload re-reads configured files without changing the configuration revision.",
|
||||
"certificateHint": "External Certbot hook (standard service install): rust-proxy --data-dir /var/lib/rustproxy tls deploy --id shared-cert --cert /etc/letsencrypt/live/example/fullchain.pem --key /etc/letsencrypt/live/example/privkey.pem",
|
||||
"certificateHint": "External Certbot hook (standard service install): /opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy tls deploy --id shared-cert --cert /etc/letsencrypt/live/example/fullchain.pem --key /etc/letsencrypt/live/example/privkey.pem",
|
||||
"emptyCertificates": "No explicit SNI bindings. The generated default certificate is used while the TLS listener is enabled.",
|
||||
"certificateSafety": "Certificates secure TLS connections, not routing. Assign a certificate to a vhost. Changes affect new handshakes.",
|
||||
"wsActive": "WebSocket tunnels",
|
||||
@@ -2148,6 +2148,10 @@
|
||||
"acme.runtime_python_missing": "Python is not available on this system. Automatic ACME generation and renewal are unavailable; PEM import and existing certificate files still work.",
|
||||
"acme.runtime_pip_missing": "Python is available, but RustProxy could not create a local pip environment and no distribution Certbot is available. Install Python with venv/pip or Certbot packages. PEM import and existing files still work.",
|
||||
"acme.runtime_setup_failed": "RustProxy could not create the local Python/pip environment and no distribution Certbot is available. Check network/package access or install Certbot from the distribution. PEM import and existing files still work.",
|
||||
"acme.runtime_venv_failed": "Python and pip are available, but RustProxy could not create its private environment in the data directory. Check venv/virtualenv support and data-directory permissions. No system Certbot is available.",
|
||||
"acme.runtime_venv_pip_failed": "The private Python environment was created, but pip/ensurepip could not be started inside it. Check the Python venv/ensurepip installation. No system Certbot is available.",
|
||||
"acme.runtime_certbot_install_failed": "The private Python environment works, but pip could not install the Certbot packages. Check PyPI/proxy/DNS/TLS access. No system Certbot is available.",
|
||||
"acme.runtime_certbot_validation_failed": "ACME packages were installed, but Certbot or one of the required plugins failed validation. Remove the acme-python directory and restart RustProxy, or check compatibility with the local Python.",
|
||||
"acme.runtime_data_dir_missing": "The RustProxy data directory is unavailable, so a private Python environment cannot be created. PEM import and existing certificate files still work."
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -183,7 +183,7 @@
|
||||
"saveCertificate": "Zastosuj certyfikat",
|
||||
"reloadCertificates": "Wczytaj pliki certyfikatów",
|
||||
"certificateHelp": "Wgraj parę PEM, wskaż pliki w data/certs albo wdrażaj certyfikat z zewnętrznego wystawcy zainstalowaną binarką RustProxy. Ponowne wczytanie odczytuje skonfigurowane pliki bez zmiany rewizji.",
|
||||
"certificateHint": "Hook Certbota (standardowa instalacja usługi): rust-proxy --data-dir /var/lib/rustproxy tls deploy --id shared-cert --cert /etc/letsencrypt/live/example/fullchain.pem --key /etc/letsencrypt/live/example/privkey.pem",
|
||||
"certificateHint": "Hook Certbota (standardowa instalacja usługi): /opt/rustproxy/current/rust-proxy --data-dir /var/lib/rustproxy tls deploy --id shared-cert --cert /etc/letsencrypt/live/example/fullchain.pem --key /etc/letsencrypt/live/example/privkey.pem",
|
||||
"emptyCertificates": "Brak jawnych powiązań SNI. Aktywny listener TLS używa wygenerowanego certyfikatu domyślnego.",
|
||||
"certificateSafety": "Certyfikaty samopodpisane służą do testów. Certyfikat nie tworzy trasy. Zmiana dotyczy nowych połączeń TLS.",
|
||||
"wsActive": "Tunele WebSocket",
|
||||
@@ -2148,6 +2148,10 @@
|
||||
"acme.runtime_python_missing": "Python nie jest dostępny w tym systemie. Automatyczne generowanie i odnawianie ACME są niedostępne; import PEM i istniejące pliki certyfikatów nadal działają.",
|
||||
"acme.runtime_pip_missing": "Python jest dostępny, ale RustProxy nie może utworzyć lokalnego środowiska pip, a systemowy Certbot nie jest dostępny. Zainstaluj Python z venv/pip albo pakiety Certbota. Import PEM i istniejące pliki nadal działają.",
|
||||
"acme.runtime_setup_failed": "RustProxy nie może utworzyć lokalnego środowiska Python/pip, a systemowy Certbot nie jest dostępny. Sprawdź dostęp do sieci/pakietów albo zainstaluj Certbota z dystrybucji. Import PEM i istniejące pliki nadal działają.",
|
||||
"acme.runtime_venv_failed": "Python i pip są dostępne, ale RustProxy nie może utworzyć prywatnego środowiska w katalogu danych. Sprawdź obsługę venv/virtualenv oraz uprawnienia do katalogu danych. Systemowy Certbot nie jest dostępny.",
|
||||
"acme.runtime_venv_pip_failed": "Prywatne środowisko Python zostało utworzone, ale nie udało się uruchomić w nim pip/ensurepip. Sprawdź instalację Python venv/ensurepip. Systemowy Certbot nie jest dostępny.",
|
||||
"acme.runtime_certbot_install_failed": "Prywatne środowisko Python działa, ale pip nie może zainstalować pakietów Certbota. Sprawdź dostęp do PyPI/proxy/DNS/TLS. Systemowy Certbot nie jest dostępny.",
|
||||
"acme.runtime_certbot_validation_failed": "Pakiety ACME zostały zainstalowane, ale Certbot lub jedna z wymaganych wtyczek nie przechodzi walidacji. Usuń katalog acme-python i uruchom RustProxy ponownie albo sprawdź zgodność lokalnego Pythona.",
|
||||
"acme.runtime_data_dir_missing": "Katalog danych RustProxy jest niedostępny, więc nie można utworzyć prywatnego środowiska Python. Import PEM i istniejące pliki certyfikatów nadal działają."
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -12,7 +12,7 @@ import zipfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
MANAGED = [
|
||||
'src', 'tests', 'scripts', 'nagios', 'docker-agent', 'migrations', 'examples', 'docs', 'langs', 'deploy', 'grafana',
|
||||
'src', 'tests', 'scripts', 'tools', 'integrations', 'nagios', 'docker-agent', 'migrations', 'examples', 'docs', 'langs', 'deploy', 'grafana',
|
||||
'.github', '.cargo', 'Cargo.toml', 'Cargo.lock', 'build.rs', 'rust-toolchain.toml', 'UPDATE_CHECK.toml',
|
||||
'dev.sh', 'upgrade.sh', 'install.sh', 'install-debian13.sh', 'update.sh', 'README.md', 'ROADMAP.md',
|
||||
'CHANGELOG.md', 'AGENTS.md', '.env.example', '.gitignore', '.dockerignore',
|
||||
@@ -67,7 +67,7 @@ def release_datetime() -> tuple[int, int, int, int, int, int]:
|
||||
|
||||
def deterministic_mode(rel: Path) -> int:
|
||||
executable = rel.as_posix() in EXECUTABLE_ROOT or (
|
||||
rel.parts and rel.parts[0] == 'scripts' and rel.suffix in {'.py', '.sh'}
|
||||
rel.parts and rel.parts[0] in {'scripts', 'tools', 'integrations'} and rel.suffix in {'.py', '.sh'}
|
||||
) or (
|
||||
rel.parts and rel.parts[0] == 'nagios' and (rel.suffix in {'.py', '.sh'} or rel.name.startswith('check_'))
|
||||
) or (
|
||||
@@ -86,7 +86,7 @@ def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.27.2.zip')
|
||||
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.27.3.zip')
|
||||
output = parser.parse_args().output.resolve()
|
||||
paths = files()
|
||||
checksum = ROOT / 'SHA256SUMS'
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Application scripts
|
||||
|
||||
This directory contains scripts coupled to RustProxy application/build/deployment behavior.
|
||||
External development and QA helpers live under `tools/`; external platform integrations live under `integrations/`.
|
||||
|
||||
- `runtime/` - application-owned helpers embedded or invoked by RustProxy (`acme_provider.py`).
|
||||
- `build/` - build-time asset preparation used by `build.rs`.
|
||||
- `deploy/` - install/update and stopped source-tree upgrade implementation.
|
||||
- `migration/` - application migration utilities, currently the Nginx Proxy Manager importer.
|
||||
|
||||
Keep these paths stable because Rust source, `build.rs` and shell entry points reference them directly.
|
||||
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
|
||||
"""TLS verification uses Python's default trust store; never executes a shell."""
|
||||
started = time.monotonic()
|
||||
opener = build_opener(HttpsRedirects())
|
||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.27.2"})
|
||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.27.3"})
|
||||
with opener.open(request, timeout=30) as response, destination.open("wb") as output:
|
||||
length = response.headers.get("Content-Length")
|
||||
if length and int(length) > MAX_ARCHIVE:
|
||||
File renamed without changes.
@@ -31,7 +31,7 @@ import uuid
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
UNIT_PATH = Path('/etc/systemd/system/rustproxy.service')
|
||||
SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh')
|
||||
SERVICE = 'rustproxy.service'
|
||||
@@ -97,7 +97,7 @@ def digest(path: Path) -> str:
|
||||
def fetch_https(url: str, max_bytes: int) -> bytes:
|
||||
if urllib.parse.urlparse(url).scheme != 'https':
|
||||
raise ValueError('GeoIP downloads require HTTPS.')
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.27.2'})
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.27.3'})
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
if urllib.parse.urlparse(response.geturl()).scheme != 'https':
|
||||
raise ValueError('GeoIP download redirected away from HTTPS.')
|
||||
@@ -43,12 +43,18 @@ def _run_user(account, data: Path, args: list[str], timeout: int) -> subprocess.
|
||||
def _acme_python(data: Path) -> Path:
|
||||
return data / 'acme-python' / 'bin/python'
|
||||
|
||||
def _acme_certbot(data: Path) -> Path:
|
||||
return data / 'acme-python' / 'bin/certbot'
|
||||
|
||||
def _acme_runtime_valid(data: Path, account) -> bool:
|
||||
python = _acme_python(data)
|
||||
if not python.is_file():
|
||||
return False
|
||||
certbot = _acme_certbot(data)
|
||||
if not certbot.is_file():
|
||||
return False
|
||||
checks = ([str(python), '-I', '-c', 'import certbot, certbot_dns_cloudflare, certbot_dns_ovh, certbot_dns_rfc2136'],
|
||||
[str(python), '-I', '-m', 'certbot', '--version'])
|
||||
[str(certbot), '--version'])
|
||||
try:
|
||||
return all(_run_user(account, data, command, 30).returncode == 0 for command in checks)
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
@@ -9,7 +9,7 @@ import shutil
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
MANAGED = ["build.rs", "langs", "deploy", "debian", "grafana", "make_zip.py", "src", "tests", "scripts", "nagios", "docker-agent", "migrations", "examples", "docs", ".github", ".cargo",
|
||||
MANAGED = ["build.rs", "langs", "deploy", "debian", "grafana", "make_zip.py", "src", "tests", "scripts", "tools", "integrations", "nagios", "docker-agent", "migrations", "examples", "docs", ".github", ".cargo",
|
||||
"Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "dev.sh", "upgrade.sh", "install.sh", "update.sh",
|
||||
"README.md", "ROADMAP.md", "CHANGELOG.md", "AGENTS.md", "SHA256SUMS",
|
||||
".env.example", ".gitignore"]
|
||||
@@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
for name in MANAGED:
|
||||
if (target / name).is_symlink():
|
||||
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
|
||||
backup = target / ".upgrade-backup" / ("before-0.27.2-" + uuid.uuid4().hex[:12])
|
||||
backup = target / ".upgrade-backup" / ("before-0.27.3-" + uuid.uuid4().hex[:12])
|
||||
backup.mkdir(parents=True, mode=0o700)
|
||||
installed: list[str] = []
|
||||
moved: list[str] = []
|
||||
@@ -84,7 +84,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
remove_legacy = legacy.exists()
|
||||
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
|
||||
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
|
||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.27.2", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.27.3", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||
if remove_legacy:
|
||||
legacy.unlink()
|
||||
except BaseException:
|
||||
@@ -112,10 +112,10 @@ def main() -> None:
|
||||
os.umask(0o077)
|
||||
data_dir = args.data_dir or (Path(os.environ["RUST_PROXY_DATA_DIR"]) if os.environ.get("RUST_PROXY_DATA_DIR") else None)
|
||||
try:
|
||||
backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir)
|
||||
backup = upgrade(Path(__file__).resolve().parents[2], args.target, stopped=args.stopped, data_dir=data_dir)
|
||||
except (OSError, ValueError) as error:
|
||||
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
|
||||
print(f"Installed RustProxy 0.27.2. Previous managed source files: {backup}")
|
||||
print(f"Installed RustProxy 0.27.3. Previous managed source files: {backup}")
|
||||
print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.")
|
||||
print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh")
|
||||
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -1,39 +0,0 @@
|
||||
# Zot / OCI registry. Do not include a path in REGISTRY.
|
||||
REGISTRY=zot.linuxiarz.pl
|
||||
IMAGE_NAME=rustproxy
|
||||
|
||||
# Alternative shorthand instead of REGISTRY + IMAGE_NAME:
|
||||
# OCI_IMAGE=zot.example.net/network/rustproxy
|
||||
|
||||
# Zot Basic Auth / htpasswd or token credentials.
|
||||
OCI_USERNAME=admin
|
||||
OCI_PASSWORD=
|
||||
# OCI_TOKEN=
|
||||
|
||||
# RouterOS targets used by this helper.
|
||||
BUILD_PLATFORMS=linux/amd64,linux/arm64
|
||||
#BUILD_PLATFORMS=linux/amd64
|
||||
BUILDER=rustproxy
|
||||
|
||||
# RustProxy currently requires Rust 1.88.
|
||||
RUST_VERSION=1.88
|
||||
DEBIAN_SUITE=trixie
|
||||
|
||||
# Empty OCI_TAG means the script uses Cargo.toml package version.
|
||||
OCI_TAG=
|
||||
OCI_LATEST=1
|
||||
OCI_EXTRA_TAGS=
|
||||
|
||||
# Build behavior.
|
||||
OCI_PULL=1
|
||||
OCI_NO_CACHE=0
|
||||
OCI_VERIFY=1
|
||||
OCI_PROGRESS=plain
|
||||
|
||||
# 1 stores BuildKit cache in Zot as IMAGE_NAME:buildcache.
|
||||
# Recommended for CI/ephemeral buildx builders.
|
||||
OCI_REGISTRY_CACHE=0
|
||||
|
||||
# Optional BuildKit config, useful for an internal Zot with a custom CA.
|
||||
# BUILDER_CONFIG=/etc/buildkit/buildkitd.toml
|
||||
|
||||
@@ -168,10 +168,9 @@ def credentials(value: dict) -> bytes:
|
||||
def certbot_prefix(executable: str | None = None) -> list[str]:
|
||||
if executable:
|
||||
return [executable]
|
||||
managed_python = os.environ.get("RUSTPROXY_CERTBOT_PYTHON")
|
||||
if managed_python:
|
||||
managed_certbot = str(Path(managed_python).with_name("certbot"))
|
||||
return [managed_python, "-I", managed_certbot]
|
||||
managed_certbot = os.environ.get("RUSTPROXY_CERTBOT_EXECUTABLE")
|
||||
if managed_certbot:
|
||||
return [managed_certbot]
|
||||
discovered = shutil.which("certbot")
|
||||
if discovered:
|
||||
return [discovered]
|
||||
File renamed without changes.
@@ -0,0 +1,128 @@
|
||||
async fn create_virtual_environment(
|
||||
python: &Path,
|
||||
staging: &Path,
|
||||
system_pip: bool,
|
||||
shutdown: &tokio_util::sync::CancellationToken,
|
||||
) -> bool {
|
||||
let mut create = Command::new(python);
|
||||
create.arg("-m").arg("venv").arg(staging);
|
||||
if command_success(create, Duration::from_secs(180), shutdown).await {
|
||||
return true;
|
||||
}
|
||||
if !system_pip {
|
||||
return false;
|
||||
}
|
||||
let _ = tokio::fs::remove_dir_all(staging).await;
|
||||
// Fall back to an application-local virtualenv bootstrap.
|
||||
let bootstrap = staging.with_extension("virtualenv-bootstrap");
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
let install_bootstrap = |break_system_packages: bool| {
|
||||
let mut install = Command::new(python);
|
||||
install.args([
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"--no-input",
|
||||
"--no-cache-dir",
|
||||
"--retries",
|
||||
"3",
|
||||
"--timeout",
|
||||
"30",
|
||||
]);
|
||||
// Some distro pip builds enforce PEP 668 even for --target. Retrying with
|
||||
// --break-system-packages is safe here because the destination is the
|
||||
// private bootstrap directory; distribution site-packages are untouched.
|
||||
if break_system_packages {
|
||||
install.arg("--break-system-packages");
|
||||
}
|
||||
install.arg("--target").arg(&bootstrap).arg("virtualenv>=20,<21");
|
||||
install
|
||||
};
|
||||
let installed = command_success(
|
||||
install_bootstrap(false),
|
||||
Duration::from_secs(300),
|
||||
shutdown,
|
||||
)
|
||||
.await;
|
||||
if !installed {
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
if !command_success(
|
||||
install_bootstrap(true),
|
||||
Duration::from_secs(300),
|
||||
shutdown,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
let mut create = Command::new(python);
|
||||
create
|
||||
.arg("-c")
|
||||
.arg("import runpy,sys; sys.path.insert(0,sys.argv[1]); sys.argv=['virtualenv',sys.argv[2]]; runpy.run_module('virtualenv',run_name='__main__')")
|
||||
.arg(&bootstrap)
|
||||
.arg(staging);
|
||||
let created = command_success(create, Duration::from_secs(180), shutdown).await;
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
created
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum BuildFailure {
|
||||
CreateEnvironment,
|
||||
PreparePip,
|
||||
InstallPackages,
|
||||
Validate,
|
||||
}
|
||||
|
||||
async fn build_managed_environment(
|
||||
python: &Path,
|
||||
target: &Path,
|
||||
system_pip: bool,
|
||||
shutdown: &tokio_util::sync::CancellationToken,
|
||||
) -> Result<(), BuildFailure> {
|
||||
let legacy_stage = target.with_extension("installing");
|
||||
let _ = tokio::fs::remove_dir_all(&legacy_stage).await;
|
||||
let _ = tokio::fs::remove_dir_all(target).await;
|
||||
let _ = tokio::fs::remove_file(target).await;
|
||||
if !create_virtual_environment(python, target, system_pip, shutdown).await {
|
||||
let _ = tokio::fs::remove_dir_all(target).await;
|
||||
return Err(BuildFailure::CreateEnvironment);
|
||||
}
|
||||
let target_python = venv_python(target);
|
||||
let mut pip_check = Command::new(&target_python);
|
||||
pip_check.args(["-m", "pip", "--version"]);
|
||||
if !command_success(pip_check, Duration::from_secs(30), shutdown).await {
|
||||
let mut ensurepip = Command::new(&target_python);
|
||||
ensurepip.args(["-m", "ensurepip", "--upgrade"]);
|
||||
if !command_success(ensurepip, Duration::from_secs(120), shutdown).await {
|
||||
let _ = tokio::fs::remove_dir_all(target).await;
|
||||
return Err(BuildFailure::PreparePip);
|
||||
}
|
||||
}
|
||||
let mut pip = Command::new(&target_python);
|
||||
pip.args([
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"--no-input",
|
||||
"--no-cache-dir",
|
||||
"--retries",
|
||||
"3",
|
||||
"--timeout",
|
||||
"30",
|
||||
]);
|
||||
pip.args(CERTBOT_PACKAGES.iter().copied());
|
||||
if !command_success(pip, Duration::from_secs(600), shutdown).await {
|
||||
let _ = tokio::fs::remove_dir_all(target).await;
|
||||
return Err(BuildFailure::InstallPackages);
|
||||
}
|
||||
if !managed_environment_valid(target, shutdown).await {
|
||||
let _ = tokio::fs::remove_dir_all(target).await;
|
||||
return Err(BuildFailure::Validate);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+38
-115
@@ -60,7 +60,7 @@ impl Manager {
|
||||
struct RuntimeLaunch {
|
||||
python: PathBuf,
|
||||
prepend_path: Option<PathBuf>,
|
||||
managed_certbot_python: Option<PathBuf>,
|
||||
managed_certbot_executable: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn find_on_path(name: &str) -> Option<PathBuf> {
|
||||
@@ -107,6 +107,15 @@ fn venv_python(directory: &Path) -> PathBuf {
|
||||
venv_bin(directory).join("python")
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn venv_certbot(directory: &Path) -> PathBuf {
|
||||
venv_bin(directory).join("certbot.exe")
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
fn venv_certbot(directory: &Path) -> PathBuf {
|
||||
venv_bin(directory).join("certbot")
|
||||
}
|
||||
|
||||
async fn command_success(
|
||||
mut command: Command,
|
||||
timeout: Duration,
|
||||
@@ -145,9 +154,12 @@ async fn managed_environment_valid(
|
||||
if !command_success(imports, Duration::from_secs(20), shutdown).await {
|
||||
return false;
|
||||
}
|
||||
let certbot = venv_bin(directory).join("certbot");
|
||||
let mut version = Command::new(&python);
|
||||
version.arg("-I").arg(certbot).arg("--version");
|
||||
let certbot = venv_certbot(directory);
|
||||
if !certbot.is_file() {
|
||||
return false;
|
||||
}
|
||||
let mut version = Command::new(&certbot);
|
||||
version.arg("--version");
|
||||
command_success(version, Duration::from_secs(20), shutdown).await
|
||||
}
|
||||
|
||||
@@ -172,107 +184,7 @@ async fn system_has_pip(python: &Path, shutdown: &tokio_util::sync::Cancellation
|
||||
command_success(command, Duration::from_secs(20), shutdown).await
|
||||
}
|
||||
|
||||
async fn create_virtual_environment(
|
||||
python: &Path,
|
||||
staging: &Path,
|
||||
system_pip: bool,
|
||||
shutdown: &tokio_util::sync::CancellationToken,
|
||||
) -> bool {
|
||||
let mut create = Command::new(python);
|
||||
create.arg("-m").arg("venv").arg(staging);
|
||||
if command_success(create, Duration::from_secs(180), shutdown).await {
|
||||
return true;
|
||||
}
|
||||
if !system_pip {
|
||||
return false;
|
||||
}
|
||||
let _ = tokio::fs::remove_dir_all(staging).await;
|
||||
// Fall back to an application-local virtualenv bootstrap.
|
||||
let bootstrap = staging.with_extension("virtualenv-bootstrap");
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
let mut install = Command::new(python);
|
||||
install.args([
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"--no-input",
|
||||
"--no-cache-dir",
|
||||
"--retries",
|
||||
"3",
|
||||
"--timeout",
|
||||
"30",
|
||||
"--target",
|
||||
]);
|
||||
install.arg(&bootstrap).arg("virtualenv>=20,<21");
|
||||
if !command_success(install, Duration::from_secs(300), shutdown).await {
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
return false;
|
||||
}
|
||||
let mut create = Command::new(python);
|
||||
create
|
||||
.arg("-c")
|
||||
.arg("import runpy,sys; sys.path.insert(0,sys.argv[1]); sys.argv=['virtualenv',sys.argv[2]]; runpy.run_module('virtualenv',run_name='__main__')")
|
||||
.arg(&bootstrap)
|
||||
.arg(staging);
|
||||
let created = command_success(create, Duration::from_secs(180), shutdown).await;
|
||||
let _ = tokio::fs::remove_dir_all(&bootstrap).await;
|
||||
created
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum BuildFailure {
|
||||
Bootstrap,
|
||||
Install,
|
||||
}
|
||||
|
||||
async fn build_managed_environment(
|
||||
python: &Path,
|
||||
target: &Path,
|
||||
system_pip: bool,
|
||||
shutdown: &tokio_util::sync::CancellationToken,
|
||||
) -> Result<(), BuildFailure> {
|
||||
let legacy_stage = target.with_extension("installing");
|
||||
let _ = tokio::fs::remove_dir_all(&legacy_stage).await;
|
||||
let _ = tokio::fs::remove_dir_all(target).await;
|
||||
let _ = tokio::fs::remove_file(target).await;
|
||||
if !create_virtual_environment(python, target, system_pip, shutdown).await {
|
||||
let _ = tokio::fs::remove_dir_all(target).await;
|
||||
return Err(BuildFailure::Bootstrap);
|
||||
}
|
||||
let target_python = venv_python(target);
|
||||
let mut pip_check = Command::new(&target_python);
|
||||
pip_check.args(["-m", "pip", "--version"]);
|
||||
if !command_success(pip_check, Duration::from_secs(30), shutdown).await {
|
||||
let mut ensurepip = Command::new(&target_python);
|
||||
ensurepip.args(["-m", "ensurepip", "--upgrade"]);
|
||||
if !command_success(ensurepip, Duration::from_secs(120), shutdown).await {
|
||||
let _ = tokio::fs::remove_dir_all(target).await;
|
||||
return Err(BuildFailure::Bootstrap);
|
||||
}
|
||||
}
|
||||
let mut pip = Command::new(&target_python);
|
||||
pip.args([
|
||||
"-m",
|
||||
"pip",
|
||||
"install",
|
||||
"--disable-pip-version-check",
|
||||
"--no-input",
|
||||
"--no-cache-dir",
|
||||
"--retries",
|
||||
"3",
|
||||
"--timeout",
|
||||
"30",
|
||||
]);
|
||||
pip.args(CERTBOT_PACKAGES.iter().copied());
|
||||
if !command_success(pip, Duration::from_secs(600), shutdown).await
|
||||
|| !managed_environment_valid(target, shutdown).await
|
||||
{
|
||||
let _ = tokio::fs::remove_dir_all(target).await;
|
||||
return Err(BuildFailure::Install);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
include!("runtime_bootstrap.rs");
|
||||
|
||||
async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
let _permit = state
|
||||
@@ -309,7 +221,7 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
return Ok(RuntimeLaunch {
|
||||
python: venv_python(&managed),
|
||||
prepend_path: Some(venv_bin(&managed)),
|
||||
managed_certbot_python: Some(venv_python(&managed)),
|
||||
managed_certbot_executable: Some(venv_certbot(&managed)),
|
||||
});
|
||||
}
|
||||
let distro_certbot = find_on_path("certbot");
|
||||
@@ -357,7 +269,7 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
return Ok(RuntimeLaunch {
|
||||
python: venv_python(&managed),
|
||||
prepend_path: Some(venv_bin(&managed)),
|
||||
managed_certbot_python: Some(venv_python(&managed)),
|
||||
managed_certbot_executable: Some(venv_certbot(&managed)),
|
||||
});
|
||||
}
|
||||
if distro_certbot_available {
|
||||
@@ -374,13 +286,23 @@ async fn ensure_runtime(state: &App) -> Result<RuntimeLaunch, String> {
|
||||
return Ok(RuntimeLaunch {
|
||||
python,
|
||||
prepend_path: None,
|
||||
managed_certbot_python: None,
|
||||
managed_certbot_executable: None,
|
||||
});
|
||||
}
|
||||
let message = if build == Err(BuildFailure::Bootstrap) && !system_pip {
|
||||
"acme.runtime_pip_missing"
|
||||
} else {
|
||||
"acme.runtime_setup_failed"
|
||||
tracing::warn!(
|
||||
python = %python.display(),
|
||||
managed = %managed.display(),
|
||||
system_pip,
|
||||
failure = ?build,
|
||||
"managed ACME runtime setup failed; no system Certbot fallback available"
|
||||
);
|
||||
let message = match build {
|
||||
Err(BuildFailure::CreateEnvironment) if !system_pip => "acme.runtime_pip_missing",
|
||||
Err(BuildFailure::CreateEnvironment) => "acme.runtime_venv_failed",
|
||||
Err(BuildFailure::PreparePip) => "acme.runtime_venv_pip_failed",
|
||||
Err(BuildFailure::InstallPackages) => "acme.runtime_certbot_install_failed",
|
||||
Err(BuildFailure::Validate) => "acme.runtime_certbot_validation_failed",
|
||||
Ok(()) => "acme.runtime_setup_failed",
|
||||
};
|
||||
state.acme.set_runtime(RuntimeStatus {
|
||||
status: "unavailable",
|
||||
@@ -428,19 +350,20 @@ async fn worker(state: &App, directory: PathBuf, raw: Zeroizing<String>) -> Resu
|
||||
command
|
||||
.arg("-I")
|
||||
.arg("-c")
|
||||
.arg(include_str!("../../scripts/acme_provider.py"))
|
||||
.arg(include_str!("../../scripts/runtime/acme_provider.py"))
|
||||
.arg("--cert-dir")
|
||||
.arg(&directory)
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::null())
|
||||
.kill_on_drop(true);
|
||||
command.env_remove("RUSTPROXY_CERTBOT_EXECUTABLE");
|
||||
command.env_remove("RUSTPROXY_CERTBOT_PYTHON");
|
||||
if let Some(prepend) = launch.prepend_path.as_deref() {
|
||||
command.env("PATH", merged_path(prepend)?);
|
||||
}
|
||||
if let Some(python) = launch.managed_certbot_python.as_deref() {
|
||||
command.env("RUSTPROXY_CERTBOT_PYTHON", python);
|
||||
if let Some(certbot) = launch.managed_certbot_executable.as_deref() {
|
||||
command.env("RUSTPROXY_CERTBOT_EXECUTABLE", certbot);
|
||||
}
|
||||
#[cfg(unix)]
|
||||
{
|
||||
|
||||
@@ -409,6 +409,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
"/docs/V0.25.1_AUDIT.md"
|
||||
)),
|
||||
),
|
||||
reader_doc!("docs/V0.27.3_IMPLEMENTATION.md", "technical"),
|
||||
reader_doc!("docs/V0.27.2_IMPLEMENTATION.md", "technical"),
|
||||
reader_doc!("docs/V0.27.1_IMPLEMENTATION.md", "historical"),
|
||||
reader_doc!("docs/V0.27.0_IMPLEMENTATION.md", "technical"),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.27.2",
|
||||
"version": "0.27.3",
|
||||
"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": [
|
||||
|
||||
@@ -2823,7 +2823,7 @@
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="page-footer"><span>RustProxy <b>0.27.2</b></span><span id="footer-status">API - /
|
||||
<footer class="page-footer"><span>RustProxy <b>0.27.3</b></span><span id="footer-status">API - /
|
||||
dropped: 0</span></footer>
|
||||
</div>
|
||||
</main>
|
||||
@@ -3567,7 +3567,7 @@ max_execution_time = 30</code></pre>
|
||||
<article class="card about-card">
|
||||
<div class="about-product">
|
||||
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
|
||||
<div><span class="small-label">RustProxy</span><strong>0.27.2</strong></div>
|
||||
<div><span class="small-label">RustProxy</span><strong>0.27.3</strong></div>
|
||||
</div>
|
||||
<dl class="about-meta">
|
||||
<div>
|
||||
|
||||
@@ -462,6 +462,13 @@ fn hash_bytes(bytes: &[u8]) -> String {
|
||||
fn excluded_directory(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"cache" | "logs" | "tmp" | "temp" | "work" | "acme-http01"
|
||||
"cache"
|
||||
| "logs"
|
||||
| "tmp"
|
||||
| "temp"
|
||||
| "work"
|
||||
| "acme-http01"
|
||||
| "acme-python"
|
||||
| "acme_python"
|
||||
)
|
||||
}
|
||||
@@ -29,6 +29,13 @@ mod tests {
|
||||
assert!(validate_backup_version("999.0.0").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backup_excludes_rebuildable_managed_python_runtime() {
|
||||
assert!(excluded_directory("acme-python"));
|
||||
assert!(excluded_directory("acme_python"));
|
||||
assert!(!excluded_directory("certs"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn live_backup_round_trips_internal_certbot_symlink() {
|
||||
|
||||
Vendored
+1
-2
@@ -243,8 +243,7 @@ impl Cache {
|
||||
entry.ttl,
|
||||
entry.stale_while_revalidate,
|
||||
entry.stale_if_error,
|
||||
entry.origin_etag.is_some()
|
||||
|| entry.origin_last_modified.is_some(),
|
||||
entry.has_validator,
|
||||
)
|
||||
});
|
||||
if should_remove {
|
||||
|
||||
Vendored
+15
-18
@@ -99,19 +99,19 @@ impl Cache {
|
||||
if path.file_stem().and_then(|v| v.to_str()) != Some(file.key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
let Some(headers) = pairs_to_headers(&file.headers) else {
|
||||
if pairs_to_headers(&file.headers).is_none() {
|
||||
continue;
|
||||
};
|
||||
}
|
||||
if body_path.parent() != Some(shard.as_path()) {
|
||||
continue;
|
||||
}
|
||||
let Ok(body_meta) = std::fs::metadata(&body_path) else {
|
||||
continue;
|
||||
};
|
||||
if body_meta.len() != file.body_bytes as u64
|
||||
|| file.body_sha256.len() != 64
|
||||
|| !file.body_sha256.bytes().all(|b| b.is_ascii_hexdigit())
|
||||
{
|
||||
let Some(body_sha256) = parse_key_hex(&file.body_sha256) else {
|
||||
continue;
|
||||
};
|
||||
if body_meta.len() != file.body_bytes as u64 {
|
||||
continue;
|
||||
}
|
||||
let origin_etag = file
|
||||
@@ -138,8 +138,7 @@ impl Cache {
|
||||
let _ = std::fs::remove_file(&body_path);
|
||||
continue;
|
||||
}
|
||||
loaded.push(ColdEntry {
|
||||
key,
|
||||
loaded.push((key, ColdEntry {
|
||||
base_key,
|
||||
vary,
|
||||
revision,
|
||||
@@ -163,11 +162,10 @@ impl Cache {
|
||||
} else {
|
||||
system_time_ms(file.created_at_ms)
|
||||
},
|
||||
headers,
|
||||
body_path,
|
||||
meta_path: path,
|
||||
body_bytes: file.body_bytes,
|
||||
body_sha256: file.body_sha256,
|
||||
body_sha256,
|
||||
disk_bytes: body_meta.len().saturating_add(bytes.len() as u64),
|
||||
stored_at,
|
||||
last_access: if file.last_access_ms == 0 {
|
||||
@@ -182,8 +180,7 @@ impl Cache {
|
||||
stale_if_error: file.stale_if_error,
|
||||
must_revalidate: file.must_revalidate,
|
||||
proxy_revalidate: file.proxy_revalidate,
|
||||
origin_etag,
|
||||
origin_last_modified,
|
||||
has_validator: origin_etag.is_some() || origin_last_modified.is_some(),
|
||||
hits: file.hits,
|
||||
transformed: file.transformed,
|
||||
sequence: 0,
|
||||
@@ -191,12 +188,12 @@ impl Cache {
|
||||
skip_compression: file.skip_compression,
|
||||
requires_revalidation: file.requires_revalidation,
|
||||
pinned_reads: 0,
|
||||
});
|
||||
}));
|
||||
}
|
||||
}
|
||||
loaded.sort_by_key(|entry| entry.last_access);
|
||||
loaded.sort_by_key(|(_, entry)| entry.last_access);
|
||||
let mut cold = self.cold.lock().unwrap_or_else(|e| e.into_inner());
|
||||
for mut entry in loaded {
|
||||
for (key, mut entry) in loaded {
|
||||
for key in incompatible_cold_variants(&cold, &entry.base_key, &entry.vary) {
|
||||
if let Some(old) = cold.remove(&key) {
|
||||
let _ = std::fs::remove_file(old.meta_path);
|
||||
@@ -218,9 +215,9 @@ impl Cache {
|
||||
entry.sequence = cold.next_sequence();
|
||||
entry.generation = entry.sequence;
|
||||
cold.bytes = cold.bytes.saturating_add(entry.disk_bytes);
|
||||
cold.lru.insert(entry.sequence, entry.key);
|
||||
cold.index_variant(entry.base_key, entry.key);
|
||||
cold.entries.insert(entry.key, entry);
|
||||
cold.lru.insert(entry.sequence, key);
|
||||
cold.index_variant(entry.base_key, key);
|
||||
cold.entries.insert(key, entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+6
-6
@@ -114,6 +114,7 @@ impl Cache {
|
||||
.saturating_add(draft.entry.received.elapsed())
|
||||
.as_secs();
|
||||
let resident_age_at_store = hot_resident_age(&draft.entry).as_secs();
|
||||
let body_sha256 = body_sha256_bytes(&draft.entry.body);
|
||||
let file = ColdFile {
|
||||
format: COLD_FORMAT_VERSION,
|
||||
key: key.clone(),
|
||||
@@ -137,7 +138,7 @@ impl Cache {
|
||||
headers,
|
||||
body_file: body_name,
|
||||
body_bytes: draft.entry.body.len(),
|
||||
body_sha256: body_sha256(&draft.entry.body),
|
||||
body_sha256: key_hex(&body_sha256),
|
||||
stored_at_ms: now_ms(),
|
||||
age_at_store_seconds: age_at_store,
|
||||
resident_age_at_store_seconds: resident_age_at_store,
|
||||
@@ -236,8 +237,9 @@ impl Cache {
|
||||
delete.extend([meta_path.clone(), body_path.clone()]);
|
||||
} else {
|
||||
let sequence = cold.next_sequence();
|
||||
let has_validator =
|
||||
draft.entry.origin_etag.is_some() || draft.entry.origin_last_modified.is_some();
|
||||
let entry = ColdEntry {
|
||||
key: draft.key,
|
||||
base_key: draft.entry.base_key,
|
||||
vary: draft.entry.vary,
|
||||
revision: draft.revision,
|
||||
@@ -249,11 +251,10 @@ impl Cache {
|
||||
target: draft.entry.target,
|
||||
tags: draft.entry.tags,
|
||||
created_at: draft.entry.created_at,
|
||||
headers: draft.entry.headers,
|
||||
body_path: body_path.clone(),
|
||||
meta_path: meta_path.clone(),
|
||||
body_bytes: draft.entry.body.len(),
|
||||
body_sha256: file.body_sha256,
|
||||
body_sha256,
|
||||
disk_bytes,
|
||||
stored_at: SystemTime::now(),
|
||||
age_at_store_seconds: age_at_store,
|
||||
@@ -263,8 +264,7 @@ impl Cache {
|
||||
stale_if_error: draft.entry.stale_if_error,
|
||||
must_revalidate: draft.entry.must_revalidate,
|
||||
proxy_revalidate: draft.entry.proxy_revalidate,
|
||||
origin_etag: draft.entry.origin_etag,
|
||||
origin_last_modified: draft.entry.origin_last_modified,
|
||||
has_validator,
|
||||
last_access: SystemTime::now(),
|
||||
hits: draft.entry.hits,
|
||||
transformed: draft.entry.transformed,
|
||||
|
||||
Vendored
+14
-7
@@ -150,6 +150,13 @@ impl Cache {
|
||||
key: storage_key,
|
||||
generation: entry.generation,
|
||||
};
|
||||
let Some(metadata) = read_cold_metadata(&storage_key, &entry).await else {
|
||||
self.counters.cold_errors.fetch_add(1, Ordering::Relaxed);
|
||||
if let Some(entry) = self.take_cold(&storage_key) {
|
||||
self.delete_cold_entry(entry);
|
||||
}
|
||||
return None;
|
||||
};
|
||||
if entry.body_bytes > settings.max_object_bytes {
|
||||
self.counters
|
||||
.memory_pressure_rejections
|
||||
@@ -203,7 +210,7 @@ impl Cache {
|
||||
let hits = entry.hits.saturating_add(1);
|
||||
let cost = hot_entry_cost(
|
||||
body_allocated,
|
||||
&entry.headers,
|
||||
&metadata.headers,
|
||||
&entry.vary,
|
||||
&entry.scope,
|
||||
&entry.scheme,
|
||||
@@ -212,8 +219,8 @@ impl Cache {
|
||||
&entry.path,
|
||||
&entry.target,
|
||||
&entry.tags,
|
||||
entry.origin_etag.as_ref(),
|
||||
entry.origin_last_modified.as_ref(),
|
||||
metadata.origin_etag.as_ref(),
|
||||
metadata.origin_last_modified.as_ref(),
|
||||
);
|
||||
let resident_age_seconds = cold_resident_age(&entry);
|
||||
let hot_entry = Entry {
|
||||
@@ -227,7 +234,7 @@ impl Cache {
|
||||
target: entry.target.clone(),
|
||||
tags: entry.tags.clone(),
|
||||
created_at: entry.created_at,
|
||||
headers: entry.headers.clone(),
|
||||
headers: metadata.headers.clone(),
|
||||
body: body.clone(),
|
||||
body_allocated,
|
||||
received: Instant::now(),
|
||||
@@ -240,8 +247,8 @@ impl Cache {
|
||||
stale_if_error: entry.stale_if_error,
|
||||
must_revalidate: entry.must_revalidate,
|
||||
proxy_revalidate: entry.proxy_revalidate,
|
||||
origin_etag: entry.origin_etag.clone(),
|
||||
origin_last_modified: entry.origin_last_modified.clone(),
|
||||
origin_etag: metadata.origin_etag.clone(),
|
||||
origin_last_modified: metadata.origin_last_modified.clone(),
|
||||
cost,
|
||||
sequence: 0,
|
||||
hits,
|
||||
@@ -253,7 +260,7 @@ impl Cache {
|
||||
return None;
|
||||
}
|
||||
let mut response = Response::new(Body::from(body.clone()));
|
||||
*response.headers_mut() = entry.headers;
|
||||
*response.headers_mut() = metadata.headers;
|
||||
if entry.transformed {
|
||||
response
|
||||
.extensions_mut()
|
||||
|
||||
Vendored
+1
@@ -2,4 +2,5 @@ include!("model/settings.rs");
|
||||
include!("model/entry.rs");
|
||||
include!("model/cache.rs");
|
||||
include!("model/maintenance.rs");
|
||||
include!("model/cold_metadata.rs");
|
||||
include!("model/views.rs");
|
||||
Vendored
+2
-5
@@ -246,7 +246,6 @@ struct ColdFile {
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ColdEntry {
|
||||
key: Key,
|
||||
base_key: Key,
|
||||
vary: Vec<String>,
|
||||
revision: u64,
|
||||
@@ -258,11 +257,10 @@ struct ColdEntry {
|
||||
target: String,
|
||||
tags: Vec<String>,
|
||||
created_at: SystemTime,
|
||||
headers: HeaderMap,
|
||||
body_path: PathBuf,
|
||||
meta_path: PathBuf,
|
||||
body_bytes: usize,
|
||||
body_sha256: String,
|
||||
body_sha256: Key,
|
||||
disk_bytes: u64,
|
||||
stored_at: SystemTime,
|
||||
last_access: SystemTime,
|
||||
@@ -273,8 +271,7 @@ struct ColdEntry {
|
||||
stale_if_error: u64,
|
||||
must_revalidate: bool,
|
||||
proxy_revalidate: bool,
|
||||
origin_etag: Option<HeaderValue>,
|
||||
origin_last_modified: Option<HeaderValue>,
|
||||
has_validator: bool,
|
||||
hits: u64,
|
||||
transformed: bool,
|
||||
sequence: u64,
|
||||
|
||||
Vendored
+69
@@ -0,0 +1,69 @@
|
||||
struct ColdMetadata {
|
||||
headers: HeaderMap,
|
||||
origin_etag: Option<HeaderValue>,
|
||||
origin_last_modified: Option<HeaderValue>,
|
||||
}
|
||||
|
||||
async fn read_cold_metadata(storage_key: &Key, entry: &ColdEntry) -> Option<ColdMetadata> {
|
||||
let bytes = tokio::fs::read(&entry.meta_path).await.ok()?;
|
||||
let file: ColdFile = serde_json::from_slice(&bytes).ok()?;
|
||||
let key = parse_key_hex(&file.key)?;
|
||||
let base_key = parse_key_hex(&file.base_key)?;
|
||||
let body_sha256 = parse_key_hex(&file.body_sha256)?;
|
||||
let mut vary = file.vary.clone();
|
||||
vary.sort();
|
||||
vary.dedup();
|
||||
let authority = if file.authority.is_empty() {
|
||||
file.domain.as_str()
|
||||
} else {
|
||||
file.authority.as_str()
|
||||
};
|
||||
let target = if file.target.is_empty() {
|
||||
file.path.as_str()
|
||||
} else {
|
||||
file.target.as_str()
|
||||
};
|
||||
if file.format != COLD_FORMAT_VERSION
|
||||
|| key != *storage_key
|
||||
|| base_key != entry.base_key
|
||||
|| vary != entry.vary
|
||||
|| file.revision != entry.revision
|
||||
|| file.scope != entry.scope
|
||||
|| file.scheme != entry.scheme
|
||||
|| authority != entry.authority
|
||||
|| file.domain != entry.domain
|
||||
|| file.path != entry.path
|
||||
|| target != entry.target
|
||||
|| file.tags != entry.tags
|
||||
|| file.body_bytes != entry.body_bytes
|
||||
|| body_sha256 != entry.body_sha256
|
||||
|| file.ttl != entry.ttl
|
||||
|| file.stale_while_revalidate != entry.stale_while_revalidate
|
||||
|| file.stale_if_error != entry.stale_if_error
|
||||
|| file.must_revalidate != entry.must_revalidate
|
||||
|| file.proxy_revalidate != entry.proxy_revalidate
|
||||
|| file.transformed != entry.transformed
|
||||
|| file.skip_compression != entry.skip_compression
|
||||
|| file.requires_revalidation != entry.requires_revalidation
|
||||
|| entry.body_path.file_name().and_then(|name| name.to_str()) != Some(file.body_file.as_str())
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let headers = pairs_to_headers(&file.headers)?;
|
||||
let origin_etag = file
|
||||
.origin_etag
|
||||
.as_deref()
|
||||
.and_then(|value| HeaderValue::from_str(value).ok());
|
||||
let origin_last_modified = file
|
||||
.origin_last_modified
|
||||
.as_deref()
|
||||
.and_then(|value| HeaderValue::from_str(value).ok());
|
||||
if entry.has_validator != (origin_etag.is_some() || origin_last_modified.is_some()) {
|
||||
return None;
|
||||
}
|
||||
Some(ColdMetadata {
|
||||
headers,
|
||||
origin_etag,
|
||||
origin_last_modified,
|
||||
})
|
||||
}
|
||||
Vendored
+4
-7
@@ -404,18 +404,15 @@ fn incompatible_cold_variants(store: &ColdStore, base_key: &Key, vary: &[String]
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn body_sha256(body: &[u8]) -> String {
|
||||
fn body_sha256_bytes(body: &[u8]) -> Key {
|
||||
let digest = Sha256::digest(body);
|
||||
let mut out = String::with_capacity(64);
|
||||
for byte in digest {
|
||||
use std::fmt::Write as _;
|
||||
let _ = write!(&mut out, "{byte:02x}");
|
||||
}
|
||||
let mut out = [0u8; 32];
|
||||
out.copy_from_slice(&digest);
|
||||
out
|
||||
}
|
||||
|
||||
fn cold_body_matches(entry: &ColdEntry, body: &[u8]) -> bool {
|
||||
body.len() == entry.body_bytes && body_sha256(body).as_str() == entry.body_sha256.as_str()
|
||||
body.len() == entry.body_bytes && body_sha256_bytes(body) == entry.body_sha256
|
||||
}
|
||||
|
||||
fn key_hex(key: &Key) -> String {
|
||||
|
||||
+2
-5
@@ -256,7 +256,6 @@ fn old_cold_read_guard_does_not_unpin_a_replacement_with_the_same_key() {
|
||||
let cache = Cache::new(1);
|
||||
let key = [7_u8; 32];
|
||||
let template = ColdEntry {
|
||||
key,
|
||||
base_key: key,
|
||||
vary: Vec::new(),
|
||||
revision: 1,
|
||||
@@ -268,11 +267,10 @@ fn old_cold_read_guard_does_not_unpin_a_replacement_with_the_same_key() {
|
||||
target: "https://example.test/asset".to_owned(),
|
||||
tags: Vec::new(),
|
||||
created_at: SystemTime::now(),
|
||||
headers: HeaderMap::new(),
|
||||
body_path: PathBuf::from("old.body"),
|
||||
meta_path: PathBuf::from("old.json"),
|
||||
body_bytes: 1,
|
||||
body_sha256: "00".repeat(32),
|
||||
body_sha256: [0u8; 32],
|
||||
disk_bytes: 2,
|
||||
stored_at: SystemTime::now(),
|
||||
last_access: SystemTime::now(),
|
||||
@@ -283,8 +281,7 @@ fn old_cold_read_guard_does_not_unpin_a_replacement_with_the_same_key() {
|
||||
stale_if_error: 0,
|
||||
must_revalidate: false,
|
||||
proxy_revalidate: false,
|
||||
origin_etag: None,
|
||||
origin_last_modified: None,
|
||||
has_validator: false,
|
||||
hits: 0,
|
||||
transformed: false,
|
||||
sequence: 1,
|
||||
|
||||
Vendored
+11
-4
@@ -158,7 +158,7 @@ impl Cache {
|
||||
entry.ttl,
|
||||
entry.stale_while_revalidate,
|
||||
entry.stale_if_error,
|
||||
entry.origin_etag.is_some() || entry.origin_last_modified.is_some(),
|
||||
entry.has_validator,
|
||||
)
|
||||
{
|
||||
return None;
|
||||
@@ -179,6 +179,13 @@ impl Cache {
|
||||
key: storage_key,
|
||||
generation: entry.generation,
|
||||
};
|
||||
let Some(metadata) = read_cold_metadata(&storage_key, &entry).await else {
|
||||
self.counters.cold_errors.fetch_add(1, Ordering::Relaxed);
|
||||
if let Some(entry) = self.take_cold(&storage_key) {
|
||||
self.delete_cold_entry(entry);
|
||||
}
|
||||
return None;
|
||||
};
|
||||
let settings = self
|
||||
.tier_settings
|
||||
.lock()
|
||||
@@ -219,7 +226,7 @@ impl Cache {
|
||||
let age = cold_age(&entry);
|
||||
let resident_age_at_lookup = Duration::from_secs(cold_resident_age(&entry));
|
||||
Some(Revalidation {
|
||||
headers: entry.headers,
|
||||
headers: metadata.headers,
|
||||
body,
|
||||
body_allocated,
|
||||
_transient_leases: extra_lease
|
||||
@@ -237,8 +244,8 @@ impl Cache {
|
||||
stale_if_error: entry.stale_if_error,
|
||||
must_revalidate: entry.must_revalidate,
|
||||
proxy_revalidate: entry.proxy_revalidate,
|
||||
origin_etag: entry.origin_etag,
|
||||
origin_last_modified: entry.origin_last_modified,
|
||||
origin_etag: metadata.origin_etag,
|
||||
origin_last_modified: metadata.origin_last_modified,
|
||||
})
|
||||
}
|
||||
pub async fn revalidation_tiered(&self, ticket: &Ticket) -> Option<Revalidation> {
|
||||
|
||||
@@ -41,7 +41,7 @@ use zeroize::Zeroizing;
|
||||
#[command(
|
||||
name = "rust-proxy",
|
||||
version,
|
||||
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.27.2"
|
||||
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.27.3"
|
||||
)]
|
||||
struct Cli {
|
||||
#[arg(
|
||||
|
||||
+1
-1
@@ -239,7 +239,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
|
||||
}
|
||||
});
|
||||
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
|
||||
println!("RustProxy live logs 0.27.2 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
println!("RustProxy live logs 0.27.3 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||
if interactive { print!("> "); io::stdout().flush()?; }
|
||||
let mut filters = LiveLogFilters::default();
|
||||
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
|
||||
|
||||
@@ -162,7 +162,10 @@ let policy_reason = if admin_control_plane {
|
||||
None
|
||||
};
|
||||
let cache_enabled = policy_reason.is_none();
|
||||
let body_empty = if cache_enabled && handshake.is_none() && request.method() == Method::GET {
|
||||
let body_empty = if cache_enabled
|
||||
&& handshake.is_none()
|
||||
&& (request.method() == Method::GET || request.method() == Method::HEAD)
|
||||
{
|
||||
match empty_for_cache(&mut request).await {
|
||||
Ok(empty) => empty,
|
||||
Err(_) => {
|
||||
|
||||
+75
-23
@@ -209,32 +209,76 @@ struct Limit {
|
||||
window_seconds: u64,
|
||||
burst: u32,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
struct Key {
|
||||
revision: u64,
|
||||
vhost: String,
|
||||
client: IpAddr,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Bucket {
|
||||
started: Instant,
|
||||
count: u32,
|
||||
window_seconds: u64,
|
||||
}
|
||||
#[derive(Debug, Default)]
|
||||
struct LimiterState {
|
||||
revision: Option<u64>,
|
||||
// Store the vhost String once per vhost instead of once per client IP.
|
||||
buckets: HashMap<String, HashMap<IpAddr, Bucket>>,
|
||||
bucket_count: usize,
|
||||
last_sweep: Option<Instant>,
|
||||
}
|
||||
impl LimiterState {
|
||||
fn clear(&mut self) {
|
||||
self.buckets.clear();
|
||||
self.bucket_count = 0;
|
||||
self.last_sweep = None;
|
||||
}
|
||||
fn sweep_expired(&mut self, now: Instant, force: bool) {
|
||||
if !force
|
||||
&& self
|
||||
.last_sweep
|
||||
.is_some_and(|last| now.duration_since(last) < Duration::from_secs(5))
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.last_sweep = Some(now);
|
||||
let mut removed = 0usize;
|
||||
self.buckets.retain(|_, clients| {
|
||||
let before = clients.len();
|
||||
clients.retain(|_, bucket| {
|
||||
now.duration_since(bucket.started) < Duration::from_secs(bucket.window_seconds)
|
||||
});
|
||||
let removed_here = before.saturating_sub(clients.len());
|
||||
removed = removed.saturating_add(removed_here);
|
||||
if removed_here > 0
|
||||
&& !clients.is_empty()
|
||||
&& clients.capacity() > clients.len().saturating_mul(2).max(64)
|
||||
{
|
||||
clients.shrink_to_fit();
|
||||
}
|
||||
!clients.is_empty()
|
||||
});
|
||||
self.bucket_count = self.bucket_count.saturating_sub(removed);
|
||||
if removed > 0
|
||||
&& self.buckets.capacity() > self.buckets.len().saturating_mul(2).max(16)
|
||||
{
|
||||
self.buckets.shrink_to_fit();
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Limited {
|
||||
pub retry_after_seconds: u64,
|
||||
}
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Limiter {
|
||||
buckets: Mutex<HashMap<Key, Bucket>>,
|
||||
state: Mutex<LimiterState>,
|
||||
}
|
||||
impl Limiter {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
pub fn reset(&self) {
|
||||
lock(&self.buckets).clear();
|
||||
lock(&self.state).clear();
|
||||
}
|
||||
pub fn maintenance(&self) {
|
||||
lock(&self.state).sweep_expired(Instant::now(), true);
|
||||
}
|
||||
pub fn check(
|
||||
&self,
|
||||
@@ -247,26 +291,34 @@ impl Limiter {
|
||||
let Some(limit) = settings.effective(country) else {
|
||||
return Ok(());
|
||||
};
|
||||
let key = Key {
|
||||
revision,
|
||||
vhost: vhost.to_owned(),
|
||||
client,
|
||||
};
|
||||
let mut buckets = lock(&self.buckets);
|
||||
if !buckets.contains_key(&key) && buckets.len() >= MAX_BUCKETS {
|
||||
buckets.retain(|key, bucket| {
|
||||
key.revision == revision
|
||||
&& bucket.started.elapsed()
|
||||
< Duration::from_secs(bucket.window_seconds.saturating_mul(2).max(2))
|
||||
});
|
||||
if buckets.len() >= MAX_BUCKETS {
|
||||
let now = Instant::now();
|
||||
let mut state = lock(&self.state);
|
||||
if state.revision != Some(revision) {
|
||||
state.clear();
|
||||
state.revision = Some(revision);
|
||||
}
|
||||
state.sweep_expired(now, false);
|
||||
let new_client = !state
|
||||
.buckets
|
||||
.get(vhost)
|
||||
.is_some_and(|clients| clients.contains_key(&client));
|
||||
if new_client && state.bucket_count >= MAX_BUCKETS {
|
||||
state.sweep_expired(now, true);
|
||||
if state.bucket_count >= MAX_BUCKETS {
|
||||
return Err(Limited {
|
||||
retry_after_seconds: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
let now = Instant::now();
|
||||
let bucket = buckets.entry(key).or_insert(Bucket {
|
||||
if new_client {
|
||||
state.bucket_count = state.bucket_count.saturating_add(1);
|
||||
}
|
||||
let bucket = state
|
||||
.buckets
|
||||
.entry(vhost.to_owned())
|
||||
.or_default()
|
||||
.entry(client)
|
||||
.or_insert(Bucket {
|
||||
started: now,
|
||||
count: 0,
|
||||
window_seconds: limit.window_seconds,
|
||||
|
||||
@@ -33,4 +33,80 @@ mod tests {
|
||||
assert!(limiter.check(1, "site", ip, Some("PL"), &settings).is_ok());
|
||||
assert!(limiter.check(1, "site", ip, Some("PL"), &settings).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expired_client_buckets_are_reclaimed_without_waiting_for_capacity_pressure() {
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
requests: 10,
|
||||
window_seconds: 1,
|
||||
burst: 0,
|
||||
..Default::default()
|
||||
};
|
||||
let limiter = Limiter::new();
|
||||
let first = "192.0.2.1".parse().unwrap();
|
||||
let second = "192.0.2.2".parse().unwrap();
|
||||
assert!(limiter.check(7, "site", first, None, &settings).is_ok());
|
||||
{
|
||||
let mut state = lock(&limiter.state);
|
||||
let bucket = state.buckets.get_mut("site").unwrap().get_mut(&first).unwrap();
|
||||
bucket.started = Instant::now()
|
||||
.checked_sub(Duration::from_secs(2))
|
||||
.unwrap();
|
||||
state.last_sweep = None;
|
||||
}
|
||||
assert!(limiter.check(7, "site", second, None, &settings).is_ok());
|
||||
let state = lock(&limiter.state);
|
||||
assert_eq!(state.bucket_count, 1);
|
||||
assert!(!state.buckets.get("site").unwrap().contains_key(&first));
|
||||
assert!(state.buckets.get("site").unwrap().contains_key(&second));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn maintenance_reclaims_expired_clients_without_new_traffic() {
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
requests: 10,
|
||||
window_seconds: 1,
|
||||
burst: 0,
|
||||
..Default::default()
|
||||
};
|
||||
let limiter = Limiter::new();
|
||||
let ip = "192.0.2.10".parse().unwrap();
|
||||
assert!(limiter.check(9, "idle-site", ip, None, &settings).is_ok());
|
||||
{
|
||||
let mut state = lock(&limiter.state);
|
||||
let bucket = state
|
||||
.buckets
|
||||
.get_mut("idle-site")
|
||||
.unwrap()
|
||||
.get_mut(&ip)
|
||||
.unwrap();
|
||||
bucket.started = Instant::now()
|
||||
.checked_sub(Duration::from_secs(2))
|
||||
.unwrap();
|
||||
}
|
||||
limiter.maintenance();
|
||||
let state = lock(&limiter.state);
|
||||
assert_eq!(state.bucket_count, 0);
|
||||
assert!(state.buckets.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vhost_name_is_stored_once_for_multiple_clients() {
|
||||
let settings = Settings {
|
||||
enabled: true,
|
||||
requests: 10,
|
||||
..Default::default()
|
||||
};
|
||||
let limiter = Limiter::new();
|
||||
for ip in ["192.0.2.1", "192.0.2.2"] {
|
||||
assert!(limiter
|
||||
.check(1, "shared-vhost", ip.parse().unwrap(), None, &settings)
|
||||
.is_ok());
|
||||
}
|
||||
let state = lock(&limiter.state);
|
||||
assert_eq!(state.buckets.len(), 1);
|
||||
assert_eq!(state.bucket_count, 2);
|
||||
}
|
||||
}
|
||||
@@ -212,6 +212,7 @@ impl App {
|
||||
// cache fill paths. glibc may retain already-free arenas as RSS. Trim
|
||||
// them periodically without evicting live cache objects or connections.
|
||||
let trim_shutdown = app.shutdown.clone();
|
||||
let trim_app = Arc::downgrade(&app);
|
||||
app.tunnels.spawn(async move {
|
||||
let mut tick = tokio::time::interval(Duration::from_secs(60));
|
||||
tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
|
||||
@@ -220,6 +221,9 @@ impl App {
|
||||
tokio::select! {
|
||||
_ = trim_shutdown.cancelled() => break,
|
||||
_ = tick.tick() => {
|
||||
if let Some(app) = trim_app.upgrade() {
|
||||
app.rate_limiter.maintenance();
|
||||
}
|
||||
let _ = tokio::task::spawn_blocking(crate::diagnostics::trim_allocator).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ pub(super) fn fastcgi_params(
|
||||
if context.tls { "on" } else { "off" }.into(),
|
||||
);
|
||||
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
|
||||
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.27.2".into());
|
||||
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.27.3".into());
|
||||
params.insert("REDIRECT_STATUS".into(), "200".into());
|
||||
if let Some(value) = headers
|
||||
.get(CONTENT_TYPE)
|
||||
|
||||
@@ -185,7 +185,7 @@ pub(super) fn directory_listing(path: &str, entries: &[DirectoryEntry]) -> Vec<u
|
||||
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
|
||||
));
|
||||
}
|
||||
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.27.2</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
|
||||
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.27.3</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
|
||||
html.into_bytes()
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ async fn empty_index_can_go_directly_to_directory_listing() {
|
||||
let body = std::str::from_utf8(&body).unwrap();
|
||||
assert!(body.contains("file.txt"));
|
||||
assert!(body.contains("DIRECTORY LISTING"));
|
||||
assert!(body.contains("RustProxy <b>0.27.2</b>"));
|
||||
assert!(body.contains("RustProxy <b>0.27.3</b>"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Developer and operator tools
|
||||
|
||||
These files are not required by the running RustProxy proxy process.
|
||||
|
||||
- `checks/` - compiler-independent source/release/documentation contracts and UI checks.
|
||||
- `dev/` - local mock services and diagnostic traffic/compression helpers.
|
||||
- `audit/` - explicit operator audit/probe tools.
|
||||
- `legacy/` - retained compatibility/release-test helpers; not the supported production workflow.
|
||||
|
||||
Production packages do not need this tree unless a packaging/test workflow intentionally ships it.
|
||||
@@ -57,7 +57,7 @@ def main():
|
||||
check('/healthz',200,lambda b,h:json.loads(b)=={'status':'ok'})
|
||||
check('/api/auth/status',200,lambda b,h:isinstance((v:=json.loads(b)).get('setup_required'),bool) and set(v)=={'setup_required'})
|
||||
check('/',200,lambda b,h:b'/assets/login.js' in b and all(x not in b for x in [b'/assets/app.js',b'config-editor',b'loginLead']))
|
||||
check('/assets/login-langs.json',200,lambda b,h:all(set(p['translations'])<=set(json.loads((Path(__file__).resolve().parents[1]/'src/admin/web/login-keys.json').read_text())) for p in json.loads(b)['languages']))
|
||||
check('/assets/login-langs.json',200,lambda b,h:all(set(p['translations'])<=set(json.loads((Path(__file__).resolve().parents[2]/'src/admin/web/login-keys.json').read_text())) for p in json.loads(b)['languages']))
|
||||
print(f'{count-failed}/{count} anonymous probes passed. No authenticated/mutation/TLS checks performed.')
|
||||
raise SystemExit(bool(failed))
|
||||
if __name__=='__main__':main()
|
||||
@@ -11,20 +11,20 @@ from preflight import check as preflight
|
||||
from rust_source import rust_source
|
||||
from web_assets import read_app_js, read_vhosts_js
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
|
||||
def read_cache_source() -> str:
|
||||
return rust_source(root, "src/cache/mod.rs")
|
||||
|
||||
def read_deploy_source() -> str:
|
||||
return "\n".join((root / name).read_text(encoding="utf-8") for name in ("scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/deploy.py"))
|
||||
return "\n".join((root / name).read_text(encoding="utf-8") for name in ("scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/deploy/deploy.py"))
|
||||
|
||||
def read_docker_agent_source() -> str:
|
||||
return "\n".join(path.read_text(encoding="utf-8") for path in sorted((root / "docker-agent").glob("*.go")) if not path.name.endswith("_test.go"))
|
||||
|
||||
preflight(root)
|
||||
manifest = tomllib.loads((root / "Cargo.toml").read_text())
|
||||
assert manifest["package"]["version"] == "0.27.2"
|
||||
assert manifest["package"]["version"] == "0.27.3"
|
||||
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
||||
h3dep = manifest["dependencies"]["h3"]
|
||||
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
|
||||
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
|
||||
lock_path = root / "Cargo.lock"
|
||||
if lock_path.exists():
|
||||
lock = lock_path.read_text()
|
||||
assert 'name = "rust-proxy"\nversion = "0.27.2"' in lock
|
||||
assert 'name = "rust-proxy"\nversion = "0.27.3"' in lock
|
||||
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
|
||||
for release_doc, marker in {
|
||||
"README.md": "# RustProxy 0.27.2",
|
||||
"CHANGELOG.md": "## 0.27.2",
|
||||
"ROADMAP.md": "## 0.27.2",
|
||||
"AGENTS.md": "Current source version: 0.27.2.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.27.2",
|
||||
"README.md": "# RustProxy 0.27.3",
|
||||
"CHANGELOG.md": "## 0.27.3",
|
||||
"ROADMAP.md": "## 0.27.3",
|
||||
"AGENTS.md": "Current source version: 0.27.3.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.27.3",
|
||||
}.items():
|
||||
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
|
||||
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
|
||||
@@ -73,12 +73,12 @@ for marker in [
|
||||
]:
|
||||
assert marker in update_check_source, marker
|
||||
assert 'pub last_error:' not in update_check_source, "update-check errors must not change dashboard/API state"
|
||||
for name in ["README.md", "ROADMAP.md", "AGENTS.md", "CHANGELOG.md", "UPDATE_CHECK.toml", "Dockerfile", ".dockerignore", "docs/TEST_REPORT.md", "docs/V0.24.8_AUDIT.md", "docs/V0.24.7_AUDIT.md", "docs/V0.24.6_AUDIT.md", "docs/V0.24.5_AUDIT.md", "docs/V0.24.1_AUDIT.md", "docs/V0.24.0_AUDIT.md", "docs/V0.23.2_AUDIT.md", "docs/V0.23.1_AUDIT.md", "docs/V0.22.3_AUDIT.md", "docs/LIVE_LOGS.md", "docs/README.md", "docs/API.md", "docs/ARCHITECTURE.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/VHOSTS.md", "docs/STREAMS.md", "docs/REDIRECTS.md", "docs/CONTAINER.md", "docs/NPM_MIGRATION.md", "docs/DOCKER_DISCOVERY.md", "docker-agent/main.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/docker-agent.env.example", "docker-agent/install.sh", "src/docker.rs", "src/admin/web/docker.js", "scripts/migrate_npm.py", "docs/CUSTOM_LOCATIONS.md", "nagios/README.md", "nagios/check_rustproxy.py", "nagios/check_rustproxy_api", "nagios/check_rustproxy_backends", "nagios/check_rustproxy_latency", "nagios/check_rustproxy_queues", "src/admin/web/error_401.html", "src/admin/web/error_404.html", "src/admin/web/streams.js", "src/admin/web/redirects.js", "src/admin/web/host404.js", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json", "grafana/README.md"]:
|
||||
for name in ["README.md", "ROADMAP.md", "AGENTS.md", "CHANGELOG.md", "UPDATE_CHECK.toml", "Dockerfile", ".dockerignore", "docs/TEST_REPORT.md", "docs/V0.24.8_AUDIT.md", "docs/V0.24.7_AUDIT.md", "docs/V0.24.6_AUDIT.md", "docs/V0.24.5_AUDIT.md", "docs/V0.24.1_AUDIT.md", "docs/V0.24.0_AUDIT.md", "docs/V0.23.2_AUDIT.md", "docs/V0.23.1_AUDIT.md", "docs/V0.22.3_AUDIT.md", "docs/LIVE_LOGS.md", "docs/README.md", "docs/API.md", "docs/ARCHITECTURE.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/VHOSTS.md", "docs/STREAMS.md", "docs/REDIRECTS.md", "docs/CONTAINER.md", "docs/NPM_MIGRATION.md", "docs/DOCKER_DISCOVERY.md", "docker-agent/main.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/docker-agent.env.example", "docker-agent/install.sh", "src/docker.rs", "src/admin/web/docker.js", "scripts/migration/migrate_npm.py", "docs/CUSTOM_LOCATIONS.md", "nagios/README.md", "nagios/check_rustproxy.py", "nagios/check_rustproxy_api", "nagios/check_rustproxy_backends", "nagios/check_rustproxy_latency", "nagios/check_rustproxy_queues", "src/admin/web/error_401.html", "src/admin/web/error_404.html", "src/admin/web/streams.js", "src/admin/web/redirects.js", "src/admin/web/host404.js", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json", "grafana/README.md"]:
|
||||
assert (root / name).is_file(), name
|
||||
npm_doc = (root / "docs/NPM_MIGRATION.md").read_text(encoding="utf-8")
|
||||
for marker in ["scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", '-v "$PWD/scripts:/rustproxy-scripts:ro"', "python /rustproxy-scripts/migrate_npm.py"]:
|
||||
for marker in ["scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", '-v "$PWD/scripts/migration:/rustproxy-migration:ro"', "python /rustproxy-migration/migrate_npm.py"]:
|
||||
assert marker in npm_doc, marker
|
||||
for module in ["scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py"]:
|
||||
for module in ["scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py"]:
|
||||
assert (root / module).is_file(), module
|
||||
for path in (root / "examples").glob("*.json"):
|
||||
data = json.loads(path.read_text())
|
||||
@@ -100,7 +100,8 @@ ids = re.findall(r'id="([^"]+)"', html)
|
||||
assert len(ids) == len(set(ids)), "duplicate HTML IDs"
|
||||
for identifier in set(re.findall(r'\$\("([^"]+)"\)', js)):
|
||||
assert identifier in ids, f"unknown frontend ID: {identifier}"
|
||||
for path in [root / "make_zip.py", *(root / "scripts").glob("*.py")]:
|
||||
python_sources = [root / "make_zip.py", *(root / "scripts").rglob("*.py"), *(root / "tools").rglob("*.py")]
|
||||
for path in python_sources:
|
||||
ast.parse(path.read_text(), str(path))
|
||||
subprocess.run(["bash", "-n", str(root / "dev.sh")], check=True)
|
||||
subprocess.run(["bash", "-n", str(root / "upgrade.sh")], check=True)
|
||||
@@ -130,10 +131,10 @@ assert "LogsDirectory=rustproxy" in unit and "/var/log/rustproxy" in unit
|
||||
assert "Environment=MALLOC_ARENA_MAX=2" in unit
|
||||
assert "Environment=MALLOC_ARENA_MAX=2" in read_deploy_source()
|
||||
assert not re.search(r"\b20\d{2}[-/]\d{1,2}[-/]\d{1,2}\b", (root / "CHANGELOG.md").read_text()), "changelog contains a release date"
|
||||
subprocess.run(["python3", str(root / "scripts/langs_check.py")], check=True)
|
||||
subprocess.run(["python3", str(root / "scripts/web_server_contracts.py")], check=True)
|
||||
subprocess.run(["python3", str(root / "scripts/rust_module_contracts.py")], check=True)
|
||||
subprocess.run(["python3", str(root / "scripts/source_module_contracts.py")], check=True)
|
||||
subprocess.run(["python3", str(root / "tools/checks/langs_check.py")], check=True)
|
||||
subprocess.run(["python3", str(root / "tools/checks/web_server_contracts.py")], check=True)
|
||||
subprocess.run(["python3", str(root / "tools/checks/rust_module_contracts.py")], check=True)
|
||||
subprocess.run(["python3", str(root / "tools/checks/source_module_contracts.py")], check=True)
|
||||
dockerfile = (root / "Dockerfile").read_text()
|
||||
assert "cargo generate-lockfile" in dockerfile and "cargo build --locked --release" in dockerfile
|
||||
assert "USER rustproxy:rustproxy" in dockerfile
|
||||
@@ -287,7 +288,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
|
||||
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
|
||||
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
|
||||
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
|
||||
for marker in ['"version": "0.27.2"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
for marker in ['"version": "0.27.3"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
||||
assert marker in openapi, marker
|
||||
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
@@ -608,20 +609,21 @@ for marker in ['/api/certificate-api', 'certificate-api-form', 'certificate-api-
|
||||
assert marker in settings_js or marker in html, marker
|
||||
db_source = rust_source(root, "src/db.rs")
|
||||
acme_source = rust_source(root, "src/acme.rs")
|
||||
acme_runtime_source = (root / "src/acme/runtime_env.rs").read_text(encoding="utf-8")
|
||||
acme_runtime_source = (root / "src/acme/runtime_env.rs").read_text(encoding="utf-8") + (root / "src/acme/runtime_bootstrap.rs").read_text(encoding="utf-8")
|
||||
assert 'runtime_setup_once' in acme_source
|
||||
assert 'include!("acme/runtime_env.rs")' in (root / "src/acme.rs").read_text(encoding="utf-8")
|
||||
for marker in ['CERTBOT_PACKAGES', 'certbot==5.8.0', 'certbot-dns-cloudflare==5.8.0', 'certbot-dns-ovh==5.8.0', 'certbot-dns-rfc2136==5.8.0', 'join("acme-python")', 'virtualenv>=20,<21', 'system_has_pip', 'distro_certbot_available', 'acme.runtime_python_missing', 'acme.runtime_pip_missing']:
|
||||
assert 'include!("runtime_bootstrap.rs")' in (root / "src/acme/runtime_env.rs").read_text(encoding="utf-8")
|
||||
for marker in ['CERTBOT_PACKAGES', 'certbot==5.8.0', 'certbot-dns-cloudflare==5.8.0', 'certbot-dns-ovh==5.8.0', 'certbot-dns-rfc2136==5.8.0', 'join("acme-python")', 'virtualenv>=20,<21', 'system_has_pip', 'distro_certbot_available', 'acme.runtime_python_missing', 'acme.runtime_pip_missing', 'acme.runtime_venv_failed', 'acme.runtime_certbot_install_failed', '--break-system-packages']:
|
||||
assert marker in acme_runtime_source, marker
|
||||
for marker in ['"-I", "-m", "certbot", "--version"', 'RUSTPROXY_CERTBOT_PYTHON', 'managed_certbot_python']:
|
||||
for marker in ['venv_certbot', 'version.arg("--version")', 'RUSTPROXY_CERTBOT_EXECUTABLE', 'managed_certbot_executable']:
|
||||
assert marker in acme_runtime_source, marker
|
||||
main_source = rust_source(root, "src/main.rs")
|
||||
logging_source = rust_source(root, "src/logging.rs")
|
||||
secret_store_source = rust_source(root, "src/secret_store.rs")
|
||||
backup_source = rust_source(root, "src/backup.rs")
|
||||
config_source = rust_source(root, "src/config.rs")
|
||||
acme_worker_source = (root / "scripts/acme_provider.py").read_text()
|
||||
for marker in ['def certbot_prefix', 'RUSTPROXY_CERTBOT_PYTHON', '"-I", "-m", "certbot"', '[*command_prefix, "renew"', 'certbot_command(value, work, credential_file, command_prefix)']:
|
||||
acme_worker_source = (root / "scripts/runtime/acme_provider.py").read_text()
|
||||
for marker in ['def certbot_prefix', 'RUSTPROXY_CERTBOT_EXECUTABLE', 'return [managed_certbot]', '[*command_prefix, "renew"', 'certbot_command(value, work, credential_file, command_prefix)']:
|
||||
assert marker in acme_worker_source, marker
|
||||
for feature_marker in [
|
||||
"AES_256_GCM", "master.key", "O_NOFOLLOW", "migrate_acme_credentials",
|
||||
@@ -711,7 +713,7 @@ pages_rust = rust_source(root, 'src/pages.rs')
|
||||
assert 'validate_local_response' in pages_rust and '(200..=599).contains(&status)' in pages_rust and '204 | 205 | 304' in pages_rust
|
||||
assert 'store.load_certificate_renewals().await?' in main_source and 'state.acme.restore_renewals(renewal_observations)' in main_source
|
||||
assert 'renewals.json' not in main_source
|
||||
upgrade_source = (root / 'scripts/upgrade.py').read_text()
|
||||
upgrade_source = (root / 'scripts/deploy/upgrade.py').read_text()
|
||||
assert 'legacy = certs_dir / "renewals.json"' in upgrade_source and 'removed_obsolete_runtime_state' in upgrade_source and 'legacy-data' not in upgrade_source and 'certs_dir.is_symlink()' in upgrade_source
|
||||
assert '.managed-update' in main_source and 'flush_hot_to_cold_for_update' in main_source
|
||||
tls_source = rust_source(root, "src/tls.rs")
|
||||
@@ -820,8 +822,11 @@ assert any(variable.get('name') == 'traffic_vhost' for variable in grafana['temp
|
||||
assert any(panel.get('title') == 'Traffic analytics and Web Server' for panel in grafana['panels'])
|
||||
assert any(panel.get('title') == 'Traffic capture state' for panel in grafana['panels'])
|
||||
assert any(panel.get('title') == 'Top configured routes' for panel in grafana['panels'])
|
||||
# 0.27.2 compression/protocol/dashboard UX regressions.
|
||||
# 0.27.3 compression/protocol/dashboard UX regressions.
|
||||
compression_source = rust_source(root, "src/compression.rs")
|
||||
route_cache_source = rust_source(root, "src/proxy/forward/route_cache.rs")
|
||||
backup_source = rust_source(root, "src/backup.rs")
|
||||
rate_limit_source = rust_source(root, "src/rate_limit.rs")
|
||||
metrics_source = rust_source(root, "src/metrics.rs")
|
||||
admin_source = rust_source(root, "src/admin/mod.rs")
|
||||
style_source = (root / "src/admin/web/style.css").read_text()
|
||||
@@ -839,8 +844,18 @@ for marker in [
|
||||
"existing_content_encoding_is_never_recompressed",
|
||||
"no_transform_range_and_sse_are_not_compressed",
|
||||
"age_header_survives_downstream_compression",
|
||||
"CompressionLevel::Fastest",
|
||||
]:
|
||||
assert marker in compression_source, marker
|
||||
assert "request.method() == Method::GET || request.method() == Method::HEAD" in route_cache_source
|
||||
for marker in ['"acme-python"', '"acme_python"']:
|
||||
assert marker in backup_source, marker
|
||||
for marker in [
|
||||
"buckets: HashMap<String, HashMap<IpAddr, Bucket>>",
|
||||
"state.sweep_expired(now, false)",
|
||||
"state.sweep_expired(now, true)",
|
||||
]:
|
||||
assert marker in rate_limit_source, marker
|
||||
for forbidden in ["pub struct AllowCookie", "private_or_no_transform"]:
|
||||
assert forbidden not in compression_source, forbidden
|
||||
compression_finalize_source = rust_source(root, "src/proxy/upstream_helpers.rs")
|
||||
@@ -899,7 +914,7 @@ for marker in [
|
||||
assert '["host", "connection", "content-length"' in vhost_core_0267, "UI protected header set must stay intact"
|
||||
assert '["host", "age", "connection"' not in vhost_core_0267, "0.26.6 Age rules must remain UI-loadable"
|
||||
assert 'apply_response_age_rules(' in compression_finalize_source, "Age wire rules must run in final response stage"
|
||||
compression_tester_0267 = (root / "scripts/test_compression.py").read_text()
|
||||
compression_tester_0267 = (root / "tools/dev/test_compression.py").read_text()
|
||||
for marker in ["--revalidate", "if revalidate:", 'headers["Cache-Control"] = "no-cache"', "headers.get('Age', '-')"]:
|
||||
assert marker in compression_tester_0267, marker
|
||||
assert "cache_hit_and_dashboard_age_advance_from_storage_clock" in read_cache_source()
|
||||
@@ -1377,7 +1392,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
|
||||
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
|
||||
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
|
||||
assert 'location.pathname === "/logging"' in settings_js_0241
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.27.2"' in openapi
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.27.3"' in openapi
|
||||
|
||||
# 0.24.1 compiler and hostname-cardinality regressions.
|
||||
tls_0241 = rust_source(root, "src/tls.rs")
|
||||
@@ -1422,5 +1437,5 @@ for schema_name in ["RedirectHost", "NotFoundHost"]:
|
||||
node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"]
|
||||
assert "maxItems" not in node, schema_name
|
||||
|
||||
print("PASS: 0.27.2 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
|
||||
print("PASS: 0.27.3 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
|
||||
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.27.2 release."""
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.27.3 release."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
from rust_source import rust_source
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
DOCS = ROOT / "docs"
|
||||
|
||||
required = [
|
||||
@@ -14,6 +14,7 @@ required = [
|
||||
DOCS / "README.md",
|
||||
DOCS / "WEB_SERVER.md",
|
||||
DOCS / "TODO.md",
|
||||
DOCS / "V0.27.3_IMPLEMENTATION.md",
|
||||
DOCS / "V0.27.2_IMPLEMENTATION.md",
|
||||
DOCS / "V0.27.1_IMPLEMENTATION.md",
|
||||
DOCS / "V0.27.0_IMPLEMENTATION.md",
|
||||
@@ -59,16 +60,16 @@ assert '"/docs/content.json"' in admin
|
||||
readme = (ROOT / "README.md").read_text(encoding="utf-8")
|
||||
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
|
||||
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
|
||||
assert readme.startswith("# RustProxy 0.27.2")
|
||||
assert readme.startswith("# RustProxy 0.27.3")
|
||||
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
|
||||
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
|
||||
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
|
||||
assert roadmap.startswith("# RustProxy roadmap") and "## 0.27.2" in roadmap and "## 0.27.1" in roadmap and "## 0.27.0" in roadmap and "## 0.26.7" in roadmap and "## 0.26.6" in roadmap and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
|
||||
assert roadmap.startswith("# RustProxy roadmap") and "## 0.27.3" in roadmap and "## 0.27.2" in roadmap and "## 0.27.1" in roadmap and "## 0.27.0" in roadmap and "## 0.26.7" in roadmap and "## 0.26.6" in roadmap and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
|
||||
assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
|
||||
assert changelog.startswith("# Changelog\n")
|
||||
for release in ["0.27.2", "0.27.1", "0.27.0", "0.26.7", "0.26.6", "0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
|
||||
for release in ["0.27.3", "0.27.2", "0.27.1", "0.27.0", "0.26.7", "0.26.6", "0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
|
||||
assert f"## {release}" in changelog, release
|
||||
assert changelog.index("## 0.27.2") < changelog.index("## 0.27.1") < changelog.index("## 0.27.0") < changelog.index("## 0.26.7") < changelog.index("## 0.26.6") < changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
|
||||
assert changelog.index("## 0.27.3") < changelog.index("## 0.27.2") < changelog.index("## 0.27.1") < changelog.index("## 0.27.0") < changelog.index("## 0.26.7") < changelog.index("## 0.26.6") < changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
|
||||
assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog
|
||||
for developer_only in ["Clean the remaining compiler warnings", "Update package/source/UI/OpenAPI/release identity", "source regressions and update release identity"]:
|
||||
assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}"
|
||||
@@ -77,6 +78,7 @@ link_re = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
|
||||
paths = [ROOT / "README.md", ROOT / "ROADMAP.md", ROOT / "CHANGELOG.md", *sorted(DOCS.glob("*.md"))]
|
||||
for source in paths:
|
||||
text = source.read_text(encoding="utf-8")
|
||||
assert "Run compiler-enabled `cargo fmt --all`" not in text, f"obsolete release-host checklist remains in {source.relative_to(ROOT)}"
|
||||
for raw in link_re.findall(text):
|
||||
target = raw.strip().split()[0].strip("<>")
|
||||
if not target or target.startswith(("http://", "https://", "mailto:", "#", "/")):
|
||||
@@ -87,4 +89,8 @@ for source in paths:
|
||||
resolved = (source.parent / target).resolve()
|
||||
assert resolved.exists(), f"broken documentation link: {source.relative_to(ROOT)} -> {target}"
|
||||
|
||||
install = (DOCS / "INSTALL.md").read_text(encoding="utf-8")
|
||||
for marker in ["https://rustproxy.linuxiarz.pl/", "https://rustproxy-opn.linuxiarz.pl/", "Optional: Debian 13+ one-line source install/update"]:
|
||||
assert marker in install, marker
|
||||
|
||||
print("PASS: documentation index, Reader/API split, release docs and local Markdown links are consistent.")
|
||||
@@ -6,7 +6,7 @@ from pathlib import Path
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
def bundle(root=ROOT):
|
||||
languages=[];locales=set()
|
||||
for path in sorted((root/'langs').glob('*.json')):
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from rust_source import rust_source
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def read(path: str) -> str:
|
||||
@@ -100,6 +100,50 @@ require(
|
||||
"fn trim_hot_idle(&self)",
|
||||
)
|
||||
|
||||
# COLD bodies stay disk-backed and the long-lived index must not retain full
|
||||
# response header/validator allocations for every object.
|
||||
cache_model = read("src/cache/model/cache.rs")
|
||||
cold_entry = cache_model.split("struct ColdEntry {", 1)[1].split("#[derive(Default)]", 1)[0]
|
||||
assert "headers: HeaderMap" not in cold_entry, "COLD index retains full response headers"
|
||||
assert "origin_etag: Option<HeaderValue>" not in cold_entry, "COLD index retains ETag HeaderValue"
|
||||
assert "origin_last_modified: Option<HeaderValue>" not in cold_entry, "COLD index retains Last-Modified HeaderValue"
|
||||
assert "body_sha256: Key" in cold_entry, "COLD digest must use the compact 32-byte representation"
|
||||
assert "has_validator: bool" in cold_entry, "COLD index must retain validator presence without full values"
|
||||
require(
|
||||
"src/cache/model/cold_metadata.rs",
|
||||
"tokio::fs::read(&entry.meta_path).await",
|
||||
"pairs_to_headers(&file.headers)",
|
||||
"body_sha256 != entry.body_sha256",
|
||||
)
|
||||
|
||||
# Rate-limit client state stores a vhost allocation once and reclaims expired
|
||||
# fixed-window buckets without waiting for the global cardinality ceiling.
|
||||
require(
|
||||
"src/rate_limit.rs",
|
||||
"buckets: HashMap<String, HashMap<IpAddr, Bucket>>",
|
||||
"fn sweep_expired(&mut self, now: Instant, force: bool)",
|
||||
"now.duration_since(bucket.started) < Duration::from_secs(bucket.window_seconds)",
|
||||
"state.sweep_expired(now, false)",
|
||||
"state.sweep_expired(now, true)",
|
||||
"clients.shrink_to_fit()",
|
||||
"self.buckets.shrink_to_fit()",
|
||||
"pub fn maintenance(&self)",
|
||||
"MAX_BUCKETS: usize = 65_536",
|
||||
)
|
||||
require(
|
||||
"src/runtime/constructors.rs",
|
||||
"app.rate_limiter.maintenance()",
|
||||
"spawn_blocking(crate::diagnostics::trim_allocator)",
|
||||
)
|
||||
|
||||
# Compression remains response-streaming; no complete-body collection is added.
|
||||
require(
|
||||
"src/compression.rs",
|
||||
"No full-body aggregation is introduced.",
|
||||
"CompressionLevel::Fastest",
|
||||
"Compression::new(service)",
|
||||
)
|
||||
|
||||
# Other long-lived/background facilities: queue/task/worker cardinality is bounded
|
||||
# or owned by a cancellation-aware supervisor/TaskTracker.
|
||||
require(
|
||||
@@ -73,18 +73,18 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
||||
flat = module.parent.with_suffix(".rs")
|
||||
if flat.is_file():
|
||||
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/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", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.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", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]:
|
||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]:
|
||||
if not (root / path).is_file():
|
||||
raise ValueError(f"Incomplete RustProxy 0.27.2 source tree: missing {path}")
|
||||
raise ValueError(f"Incomplete RustProxy 0.27.3 source tree: missing {path}")
|
||||
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
||||
if '#[path = "auth/mod.rs"]' not in text:
|
||||
raise ValueError("src/lib.rs is not the 0.27.2 entry point; extract the complete package, not individual files.")
|
||||
raise ValueError("src/lib.rs is not the 0.27.3 entry point; extract the complete package, not individual files.")
|
||||
return messages
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[1])
|
||||
parser.add_argument("--root", type=Path, default=Path(__file__).resolve().parents[2])
|
||||
parser.add_argument("--repair-known", action="store_true")
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
@@ -17,6 +17,11 @@ import subprocess
|
||||
import tempfile
|
||||
import tomllib
|
||||
import zipfile
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
for relative in ("tools/legacy", "scripts/deploy", "scripts/migration"):
|
||||
sys.path.insert(0, str(ROOT / relative))
|
||||
|
||||
import backup as backup_module
|
||||
import deploy
|
||||
@@ -25,13 +30,11 @@ import upgrade as upgrade_module
|
||||
from rust_source import rust_source
|
||||
from web_assets import read_app_js, read_vhosts_js
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
def read_cache_source() -> str:
|
||||
return rust_source(ROOT, "src/cache/mod.rs")
|
||||
|
||||
def read_deploy_source() -> str:
|
||||
return "\n".join((ROOT / name).read_text(encoding="utf-8") for name in ("scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/deploy.py"))
|
||||
return "\n".join((ROOT / name).read_text(encoding="utf-8") for name in ("scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/deploy/deploy.py"))
|
||||
|
||||
def read_docker_agent_source() -> str:
|
||||
return "\n".join(path.read_text(encoding="utf-8") for path in sorted((ROOT / "docker-agent").glob("*.go")) if not path.name.endswith("_test.go"))
|
||||
@@ -124,7 +127,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert backup.is_dir()
|
||||
assert (backup / "src/obsolete.rs").is_file()
|
||||
assert not (target / "src/obsolete.rs").exists()
|
||||
assert 'version = "0.27.2"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert 'version = "0.27.3"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
||||
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
||||
assert (target / "langs/site.json").is_file()
|
||||
@@ -135,7 +138,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert (target / "docker-agent/certificate.go").is_file()
|
||||
assert (target / "docker-agent/server.go").is_file()
|
||||
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
|
||||
assert manifest["to_version"] == "0.27.2"
|
||||
assert manifest["to_version"] == "0.27.3"
|
||||
assert manifest["removed_obsolete_runtime_state"] is True
|
||||
assert not legacy.exists()
|
||||
assert not (backup / "legacy-data").exists()
|
||||
@@ -523,7 +526,7 @@ def source_contract_smoke() -> None:
|
||||
logging = rust_source(ROOT, "src/logging.rs")
|
||||
secret_store = rust_source(ROOT, "src/secret_store.rs")
|
||||
backup = rust_source(ROOT, "src/backup.rs")
|
||||
acme_worker = (ROOT / "scripts/acme_provider.py").read_text(encoding="utf-8")
|
||||
acme_worker = (ROOT / "scripts/runtime/acme_provider.py").read_text(encoding="utf-8")
|
||||
for feature_marker in ["AES_256_GCM", "master.key", "O_NOFOLLOW", "migrate_acme_credentials"]:
|
||||
assert feature_marker in secret_store, feature_marker
|
||||
for feature_marker in ["Argon2", "RPROXYB1", "secret-store-key.enc", "validate_backup_version"]:
|
||||
@@ -605,7 +608,7 @@ def source_contract_smoke() -> None:
|
||||
assert marker in admin, marker
|
||||
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
|
||||
assert '"/api/status"' not in anonymous_block
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.27.2"' in openapi_0241
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.27.3"' in openapi_0241
|
||||
assert 'id="panel-logging"' not in html
|
||||
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
|
||||
assert marker in html, marker
|
||||
@@ -980,10 +983,11 @@ def source_contract_smoke() -> None:
|
||||
assert '/Ubuntu%2014.04%20LTS/' in acl and 'repeated_slashes_are_normalized_for_routing' in acl and 'percent_encoded_spaces_are_valid_request_paths' in policies and 'repeated_slashes_route_normally_but_reach_upstream_unchanged' in policies and 'local_location_response_uses_response_rules_without_opening_backend' in policies and 'local_location_response_still_respects_route_acl' in policies
|
||||
db_source = rust_source(ROOT, "src/db.rs")
|
||||
acme_source = rust_source(ROOT, "src/acme.rs")
|
||||
acme_runtime = (ROOT / "src/acme/runtime_env.rs").read_text(encoding="utf-8")
|
||||
acme_runtime = (ROOT / "src/acme/runtime_env.rs").read_text(encoding="utf-8") + (ROOT / "src/acme/runtime_bootstrap.rs").read_text(encoding="utf-8")
|
||||
assert 'runtime_setup_once' in acme_source
|
||||
assert 'include!("acme/runtime_env.rs")' in (ROOT / 'src/acme.rs').read_text(encoding='utf-8')
|
||||
for marker in ['CERTBOT_PACKAGES', 'join("acme-python")', 'virtualenv>=20,<21', 'distro_certbot_available', 'acme.runtime_python_missing', 'acme.runtime_pip_missing']:
|
||||
assert 'include!("runtime_bootstrap.rs")' in (ROOT / 'src/acme/runtime_env.rs').read_text(encoding='utf-8')
|
||||
for marker in ['CERTBOT_PACKAGES', 'join("acme-python")', 'virtualenv>=20,<21', 'distro_certbot_available', 'acme.runtime_python_missing', 'acme.runtime_pip_missing', 'acme.runtime_venv_failed', 'acme.runtime_certbot_install_failed', '--break-system-packages']:
|
||||
assert marker in acme_runtime, marker
|
||||
assert "SCHEMA_VERSION: i64 = 8" in db_source and "certificate_renewals" in db_source and "0004_admin_preferences.sql" in db_source and "0005_certificate_api.sql" in db_source and "0006_certificate_api_credentials.sql" in db_source and "0007_secret_store.sql" in db_source and "0008_config_history.sql" in db_source
|
||||
runtime_source = rust_source(ROOT, "src/runtime.rs")
|
||||
@@ -1083,12 +1087,12 @@ def source_contract_smoke() -> None:
|
||||
tls_rust = rust_source(ROOT, 'src/tls.rs')
|
||||
assert 'pub fn validate_certificate_removals' in tls_rust and 'pub fn effective_cipher_suites' in tls_rust
|
||||
assert "renewals.json" not in main
|
||||
upgrade_source = (ROOT / "scripts/upgrade.py").read_text(encoding="utf-8")
|
||||
upgrade_source = (ROOT / "scripts/deploy/upgrade.py").read_text(encoding="utf-8")
|
||||
assert "removed_obsolete_runtime_state" in upgrade_source and "legacy-data" not in upgrade_source and "certs_dir.is_symlink()" in upgrade_source
|
||||
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
|
||||
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
|
||||
assert '"/api/config/history/restore"' in openapi
|
||||
for marker in ['"version": "0.27.2"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
for marker in ['"version": "0.27.3"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
||||
assert marker in openapi, marker
|
||||
assert '"software_update"' in openapi and '"update_available"' in openapi
|
||||
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
|
||||
@@ -1197,9 +1201,9 @@ def reproducible_archive_smoke(tmp: Path) -> None:
|
||||
with zipfile.ZipFile(first) as archive:
|
||||
names = archive.namelist()
|
||||
assert "RustProxy/SHA256SUMS" in names
|
||||
assert "RustProxy/scripts/rust_source.py" in names
|
||||
assert "RustProxy/scripts/rust_module_contracts.py" in names
|
||||
assert "RustProxy/scripts/source_module_contracts.py" in names
|
||||
assert "RustProxy/tools/checks/rust_source.py" in names
|
||||
assert "RustProxy/tools/checks/rust_module_contracts.py" in names
|
||||
assert "RustProxy/tools/checks/source_module_contracts.py" in names
|
||||
for name in names:
|
||||
if name.startswith("RustProxy/") and name.endswith(".rs"):
|
||||
assert len(archive.read(name).decode("utf-8").splitlines()) <= 500, name
|
||||
@@ -1221,8 +1225,16 @@ def reproducible_archive_smoke(tmp: Path) -> None:
|
||||
assert "RustProxy/src/admin/web/app.js" not in names and "RustProxy/src/admin/web/vhosts.js" not in names
|
||||
for module in ["model.go", "docker.go", "network.go", "certificate.go", "server.go", "main.go", "main_test.go"]:
|
||||
assert f"RustProxy/docker-agent/{module}" in names
|
||||
for module in ["deploy.py", "deploy_core.py", "deploy_runtime.py", "migrate_npm.py", "migrate_npm_core.py", "migrate_npm_api.py"]:
|
||||
assert f"RustProxy/scripts/{module}" in names
|
||||
for module in ["deploy.py", "deploy_core.py", "deploy_runtime.py", "upgrade.py"]:
|
||||
assert f"RustProxy/scripts/deploy/{module}" in names
|
||||
for module in ["migrate_npm.py", "migrate_npm_core.py", "migrate_npm_api.py"]:
|
||||
assert f"RustProxy/scripts/migration/{module}" in names
|
||||
assert "RustProxy/scripts/runtime/acme_provider.py" in names
|
||||
assert "RustProxy/scripts/build/swagger_assets.py" in names
|
||||
assert "RustProxy/tools/audit/admin_audit.py" in names
|
||||
assert "RustProxy/tools/legacy/backup.py" in names
|
||||
assert "RustProxy/integrations/opnsense-acme/opnsense-rustproxy-cert-deploy.sh" in names
|
||||
assert "RustProxy/integrations/routeros-oci-build/build-routeros-oci.sh" in names
|
||||
assert "RustProxy/nagios/check_rustproxy.py" in names
|
||||
assert "RustProxy/nagios/check_rustproxy_api" in names
|
||||
assert "RustProxy/src/admin/web/host404.js" in names
|
||||
@@ -1239,60 +1251,45 @@ def reproducible_archive_smoke(tmp: Path) -> None:
|
||||
assert len(timestamps) == 1
|
||||
|
||||
|
||||
def managed_acme_relocation_smoke(tmp: Path) -> None:
|
||||
"""Reproduce the 0.27.1 relocated console-script shebang failure."""
|
||||
stage = tmp / "acme-python.installing"
|
||||
def managed_acme_launch_smoke(tmp: Path) -> None:
|
||||
"""Managed ACME workers must execute the venv console script, not `python -m certbot`."""
|
||||
final = tmp / "acme-python"
|
||||
bindir = stage / "bin"
|
||||
bindir = final / "bin"
|
||||
bindir.mkdir(parents=True)
|
||||
python = bindir / "python"
|
||||
python.write_text(
|
||||
"#!/bin/sh\n"
|
||||
"test \"$1\" = -I && test \"$2\" = -m && test \"$3\" = certbot && test \"$4\" = --version || exit 2\n"
|
||||
"echo 'certbot 5.8.0'\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
python.write_text("#!/bin/sh\nexec /bin/sh \"$@\"\n", encoding="utf-8")
|
||||
python.chmod(0o755)
|
||||
console = bindir / "certbot"
|
||||
console.write_text(f"#!{python}\necho 'certbot 5.8.0'\n", encoding="utf-8")
|
||||
console.write_text("#!/bin/sh\ntest \"$1\" = --version || exit 2\necho 'certbot 5.8.0'\n", encoding="utf-8")
|
||||
console.chmod(0o755)
|
||||
stage.rename(final)
|
||||
|
||||
relocated_console = final / "bin/certbot"
|
||||
try:
|
||||
subprocess.run([str(relocated_console), "--version"], check=True, capture_output=True, text=True)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("relocated pip console script unexpectedly kept a valid staging shebang")
|
||||
|
||||
worker_path = ROOT / "scripts/acme_provider.py"
|
||||
worker_path = ROOT / "scripts/runtime/acme_provider.py"
|
||||
spec = importlib.util.spec_from_file_location("rustproxy_acme_provider", worker_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
old = os.environ.get("RUSTPROXY_CERTBOT_PYTHON")
|
||||
os.environ["RUSTPROXY_CERTBOT_PYTHON"] = str(final / "bin/python")
|
||||
old = os.environ.get("RUSTPROXY_CERTBOT_EXECUTABLE")
|
||||
os.environ["RUSTPROXY_CERTBOT_EXECUTABLE"] = str(console)
|
||||
try:
|
||||
spec.loader.exec_module(module)
|
||||
prefix = module.certbot_prefix()
|
||||
assert prefix == [str(final / "bin/python"), "-I", "-m", "certbot"]
|
||||
assert prefix == [str(console)]
|
||||
result = subprocess.run(prefix + ["--version"], check=True, capture_output=True, text=True)
|
||||
assert result.stdout.strip() == "certbot 5.8.0"
|
||||
finally:
|
||||
if old is None:
|
||||
os.environ.pop("RUSTPROXY_CERTBOT_PYTHON", None)
|
||||
os.environ.pop("RUSTPROXY_CERTBOT_EXECUTABLE", None)
|
||||
else:
|
||||
os.environ["RUSTPROXY_CERTBOT_PYTHON"] = old
|
||||
os.environ["RUSTPROXY_CERTBOT_EXECUTABLE"] = old
|
||||
|
||||
|
||||
def main() -> None:
|
||||
subprocess.run(["python3", str(ROOT / "scripts/rust_module_contracts.py")], check=True)
|
||||
subprocess.run(["python3", str(ROOT / "scripts/source_module_contracts.py")], check=True)
|
||||
subprocess.run(["python3", str(ROOT / "tools/checks/rust_module_contracts.py")], check=True)
|
||||
subprocess.run(["python3", str(ROOT / "tools/checks/source_module_contracts.py")], check=True)
|
||||
with tempfile.TemporaryDirectory(prefix="rustproxy-release-smoke-") as directory:
|
||||
tmp = Path(directory)
|
||||
sql_and_backup_smoke(tmp / "sql")
|
||||
upgrade_smoke(tmp / "upgrade")
|
||||
managed_acme_relocation_smoke(tmp / "acme-relocation")
|
||||
managed_acme_launch_smoke(tmp / "acme-launch")
|
||||
reproducible_archive_smoke(tmp / "archives")
|
||||
deployment_gate_smoke(tmp)
|
||||
npm_migration_smoke(tmp)
|
||||
@@ -1300,7 +1297,7 @@ def main() -> None:
|
||||
source_contract_smoke()
|
||||
container_contract_smoke()
|
||||
grafana_contract_smoke()
|
||||
print("PASS: 0.27.2 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
print("PASS: 0.27.3 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -13,7 +13,7 @@ import re
|
||||
|
||||
from rust_source import rust_source
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SRC = ROOT / "src"
|
||||
MAX_LINES = 500
|
||||
|
||||
File renamed without changes.
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from rust_source import rust_source, rust_tree
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
def read(path: str) -> str:
|
||||
target = ROOT / path
|
||||
+1
-1
@@ -8,7 +8,7 @@ from __future__ import annotations
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
LOCK = (ROOT / "Cargo.lock").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@@ -9,14 +9,14 @@ import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MAX_LINES = 500
|
||||
|
||||
|
||||
def lines(path: Path) -> int:
|
||||
return len(path.read_text(encoding="utf-8").splitlines())
|
||||
|
||||
# Browser implementation modules are bounded. Test harnesses under scripts/ are checked separately.
|
||||
# Browser implementation modules are bounded. QA harnesses under tools/checks are checked separately.
|
||||
for path in sorted((ROOT / "src/admin/web").rglob("*.js")):
|
||||
assert lines(path) <= MAX_LINES, f"{path.relative_to(ROOT)} has {lines(path)} lines (max {MAX_LINES})"
|
||||
|
||||
@@ -27,9 +27,9 @@ for path in sorted((ROOT / "docker-agent").glob("*.go")):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
assert not re.search(r"(?m)^func\s+Test[A-Z]", text), f"Go test remains in implementation file: {path.relative_to(ROOT)}"
|
||||
|
||||
# Operational Python entry points are split as product modules; large QA harnesses are intentionally exempt.
|
||||
for pattern in ("deploy*.py", "migrate_npm*.py"):
|
||||
for path in sorted((ROOT / "scripts").glob(pattern)):
|
||||
# Application-coupled Python entry points are split from external QA/dev tooling.
|
||||
for directory in ("scripts/runtime", "scripts/build", "scripts/deploy", "scripts/migration"):
|
||||
for path in sorted((ROOT / directory).glob("*.py")):
|
||||
assert lines(path) <= MAX_LINES, f"{path.relative_to(ROOT)} has {lines(path)} lines (max {MAX_LINES})"
|
||||
|
||||
for old in (ROOT / "src/admin/web/app.js", ROOT / "src/admin/web/vhosts.js"):
|
||||
@@ -39,8 +39,8 @@ for required in (
|
||||
"src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js",
|
||||
"docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go",
|
||||
"docker-agent/certificate.go", "docker-agent/server.go",
|
||||
"scripts/deploy_core.py", "scripts/deploy_runtime.py",
|
||||
"scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py",
|
||||
"scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py",
|
||||
"scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py",
|
||||
):
|
||||
assert (ROOT / required).is_file(), f"missing split source module: {required}"
|
||||
|
||||
@@ -62,4 +62,4 @@ if node:
|
||||
# The public vhosts asset wraps three individually valid source modules in one private IIFE.
|
||||
# Every source part is parsed above; this additional check keeps the assembled browser asset valid.
|
||||
|
||||
print(f"PASS: browser, Docker Agent and operational Python implementation modules are <= {MAX_LINES} lines, tests are isolated, and browser source boundaries parse cleanly.")
|
||||
print(f"PASS: browser, Docker Agent and application-coupled Python modules are <= {MAX_LINES} lines, tooling is separated, tests are isolated, and browser source boundaries parse cleanly.")
|
||||
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user