This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 22:53:33 +02:00
1 parent 174389e5b1
commit e6604e41fc
73 files changed
+1240 -201

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes. Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.25.2. compatibility. Current source version: 0.25.3.
## Delivery ## Delivery
+7
View File
@@ -4,6 +4,13 @@
> test-only changes and packaging implementation notes were moved to > test-only changes and packaging implementation notes were moved to
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md). > [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.25.3
- Add vhost and Custom-location HTTP method allow/block policies, with location-level reusable ACL profiles, inline IP ACLs and country access rules enforced before origin work.
- Forward a per-request SHA-256 `X-Request-ID` to backends and keep the same identifier in diagnostics and access logs for end-to-end correlation.
- Retain the newest 512 manual-configuration revisions and add a calendar-style dashboard history modal with revision metadata and path-level change previews.
- Migrate the embedded SQLite schema from 7 to 8 for configuration-history retention while preserving existing configuration automatically.
## 0.25.2 ## 0.25.2
- Allow password-encrypted full backups while RustProxy is running by using a consistent SQLite snapshot and a private staged copy of durable filesystem state; restore remains offline and atomic. - Allow password-encrypted full backups while RustProxy is running by using a consistent SQLite snapshot and a private staged copy of durable filesystem state; restore remains offline and atomic.
Generated
+1 -1
View File
@@ -1688,7 +1688,7 @@ dependencies = [
[[package]] [[package]]
name = "rust-proxy" name = "rust-proxy"
version = "0.25.2" version = "0.25.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rust-proxy" name = "rust-proxy"
version = "0.25.2" version = "0.25.3"
edition = "2021" edition = "2021"
rust-version = "1.88" rust-version = "1.88"
description = "RustProxy - independent reverse proxy with local administration" description = "RustProxy - independent reverse proxy with local administration"
+19 -16
View File
@@ -1,9 +1,9 @@
# RustProxy 0.25.2 # RustProxy 0.25.3
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration, Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools. streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
**0.25.2 release source.** **0.25.3 release source.**
## Layer-4 streams ## Layer-4 streams
@@ -11,13 +11,14 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners. Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
## What changed in 0.25.2 ## What changed in 0.25.3
- Create password-encrypted full backups without stopping RustProxy. SQLite is captured with a consistent snapshot and durable files are copied into private staging before the encrypted archive is written; restore remains offline and atomic. - Add per-vhost HTTP method policy with explicit allow-list or block-list modes. Method denials return `405 Method Not Allowed`; allow-list mode also emits `Allow`.
- Preserve safe internal relative symlinks across backup/restore, including Certbot `live/` -> `archive/` certificate links. Absolute, escaping and dangling symlinks are rejected instead of being followed. - Extend Custom locations with the same method controls plus reusable ACL profile, inline IP ACL and country allow/block policy. Host and location policies compose additively and are enforced before origin connection.
- Recover restored/resumed authenticated dashboard tabs from a transient invalid-origin state with a bounded same-origin reload. Server-side Host, Origin, request-marker and CSRF checks remain strict. - Generate a 64-character SHA-256 request ID for each exchange, forward it to the origin as `X-Request-ID`, reuse it in diagnostics and persist it in access logging.
- Retain up to 512 manual-configuration revisions in SQLite schema 8 and expose an authenticated revision-history modal with month/day navigation and per-revision change preview.
See [Web Server](docs/WEB_SERVER.md) for configuration and security details, [documentation index](docs/README.md) for the current documentation set, and [0.25.2 release audit](docs/V0.25.2_AUDIT.md) for point-by-point release status. Open follow-up work is tracked separately in [TODO](docs/TODO.md). See [host access](docs/VHOST_ACCESS.md), [Custom locations](docs/CUSTOM_LOCATIONS.md), [documentation index](docs/README.md) and the [0.25.3 release audit](docs/V0.25.3_AUDIT.md). Open follow-up work is tracked separately in [TODO](docs/TODO.md).
## Documentation ## Documentation
@@ -25,7 +26,8 @@ See [Web Server](docs/WEB_SERVER.md) for configuration and security details, [do
- [docs/README.md](docs/README.md) - canonical documentation index. - [docs/README.md](docs/README.md) - canonical documentation index.
- [docs/WEB_SERVER.md](docs/WEB_SERVER.md) - Web Server configuration, HTTP behavior and filesystem security. - [docs/WEB_SERVER.md](docs/WEB_SERVER.md) - Web Server configuration, HTTP behavior and filesystem security.
- [docs/PHP_FPM.md](docs/PHP_FPM.md) - implemented FastCGI/PHP-FPM integration, safety model and runtime requirements. - [docs/PHP_FPM.md](docs/PHP_FPM.md) - implemented FastCGI/PHP-FPM integration, safety model and runtime requirements.
- [docs/V0.25.2_AUDIT.md](docs/V0.25.2_AUDIT.md) - 0.25.2 live-backup/symlink/origin recovery release audit and compiler gate. - [docs/V0.25.3_AUDIT.md](docs/V0.25.3_AUDIT.md) - 0.25.3 method-policy/location-ACL/request-ID/config-history release audit and compiler gate.
- [docs/V0.25.2_AUDIT.md](docs/V0.25.2_AUDIT.md) - historical 0.25.2 live-backup/symlink/origin recovery audit.
- [docs/V0.25.0_AUDIT.md](docs/V0.25.0_AUDIT.md) - Web Server foundation specification audit. - [docs/V0.25.0_AUDIT.md](docs/V0.25.0_AUDIT.md) - Web Server foundation specification audit.
- [docs/TEST_REPORT.md](docs/TEST_REPORT.md) - verification results and environment limitations. - [docs/TEST_REPORT.md](docs/TEST_REPORT.md) - verification results and environment limitations.
- [docs/TODO.md](docs/TODO.md) - unresolved follow-up only. - [docs/TODO.md](docs/TODO.md) - unresolved follow-up only.
@@ -91,7 +93,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked
``` ```
The 0.25.2 source archive preserves the resolved `Cargo.lock` supplied with the source tree and The 0.25.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 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 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 locked check/test/release gates below and retain the verified lockfile. `dev.sh` can generate a
@@ -308,12 +310,13 @@ language JSON files survive the update; shipped files replace their old names,
whose previous bytes remain in the backup. Unknown custom source edits are not whose previous bytes remain in the backup. Unknown custom source edits are not
merged. Do not overlay individual source files onto an old tree. merged. Do not overlay individual source files onto an old tree.
RustProxy 0.25.2 keeps the existing SQLite schema version 7. Web Services and vhost/location RustProxy 0.25.3 upgrades SQLite to schema version 8 by adding retained manual-configuration
source selection are stored in the normal revisioned configuration document, so no separate history. Existing schema-7 data is migrated automatically; the current configuration is seeded
database migration is required; omitted source fields still default to Reverse Proxy. Once a as the first retained history row and future successful saves keep the newest 512 revisions.
0.25.2 configuration containing the new fields is saved, an older binary may reject unknown Vhost/location method and access-policy fields are additive and default to disabled/inherited
configuration fields. Roll back using the pre-upgrade data backup as well as the old source, behavior when omitted. Once a 0.25.3 configuration containing the new fields is saved, an older
not just the binary. binary may reject unknown configuration fields. Roll back using the pre-upgrade data backup as
well as the old source, not just the binary.
## Documentation and checks ## Documentation and checks
@@ -329,7 +332,7 @@ python3 scripts/release_smoke.py
# Runs those checks first, then the Rust suite when Cargo is available: # Runs those checks first, then the Rust suite when Cargo is available:
bash dev.sh test bash dev.sh test
# Source-only package, with no Git checkout required: # Source-only package, with no Git checkout required:
python3 make_zip.py /tmp/RustProxy-0.25.2.zip python3 make_zip.py /tmp/RustProxy-0.25.3.zip
``` ```
## New management and test-origin guide ## New management and test-origin guide
+12
View File
@@ -1,5 +1,17 @@
# RustProxy roadmap # RustProxy roadmap
## 0.25.3 - Request policy, correlation and configuration history
Current direction: 0.25.2 -> 0.25.3
- [x] Add per-vhost HTTP method allow/block policy.
- [x] Add per-location HTTP method policy, reusable ACL profile, inline IP ACL and country allow/block policy.
- [x] Enforce host and location policies before upstream selection/connection.
- [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 ## 0.25.2 - Live backup and resumed-session hardening
Current direction: 0.25.1 -> 0.25.2 Current direction: 0.25.1 -> 0.25.2
+72 -68
View File
@@ -1,19 +1,19 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
871884dcd2f86bd4ef429940a03da356836fd9d29af00008bbf4688d10cd79df AGENTS.md 962e4f330d5434c8a41668f96ed998940037d307f9469dd7ce7caf313e12eb92 AGENTS.md
fd5d1f9be9487eb58bfa55a2eb138e1ef675fe32c3bfd069d6d81e486fe5c592 CHANGELOG.md 3407d56ce02f80420c4a0571f376df887f2a782b8f44be1674a14b9e58a8c14c CHANGELOG.md
547274c990951a99d2a40f2ad86363da4dc3110d0d6384c985a61152c90a8024 Cargo.lock 8e75e382dcab138362e5c2ffe171143dfa6fcb5541911fdc488e494d69af2dc7 Cargo.lock
d918e60ed622b589f1ad116e78ecbdf24149d2a144025fbfde4a5b6513b9d181 Cargo.toml f7bce4570cbd1eeda68abbabd67666cc16663c041f2dd1cd00a10cf635738a38 Cargo.toml
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
a8f3a799da3ef63b9e95109c01e806b6e4ee9e0190ae0b3f046bf0b810f117ee README.md 0a310a02d96af8491d233b7607d431083e19985aed47ae7585a8b92911d65d6e README.md
3d1defbb40639dc707945297165eeab70795d25b6b0bdec1a333f9b59e06a16a ROADMAP.md 11bf5810056176e6233ffdadbddb2fd7b7980dd481ca84ffa84ae645e4a55042 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs 928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
0543cfaad02787b04037321d101cff6052cad68275694386f5bbaa6f35e731d8 debian/README.md ba49ec0e01e64effa35fbcf55cbcf3c12700c722c07def88c31c41197d4d8001 debian/README.md
4ca1bc64c9147f8f8705e81f7c3db3376ff84227ebac24c7f5516a0e24e5b203 debian/build-package.sh 91d9886a98127e4bfc99b3d9aab1f76a560c3e98238d3f7a13288f1e2a5996f1 debian/build-package.sh
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh 0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
60b631c2ec8299d1dc82f11505a9a541dd46c0bc3acd35895734193331497f91 debian/changelog b2f825f14dab955389bf9da286b3e6a1b9ff3aba93487d555fad1b74399974e3 debian/changelog
c90457b19b1b2f7998d3db59a656d215eb0fab23769197405960457b66d95cf7 debian/control c90457b19b1b2f7998d3db59a656d215eb0fab23769197405960457b66d95cf7 debian/control
dff2ecb9c14623416981f444bc6df1c4e2bf55d330260f36a312d8d42245400f debian/rules dff2ecb9c14623416981f444bc6df1c4e2bf55d330260f36a312d8d42245400f debian/rules
c537d54fd92a9b97c5f6e6ec63bc1276cda324513e2946b341b2193f63b62e6a debian/rustproxy.environment c537d54fd92a9b97c5f6e6ec63bc1276cda324513e2946b341b2193f63b62e6a debian/rustproxy.environment
@@ -24,7 +24,7 @@ cb0db03a2c1b06fa04a11f7690755a0ca90b17f19ed8fdb228810f505c48c78a debian/rustpro
e8e18df40bcd69d6aa404282679186d9b8256ac63a5b4e80d7f081e8a0095a2e debian/source/format e8e18df40bcd69d6aa404282679186d9b8256ac63a5b4e80d7f081e8a0095a2e debian/source/format
385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md 385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md
7b7fdf383bbaea4f37b5c120e98a24e9a075fba56ae7838a31a5152c7edcde02 deploy/rustproxy.service 7b7fdf383bbaea4f37b5c120e98a24e9a075fba56ae7838a31a5152c7edcde02 deploy/rustproxy.service
35a9e2248a62d9d06875a327c6925cdd3880a9e0ee7e9735b566c688ef5fbf8c dev.sh 5780b0c64b531ff9628a411791e90f76312b75e7689c5d361d0ddc8e491551d7 dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md 5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go 7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example 5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -33,35 +33,35 @@ bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/d
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh 9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go 3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go
91b976d55742ae69bf31558edb5fabf9e6d79e63dfb2d1a10bd6f76cbf3974f5 docker-agent/model.go 8c4bead51262d0152dcc3327f3fdd771c4792e2940f9b47b9d62e1c34abbfe9f docker-agent/model.go
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service 634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go 5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
802a30b793a6ae6a5046489612a23659f33d25e41d0794551381db202daac904 docs/ACME_PROVIDERS.md 802a30b793a6ae6a5046489612a23659f33d25e41d0794551381db202daac904 docs/ACME_PROVIDERS.md
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
6fb6dd879b83380187fc720e2668e412c3eacb66350e466b4aa1c3e6dba76edd docs/API.md 69ee2212f6bddbb064e0cbdf4f1df2046314bbf33e5b17c92b64dcd41ec67d37 docs/API.md
c46fa66c4f6b56ce74456d557072c712964e8180d599d10adee4d4f20b6d8329 docs/API_ACCESS.md 3ff2b6b7a087ff19363ee1e9fde4f4b378f25d6c278dcf09e7104db414e27664 docs/API_ACCESS.md
0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md 0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md
cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md
c25f3f6763c35dacf718b4b87c5665c2030b1fb3584e8c20940fc3ca7a2d26a3 docs/CACHE_DIAGNOSTICS.md c25f3f6763c35dacf718b4b87c5665c2030b1fb3584e8c20940fc3ca7a2d26a3 docs/CACHE_DIAGNOSTICS.md
f2f850b9500e7aa297337b54253f76fc3ceddad940a66c40332029adf32d124c docs/CHANGELOG_TECHNICAL_ARCHIVE.md f2f850b9500e7aa297337b54253f76fc3ceddad940a66c40332029adf32d124c docs/CHANGELOG_TECHNICAL_ARCHIVE.md
db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER.md db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER.md
c51ae0b6cacdd9b34f7b80872192d4a1e1c4e2ad697006d24855864c9e136a1b docs/CUSTOM_LOCATIONS.md 9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md 7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
9a882f07945b207d032936feb843f29467e4654f8255fa0104d988a54987f982 docs/ERROR_LOGGING.md 9a882f07945b207d032936feb843f29467e4654f8255fa0104d988a54987f982 docs/ERROR_LOGGING.md
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
94dd9a1be26d95de773a0b5f07ad460988cd84e29833f6a724102383903c4c9a docs/INSTALL.md 805f4d2a0fdec6b18cdfd78f4dce784735426d11e0643d835b35752f6aee4d68 docs/INSTALL.md
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md 3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
b30e6307d00580471a8ea44cdc88cbe2139676357f736660cb53f7120e8fae24 docs/LIVE_LOGS.md 2942a5a4310a5a21a75b844daf9e82be406e008a60b103cc0e4f16884df93701 docs/LIVE_LOGS.md
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md 7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
75b2de691687af350c5cb6332f9a20d8c15ea1021813dde274a23b7f9a8a1293 docs/MIGRATION.md 42e12cd39522e6869dde0db0b348366e412254e19f551579dd5cb2d44d2dfad2 docs/MIGRATION.md
5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md 5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md 1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md
3da047768f32064c5ef0ad7a4c73c4b14930fe7317c54f5650f426af9f484593 docs/NPM_MIGRATION.md 3da047768f32064c5ef0ad7a4c73c4b14930fe7317c54f5650f426af9f484593 docs/NPM_MIGRATION.md
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md 712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md
44e8b1940af190417574866911800ee5385ddce59c1f13c76dff35f02c134af8 docs/PROMETHEUS.md 44e8b1940af190417574866911800ee5385ddce59c1f13c76dff35f02c134af8 docs/PROMETHEUS.md
cf89251573e13f47344980c1ca4135dd01794914cb494d237d9d07c404cf6395 docs/README.md 0176b36e9ac2f2800f0d2776d2c66a2479b6cb931f9a3003218712774dd9cea5 docs/README.md
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md 6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md 0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md
@@ -69,9 +69,9 @@ ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.m
4bf7b3ea0bfe6c6b0ae0b7884b48b10ba29ff3a71611ec96957a3327d3c6ec88 docs/SECRET_STORE_BACKUP.md 4bf7b3ea0bfe6c6b0ae0b7884b48b10ba29ff3a71611ec96957a3327d3c6ec88 docs/SECRET_STORE_BACKUP.md
a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.md a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md 620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
559a0653ec7d4bc05d677408f91931881be03bc49b87ead629d2cafab9af2916 docs/SETTINGS.md d2c72fe56885488245cd3278416a35451eb7c954eecdfad5cdec88e8e305e3e9 docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md 3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
68f8985f169620ee6cc571cb4aba146466ffade4bf21af096bef979454f6fb90 docs/TEST_REPORT.md 8a24d14dfbeb8bcc76b3c876298298bb8d57ff2ca201854453bfed9b752979ea docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md 8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
b02957b708174c9def830ced3c70c943a47af7a33467ef858255d04ab92c4af0 docs/TRANSPORT.md b02957b708174c9def830ced3c70c943a47af7a33467ef858255d04ab92c4af0 docs/TRANSPORT.md
@@ -96,8 +96,9 @@ d0e609febbcd1cfdb8ea0b00b4fa6f081eac01e2d43a81e38a53ae25783152cc docs/V0.24.8_A
1a8a07eb150100e6d5ab8443dac664038e1d838033609df168aaf60a15aeb84f docs/V0.25.0_SECURITY_PREPROD_AUDIT.md 1a8a07eb150100e6d5ab8443dac664038e1d838033609df168aaf60a15aeb84f docs/V0.25.0_SECURITY_PREPROD_AUDIT.md
32c4053d48e8f24eb6c3395a4bbbc18f1c9790601166c73fb83173a14142789e docs/V0.25.1_AUDIT.md 32c4053d48e8f24eb6c3395a4bbbc18f1c9790601166c73fb83173a14142789e docs/V0.25.1_AUDIT.md
41793fe2d0fa4a2bd88448f5b3870649f08eeaadd4cd420820647d332e7fdb5a docs/V0.25.2_AUDIT.md 41793fe2d0fa4a2bd88448f5b3870649f08eeaadd4cd420820647d332e7fdb5a docs/V0.25.2_AUDIT.md
d64070b2186f9971862e9c929373d90a38022a3a9f3030e2014e280cd7b392f8 docs/VHOSTS.md 9dbb66e408bcd4d32267724e557fa9f7f44ef9b70db4ab170c61a7a839b7b157 docs/V0.25.3_AUDIT.md
e32472cea5ef3cd25c058e55df8adfcc1d7bf03e0d12dba69d2f2b9b94fe04b7 docs/VHOST_ACCESS.md 49a0ddd3071a480b331c108b718d497af9c2ec9844b3bfab5c01fbfab64c560d docs/VHOSTS.md
ace2c0dfe9154cbad760fd759efe8f120eb55dd8aa6f10f99933b7556d341f8f docs/VHOST_ACCESS.md
f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md
c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md
5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json 5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json
@@ -111,19 +112,20 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
17e7971fa9d9b141019fba3135d87cb1aa0cbc55d56b0718f879f880b615da26 examples/vhosts-balanced.json 17e7971fa9d9b141019fba3135d87cb1aa0cbc55d56b0718f879f880b615da26 examples/vhosts-balanced.json
e06905b6363103b1bcee1a09b9e02292033f114d88f3edbea22ddab53e9e5071 examples/vhosts-private.json e06905b6363103b1bcee1a09b9e02292033f114d88f3edbea22ddab53e9e5071 examples/vhosts-private.json
529b47c6d4ea44d2a25d1cf9561cb5ee1e05f50a568c6dba5889741bcf936434 grafana/README.md 529b47c6d4ea44d2a25d1cf9561cb5ee1e05f50a568c6dba5889741bcf936434 grafana/README.md
0cbe8c6805f62a884a81d5bf6c2d0ced49f269c1dd055237478f39ffca405f25 grafana/rustproxy-overview.json 63d0d880440f7c57da2960964d5c01eb087cbbfbed066e2b937ceea12e55fc9a grafana/rustproxy-overview.json
30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh 30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh 858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md 29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
402b463e17362363f3199d743286d26fc69b2f4342c23d4b6747974d21451a28 langs/en_US.json 1b967bc2a6682232427d83b3fa1d5a365610a38356b8e87efc89c1362e634a49 langs/en_US.json
931dbbcb75711d5a52bce45a3e1b5fdd0eec2ae13eb7854ce322b2e3cebd8a5b langs/pl_pl.json a28ef2ce1c4e6e736367d31fd47cc89c53babbfb2e491268ef83d7283c11c076 langs/pl_pl.json
615786f55505353ffa79930e726eb76001f04070d3f1b719c899df583fe25c3d make_zip.py 98cc369bd8051ee0f3980a2a290ff61bf8959b73ebaf94eee8cbe54260a5094b make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql 5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
57dc28982f416874d5d00d1d438a18846d1ce35cb940c4bcf67fd9a8bc57339c migrations/0005_certificate_api.sql 57dc28982f416874d5d00d1d438a18846d1ce35cb940c4bcf67fd9a8bc57339c migrations/0005_certificate_api.sql
faa7e50922bf180c363f7627fbcfe8079831e36282220d17481cf69bc64b57f1 migrations/0006_certificate_api_credentials.sql faa7e50922bf180c363f7627fbcfe8079831e36282220d17481cf69bc64b57f1 migrations/0006_certificate_api_credentials.sql
f093aef9639a959a2522946995a02d2e67c464e794c3ef665261a9a5b348c968 migrations/0007_secret_store.sql f093aef9639a959a2522946995a02d2e67c464e794c3ef665261a9a5b348c968 migrations/0007_secret_store.sql
90f6dce865496e018c047133d3bc1721fc2a19b2f4ced22e2a3625d9e9ff18ac migrations/0008_config_history.sql
478220552ffeea3de00e2866d427d9f87f4cfc4ae5232e5c3a774e62425b1e0c nagios/README.md 478220552ffeea3de00e2866d427d9f87f4cfc4ae5232e5c3a774e62425b1e0c nagios/README.md
91233910aa4dab07ae917fef93d018a7a0beaab697f9a0bb889ac613e595cca4 nagios/check_rustproxy.py 91233910aa4dab07ae917fef93d018a7a0beaab697f9a0bb889ac613e595cca4 nagios/check_rustproxy.py
a42e01b341725c9da0928edf40c8c3b9c814ef90592f208c52b1f2c56a2ad383 nagios/check_rustproxy_api a42e01b341725c9da0928edf40c8c3b9c814ef90592f208c52b1f2c56a2ad383 nagios/check_rustproxy_api
@@ -135,11 +137,11 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
7e524285bfa2e3dbde0391257660635b039f23b0b3807e9510df663f757a5ad8 scripts/admin_audit.py 7e524285bfa2e3dbde0391257660635b039f23b0b3807e9510df663f757a5ad8 scripts/admin_audit.py
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py 20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
23a42be7b6c405aa678576bdc316db50e6f09e8a7288cb04fed44d1bf6102d4e scripts/check_source.py 33b2754a8876a02c60ec067722ad6efa69475e4aee6f1b24eeb6bbbd9b1ccda2 scripts/check_source.py
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py 187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
d9eefaad26f831d07a2dc030ac39f13e6e6430c1fd94dace0073dba390c9aae2 scripts/deploy_core.py f0c72c5911ca674da57356fc3f1dbbb083e6824b778a97e055610b2307d9e93d scripts/deploy_core.py
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
0d6b407fcaa5adf674a9fe425b7c3ef5150d87cdbe18c575648e0f8a7f3e1f2b scripts/docs_check.py 5c8bb1be175dc1ab9ffd6a4eca87c1711c462167d0dfd08d8c502b31cf623c43 scripts/docs_check.py
6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py 6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py
2f846042b98a9148be944fb74a2b023b871ff884693a36847837228613a17679 scripts/memory_contracts.py 2f846042b98a9148be944fb74a2b023b871ff884693a36847837228613a17679 scripts/memory_contracts.py
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py
@@ -149,8 +151,8 @@ d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a scripts/mock_a
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py 2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md 2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh
e50e1ac1a55f9266da81cdf353638183c59f42fb9561209b18d8ba57dc7de4eb scripts/preflight.py 2bee5982de87c92d6420042e967661a9d7c795fde7fa3d72dfb5f98f6684940b scripts/preflight.py
c0913cceb60ca9f82bb0966f384a966d3c34ad7624c0dcdef4b7a22b8424802e scripts/release_smoke.py ebec656486c9a685162c46694e2474ecde1f3c2c9864fe88c1a312331de8ec5e scripts/release_smoke.py
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example 45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example
3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros 3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore 7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore
@@ -161,12 +163,12 @@ d53b2ae7315ac143bd17d6327611a39e7c23666408bb95e39da2ca8517322ccb scripts/router
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_source.py ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_source.py
1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py 1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py
295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py 295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py
2470b4b4625070fd896ae8438f7cb672ce3bfa8ffab328e2d83f2fd3b5f0af31 scripts/source_module_contracts.py a65010f828194885ffc7f0f733937957f28e6049cd978bec1380a2b8bfbd0113 scripts/source_module_contracts.py
e4d58ea628547a9d5b0247f320cd4c655caf83b8a942f07bfdc8faf1ab808a59 scripts/swagger_assets.py b097e41c3b55f2b96ad63f5e00849838cd8a1464ce52de6fc38d2633991a92de scripts/swagger_assets.py
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py 102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
115093b451c8be421652e23d3661f8fe3606eaa7569a65ca97fdfe88c1f07491 scripts/upgrade.py 344fe38b8fbfa2dc8c425522d2f5c054566f46d9d145ec0686aac5280b6b0164 scripts/upgrade.py
ea20bd340ecf3687c2721c522b4da09e88498de683bc4288bb85202969fd3d75 scripts/web_assets.py 43db35e7264b0345592172f2d1f6a6e4919fc4cbde2a9ff787acd7fff1d031d6 scripts/web_assets.py
c2b4ff7f13d1b1e4887d465fd35f1f8c2a431e642c54190dad9644d012238889 scripts/web_server_contracts.py a650d153a5a8f38d1ee53053bde1f0271205f628cddeda74e63f7fa6f885be63 scripts/web_server_contracts.py
b8ce3e8fd08865015ce312fae9c0457612af18d6ef4513a6281b9e3212f8d458 scripts/web_service_ui_test.mjs b8ce3e8fd08865015ce312fae9c0457612af18d6ef4513a6281b9e3212f8d458 scripts/web_service_ui_test.mjs
d8e2b51c39252dbb4305512fa9b1ef508ec610968aed316b10514fd160298762 src/acl.rs d8e2b51c39252dbb4305512fa9b1ef508ec610968aed316b10514fd160298762 src/acl.rs
e8ffed9a9f50a467062d0758aec6aefdc8a39615282b2eef73603d08bc33c00a src/acl/tests.rs e8ffed9a9f50a467062d0758aec6aefdc8a39615282b2eef73603d08bc33c00a src/acl/tests.rs
@@ -178,27 +180,28 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi
7acbaf34e7b953b753ab074893e47b5024f0cd756dc4c7a1074067dd936c2e8e src/acme/tests.rs 7acbaf34e7b953b753ab074893e47b5024f0cd756dc4c7a1074067dd936c2e8e src/acme/tests.rs
8a07a8c9d845e55ffa47332d4379e8ffdc2cee09b22e6c6396edb8e768b24540 src/admin/access.rs 8a07a8c9d845e55ffa47332d4379e8ffdc2cee09b22e6c6396edb8e768b24540 src/admin/access.rs
21e4ae7e72b0c86ba674bc820aabdd201cd62126fdd09ba22cc4b420e38ad300 src/admin/certificates.rs 21e4ae7e72b0c86ba674bc820aabdd201cd62126fdd09ba22cc4b420e38ad300 src/admin/certificates.rs
ae1ac6507a66bf2d0b27e6e4df99fe31bd81a52e616784eed2803730ba234f6f src/admin/documentation.rs 63a54085ebae3e731f5f0789661d783367c605455704549d725b001a418cc1b7 src/admin/documentation.rs
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs 67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
cc5c84d8476e2ba11c3a4b1996ca616eb402bb0aea734434ac6e185e1041736e src/admin/logging_api.rs cc5c84d8476e2ba11c3a4b1996ca616eb402bb0aea734434ac6e185e1041736e src/admin/logging_api.rs
468b6062cb806ef489bc107f69d4725dbf9c08d7e18d15b4482ae0f309c15d2f src/admin/mod.rs 468b6062cb806ef489bc107f69d4725dbf9c08d7e18d15b4482ae0f309c15d2f src/admin/mod.rs
4a8df2959f43c71b12a6d5d77613137ae508408a0baf5d30900dffad1c042c58 src/admin/openapi.json 28635ff531d1ca575f04b6be06563cc28b94c0d23701dced43e437060d152b25 src/admin/openapi.json
04ac147a823a1b5510977185fc10b4f5e3fddaa76c00ee73ab5209782dd4d859 src/admin/operations.rs 04ac147a823a1b5510977185fc10b4f5e3fddaa76c00ee73ab5209782dd4d859 src/admin/operations.rs
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
b7a1b9535d01db8b9cb0f97048203dffd55404b331ec8e0386c63e8c417bf19a src/admin/router.rs 8029f4b426d5b40189aa743fbdf79442ec4471360986ac99a09f2ab34bdcd437 src/admin/router.rs
68685c80f57d8e2b0e1d835368893ddc71a8bb3519933fde99e74f6e64a0a6da src/admin/session.rs 68685c80f57d8e2b0e1d835368893ddc71a8bb3519933fde99e74f6e64a0a6da src/admin/session.rs
6720220b8ca9b7a414540755b6515706c5031b5f563a7daae94c6c8c5f2da810 src/admin/status.rs e60217a8ae735d07901545cdb6b2a976d9abcc2ba0a998d297e2019c8f376462 src/admin/status.rs
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
9bed1f93a8ef7bea4478b26bc825aacc8825ca70cf26c993fe78ac784aab2526 src/admin/tests.rs 9bed1f93a8ef7bea4478b26bc825aacc8825ca70cf26c993fe78ac784aab2526 src/admin/tests.rs
5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs 5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs
b8866db46f8be4f3f5bcceb5d84d32fe482f65d534b65cc75b4b3e33dba576c0 src/admin/tests/part_02.rs b8866db46f8be4f3f5bcceb5d84d32fe482f65d534b65cc75b4b3e33dba576c0 src/admin/tests/part_02.rs
b31d594499d7f8164c14b66dd77971b9985cefd99bd4d3686f9ea26e11e5a59c src/admin/tests/part_03.rs b31d594499d7f8164c14b66dd77971b9985cefd99bd4d3686f9ea26e11e5a59c src/admin/tests/part_03.rs
7c1c18d2d07311f7fcffd2872cca6d5e56212242e4ab7a8912395b4df6d3dcd2 src/admin/tests/part_04.rs 854a17780d447ab28c77bcecd58fbba8a4a63805a6c305ba9fbe82f5b432ea77 src/admin/tests/part_04.rs
829d0ef87b6d0c4d2a8a9e1e299c27d2d84f83a73ea222df45b0384abc2e2da8 src/admin/tests/part_05.rs 829d0ef87b6d0c4d2a8a9e1e299c27d2d84f83a73ea222df45b0384abc2e2da8 src/admin/tests/part_05.rs
23c55f23deeec844a9abc2289440c9e0583aaeb72416fc5f3632a43a864c0efd src/admin/web/acme.js 23c55f23deeec844a9abc2289440c9e0583aaeb72416fc5f3632a43a864c0efd src/admin/web/acme.js
bdedac825f9176a7c7220304d015c767a6b98f57b02ca7828ff0ee0b21b8ed25 src/admin/web/app/config-history.js
f97188d3c9dcd60b19ff1a18f1e3eb5c942c996fbefe805fdc2ca07fa9785139 src/admin/web/app/core.js f97188d3c9dcd60b19ff1a18f1e3eb5c942c996fbefe805fdc2ca07fa9785139 src/admin/web/app/core.js
19abc332a1009bcb087fe4ebb58d458739b27480ab50715b03d07fd408302942 src/admin/web/app/runtime.js 19abc332a1009bcb087fe4ebb58d458739b27480ab50715b03d07fd408302942 src/admin/web/app/runtime.js
006952bbdd922772814276e37b9d3f1ca4d93b1d1ca266972647743aadadf78f src/admin/web/app/settings.js 122d4b9d95a9ae42a6c9ef0c79ad586d27a56ec7ae77fd1cce2ad5996f9dab09 src/admin/web/app/settings.js
d0be71555de13f732fd6ee44438a0110eb08c91e78bbc1aa36a1a84182780210 src/admin/web/cache-panel.js d0be71555de13f732fd6ee44438a0110eb08c91e78bbc1aa36a1a84182780210 src/admin/web/cache-panel.js
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html 50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
3b84a7a08f9f68e2c78fed4523b594c31a251addc5480a69c5f6949908230539 src/admin/web/docker.js 3b84a7a08f9f68e2c78fed4523b594c31a251addc5480a69c5f6949908230539 src/admin/web/docker.js
@@ -213,7 +216,7 @@ f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js
5ac6490404f1f23dd558d5b3f5a07b9098c2813b502b09b7efc272a6c1fe4f59 src/admin/web/index.html ba71109f9ae01fa572cec8e6b964788eedaf9b8c954cd225230e7333ddbc9f96 src/admin/web/index.html
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js 24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json
a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html
@@ -222,11 +225,11 @@ e41619e5cb0ba01a3d287748f28eb38bbe7641f9bedade252d286c18e619c400 src/admin/web/
22318f39f7bb1ae98e65737d3c2ad9e63065b8e94ad59bed560c0e80d3a46949 src/admin/web/redirects.js 22318f39f7bb1ae98e65737d3c2ad9e63065b8e94ad59bed560c0e80d3a46949 src/admin/web/redirects.js
811a6b6d7f7c0d0a268113849be021176098097cd33279acb9a9ffc40d2b934f src/admin/web/settings.js 811a6b6d7f7c0d0a268113849be021176098097cd33279acb9a9ffc40d2b934f src/admin/web/settings.js
1a30affd6a717095a1763998a5850a79ae0f86b7b694806f2aba2b880735dd1a src/admin/web/streams.js 1a30affd6a717095a1763998a5850a79ae0f86b7b694806f2aba2b880735dd1a src/admin/web/streams.js
f04e772b89e9aa265a55e2601675ccd0217df012e6f426d3be3461d5daece59e src/admin/web/style.css 697d31d26af0428d9fd50ddf70a9cf9084714e597c0818a4367a871b084dcbb5 src/admin/web/style.css
47e98065281c04b4c185d53df4ed0cb12fabc716e52143e6ae3c3c25db62126a src/admin/web/traffic-panel.js 47e98065281c04b4c185d53df4ed0cb12fabc716e52143e6ae3c3c25db62126a src/admin/web/traffic-panel.js
049be02e0f68c3f6702f3bf29be760c4e5bc1199393ed198107c0a264711300d src/admin/web/transform-panel.js 049be02e0f68c3f6702f3bf29be760c4e5bc1199393ed198107c0a264711300d src/admin/web/transform-panel.js
f42f079f648ce80ec6c55d281d73c9dcc69a120e6cdb7e3eb32edda6bf99aecc src/admin/web/vhosts/core.js ac79a0f5471f110850bf5ceaaee9a3903bb92fb2edc10e969d8fda81f66571c0 src/admin/web/vhosts/core.js
3c483d7b1eb69455d7bceb83f44f2b8fd60e02b2ec0dd03f4b69308525fa54a4 src/admin/web/vhosts/editor.js eefcd49aa90fb11d603392e230d48505de236e73cbcc124fb42cf897febd7136 src/admin/web/vhosts/editor.js
769383dfc5afa28e01a1e99c9eebcce39fc9457fba0602992f750acab9911236 src/admin/web/vhosts/render.js 769383dfc5afa28e01a1e99c9eebcce39fc9457fba0602992f750acab9911236 src/admin/web/vhosts/render.js
0de35a0671f3ac8fed62260d63ae43d04efce4947428ebdb9254cff0295295f9 src/admin/web/web-server.js 0de35a0671f3ac8fed62260d63ae43d04efce4947428ebdb9254cff0295295f9 src/admin/web/web-server.js
98c00d99822f9be54fc58afb6d4b577901712effd2c5eddce014312681864625 src/auth/files.rs 98c00d99822f9be54fc58afb6d4b577901712effd2c5eddce014312681864625 src/auth/files.rs
@@ -298,14 +301,15 @@ ad47db6519e471ec53a408129bb10a794033430e28fcd3d9a4cf273669900829 src/compressio
7921ae451ccd65d647165b02919bd97399399da471454a6a1a1f63e682b76fc8 src/config.rs 7921ae451ccd65d647165b02919bd97399399da471454a6a1a1f63e682b76fc8 src/config.rs
fcb45e7b5cf59f1fd18975db501eafcfb8d70c6139589d851f46a3fc5eac043e src/config/backend.rs fcb45e7b5cf59f1fd18975db501eafcfb8d70c6139589d851f46a3fc5eac043e src/config/backend.rs
0ce2c6b96fd96518258f08c8ed73db333948a623796a81f8af0976c3bea1e188 src/config/compile.rs 0ce2c6b96fd96518258f08c8ed73db333948a623796a81f8af0976c3bea1e188 src/config/compile.rs
2abf288c0541474e52e45b80f2fff41c572612662be3f9dea4342a1d41851440 src/config/model.rs 66b3061200c06c4fffe3350e3807bd9948b9b05aa91410da5d05edef1e0d791d src/config/model.rs
fcd733906d4477911a20ba9315635c88c5fa3f8d515420390024e06a750571a0 src/config/routing.rs a74c5879d66129f8c32677dec9594281c412b890d09605d380c0e3a61a56db23 src/config/routing.rs
51aa3205dbe5c9a789369736285703338b1524b8fe7cd366eb935abf3dd6bfb9 src/config/snapshot.rs 51aa3205dbe5c9a789369736285703338b1524b8fe7cd366eb935abf3dd6bfb9 src/config/snapshot.rs
7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/config/tests.rs 7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/config/tests.rs
be6852c11041bcea2c387b76ba3d66a8ac06f8425bd1c976075efe5cc802902d src/config/tests/part_01.rs be6852c11041bcea2c387b76ba3d66a8ac06f8425bd1c976075efe5cc802902d src/config/tests/part_01.rs
467b89435d0d115b227c8c8093d06c3443b5c4c063bc18e322e7d2edf635a02e src/config/tests/part_02.rs 467b89435d0d115b227c8c8093d06c3443b5c4c063bc18e322e7d2edf635a02e src/config/tests/part_02.rs
8568fcbc8e5d06631827fea4fd165c5596bd530c948ec50babca30f7fc4dade0 src/db.rs 6cda152af92f4cc4ef07406f56c1d90ef65ce4a9b103511d9daf882951cdd885 src/db.rs
cef6f5d63aa33433df5acc40c22e55a55c8ea275424b200084eceea3e3432cbd src/db/tests.rs fe229630e81b10b25edc1319857db1ef406cf5acc4cce4b4cd90f855d1cf55de src/db/history.rs
49200d4e69ad929d69d249fc3849b4227170e47016599fb034e6858322588753 src/db/tests.rs
19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs 19ab6c3ae37009c544291cb0b65c1bb9c721e168f95c1ed3fa88155aca7463ad src/demo.rs
e69031875ffec32c9f548c43bed5f1a5c4c626e9568c9173e3b093f9a8b73036 src/diagnostics.rs e69031875ffec32c9f548c43bed5f1a5c4c626e9568c9173e3b093f9a8b73036 src/diagnostics.rs
b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs
@@ -323,17 +327,17 @@ e40171e72327bc4350ba95cedacde2f3c9148c26a5a232d9f790f4449f20a685 src/geoip/test
68bc11160bc5275a904e48af8698b37433096fb6d592364d8281d59bb9e2b3f4 src/http3/tests.rs 68bc11160bc5275a904e48af8698b37433096fb6d592364d8281d59bb9e2b3f4 src/http3/tests.rs
3dab8fac75263120e48ee869ad55a3e2ccc2a19fc9f6d6a6fe055657ec55b1e3 src/lib.rs 3dab8fac75263120e48ee869ad55a3e2ccc2a19fc9f6d6a6fe055657ec55b1e3 src/lib.rs
6946917d57fb589bdba639fef2cec1d256fac0c8bfde92adffef2b31c3b3e08c src/logging.rs 6946917d57fb589bdba639fef2cec1d256fac0c8bfde92adffef2b31c3b3e08c src/logging.rs
452d30961b7640979c61146aef0fd1f218ad080fceb2dfcd83f31267c33603a8 src/logging/format.rs e77598ade4ad6fc27f384bd1ee42005cd745c31c2306ffe03a94be6fbf97eee2 src/logging/format.rs
b1f2fa0df9ffb7e621d0009c30a188402f0741701f68b77977ef1f40421ab659 src/logging/model.rs 345f46b92afdeb554f4459b1c37c14cb63d2d4dfdaff8be7750ce1a80dd451f0 src/logging/model.rs
13194c28f0e38e00727c70552403e3f45d4aa819ed0aba168c60d098fc03f232 src/logging/runtime.rs 13194c28f0e38e00727c70552403e3f45d4aa819ed0aba168c60d098fc03f232 src/logging/runtime.rs
d55dddba3f35a4f51a9b224af3b776ce7d25c8e57ae061e130e451aa5769edea src/logging/tests.rs 8c1f4a05a538daeda2de5a35d073750c910aed808746275afe58cf5a9d2c392d src/logging/tests.rs
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs 3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
79e1d39d7be966c22faad99146e384407427f2b1cec917b73786c5808ecc12f1 src/main/admin_runtime.rs 79e1d39d7be966c22faad99146e384407427f2b1cec917b73786c5808ecc12f1 src/main/admin_runtime.rs
077c840d16e90847d8ec41b804405214048b6645d6425034f5da4d8f78d2a4ee src/main/bootstrap.rs 9812e4067fab9965c981725315b217c80a395e5aecbf6439e79bc4da0c29722b src/main/bootstrap.rs
192623ac6d9ef376648d3dc372e7dedc0a37afbaa3dacf737e2e372228de09e5 src/main/proxy_runtime.rs 192623ac6d9ef376648d3dc372e7dedc0a37afbaa3dacf737e2e372228de09e5 src/main/proxy_runtime.rs
f58e3b75ed39e85fb4f943b970ea5bb271e709bce0979fd96a25e416387a81d9 src/main/shutdown.rs f58e3b75ed39e85fb4f943b970ea5bb271e709bce0979fd96a25e416387a81d9 src/main/shutdown.rs
a8783a34c01a288822c71b2a6807cc8d7645e22af5766053fed5c429d4536407 src/main/shutdown/tests.rs a8783a34c01a288822c71b2a6807cc8d7645e22af5766053fed5c429d4536407 src/main/shutdown/tests.rs
54fe6de7bf34db0237c1dbd0cf626f114320c27687923c801a1ec8c8b1af1f3f src/main/tasks.rs c0281857ff511d9a4042d15ed25948913d8b136fc5d8970df127b5947afa3a54 src/main/tasks.rs
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs 49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs 201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
b1ed4042f2d6c1029523c3977b85e2ab6020e2da7d5079f98051cef5e96ca390 src/metrics/vhost.rs b1ed4042f2d6c1029523c3977b85e2ab6020e2da7d5079f98051cef5e96ca390 src/metrics/vhost.rs
@@ -348,22 +352,22 @@ b40d1404dae711f9d79a777ce3e4459ecebc3601d16067bdb2a017412e703715 src/prometheus
33e409fae3f8e9574ba2776b0d2eb6c75b26f45f48806e307d78b6fa355d8c86 src/prometheus/model.rs 33e409fae3f8e9574ba2776b0d2eb6c75b26f45f48806e307d78b6fa355d8c86 src/prometheus/model.rs
53bb68f43cf66916dcb4f5ffb47c89a63275c589b6a23691bd9ccaf4fdc56332 src/prometheus/tests.rs 53bb68f43cf66916dcb4f5ffb47c89a63275c589b6a23691bd9ccaf4fdc56332 src/prometheus/tests.rs
b3ea198d87f95942a2991c122b4c90e1f0244dd1f592fa276f0df8a43e43ca43 src/prometheus/vhosts_backends.rs b3ea198d87f95942a2991c122b4c90e1f0244dd1f592fa276f0df8a43e43ca43 src/prometheus/vhosts_backends.rs
4467766076a23612938cb23cc1bb0cca08aaf4df35898156273499801135049a src/proxy/body.rs 1db8f018421a2c6821bb6c80d36ce2b555eb524011ff418c158fa44767cb028e src/proxy/body.rs
0b043c50d9da00a0cfae795ecdbab030425c2d9ed776488305db9daf73c11dec src/proxy/body/tests.rs 60d6b2049b420934f838394a351cdc731ecafbb6deaadff666af0373f95d97d9 src/proxy/body/tests.rs
e84b3b51e99a4281d70f426f6d8345ec3a244e6e1758a868a23cfbb08acaf62d src/proxy/cache_helpers.rs 2f04b5530d44827f9fa722ba9f5e80d5f8afdf3af973970afce4a3d540c5fe9c src/proxy/cache_helpers.rs
cfb0910af8cd868bde3f6c9992319e46d5328d6d8f69c50701df0ea12bef2108 src/proxy/forward/cache_lookup.rs cfb0910af8cd868bde3f6c9992319e46d5328d6d8f69c50701df0ea12bef2108 src/proxy/forward/cache_lookup.rs
212de22f407dd96800afcf65fd1fc7c40fede1b88f80d04850eb8d1227b08a5b src/proxy/forward/response.rs 212de22f407dd96800afcf65fd1fc7c40fede1b88f80d04850eb8d1227b08a5b src/proxy/forward/response.rs
581792030807d1db28091b232eabc3b86b9f19b890afa7d6295c051ef89f14a7 src/proxy/forward/route_cache.rs ab853f2dd6d870315fc22cbfe865d98cbb9f55c6ff09172e79d78721c797bd9b src/proxy/forward/route_cache.rs
51c620b77004cca26edf387f9650f07d27801e093d32cc0162eaee1d0702c126 src/proxy/forward/success_finish.rs 51c620b77004cca26edf387f9650f07d27801e093d32cc0162eaee1d0702c126 src/proxy/forward/success_finish.rs
5b639e63bc0110c93ab166bd22ac5bea5d8fa260e62c41d4bdb25d5ffff129d4 src/proxy/forward/success_prepare.rs 5b639e63bc0110c93ab166bd22ac5bea5d8fa260e62c41d4bdb25d5ffff129d4 src/proxy/forward/success_prepare.rs
23b6bd26f6175b52c13d2571318fddd12550ba34ee77b87b5ac0e4811f8e2bee src/proxy/forward/upstream.rs 23b6bd26f6175b52c13d2571318fddd12550ba34ee77b87b5ac0e4811f8e2bee src/proxy/forward/upstream.rs
abdc5681e2f1b9cd59fae410aaf858903a723dbad081b27159313ec2d62ddbcf src/proxy/forward_entry.rs 720e61e70e3b595900595090265a2e536efd05337eacd42a904ebba37f5ca390 src/proxy/forward_entry.rs
10d740d2e41c2b1349553f8f6e8d624fda8085f665fe6b4a84d913cd465c5825 src/proxy/headers.rs 10d740d2e41c2b1349553f8f6e8d624fda8085f665fe6b4a84d913cd465c5825 src/proxy/headers.rs
5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs 5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs
f6aef54204b5bb9305dd1367818c7ffd3cb273008404c9fab0317af1d13ba7a6 src/proxy/mod.rs f6aef54204b5bb9305dd1367818c7ffd3cb273008404c9fab0317af1d13ba7a6 src/proxy/mod.rs
ac678d8a2c565fc5662d97acf60e3c59850446861103d2fe04719f96295933a8 src/proxy/tests/authority_tests.rs ac678d8a2c565fc5662d97acf60e3c59850446861103d2fe04719f96295933a8 src/proxy/tests/authority_tests.rs
b8214b1c847d80c4d42390e913832e2a393dd35e0c8f0657d6eb8aea3254c155 src/proxy/types.rs b8214b1c847d80c4d42390e913832e2a393dd35e0c8f0657d6eb8aea3254c155 src/proxy/types.rs
1dc295db8e0c395bc8483ec5b3679b585bb59cb82942bf52b647b9853a7c5e4c src/proxy/upstream_helpers.rs df161a330667b6409ffd46ba22bf2f9fa9ae61eb060277187af576133ee1e62d src/proxy/upstream_helpers.rs
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs 69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
5980ffd2f48248abab72ec96ce22da80a023ccd5e14f6bc21e3c85125876f8b0 src/proxy/websocket/handshake.rs 5980ffd2f48248abab72ec96ce22da80a023ccd5e14f6bc21e3c85125876f8b0 src/proxy/websocket/handshake.rs
11b1d7291c75c5b41aeb551ae87fb4878f93ba68c87178a9aeff88fd84fe2b6a src/proxy/websocket/tests.rs 11b1d7291c75c5b41aeb551ae87fb4878f93ba68c87178a9aeff88fd84fe2b6a src/proxy/websocket/tests.rs
@@ -390,7 +394,7 @@ cdb65a73886c360f453ca8f2ba473baa76e9bad92a36b4b3d350cd5db053577d src/server/lis
8838a30cc1b070c7d7deec408b5457e7d0c561f2c583be20bc49d41e7735b316 src/server/serve.rs 8838a30cc1b070c7d7deec408b5457e7d0c561f2c583be20bc49d41e7735b316 src/server/serve.rs
a51651d9c97965f9c34d127cdd81c14a42532dc26e794165456e80190e29c17b src/server/serve/tests.rs a51651d9c97965f9c34d127cdd81c14a42532dc26e794165456e80190e29c17b src/server/serve/tests.rs
0ac35c8432c9e3a6d31374e7cd96731b685d9a5e01beb751bdbfff2eb144288b src/server/tls_runtime.rs 0ac35c8432c9e3a6d31374e7cd96731b685d9a5e01beb751bdbfff2eb144288b src/server/tls_runtime.rs
8ec31f92cf11a3b676bcf2b6cc251a5b509ff34088ae6efef513fffdc84a3be0 src/settings.rs 1be88858215d393d4f62f04f36a77ee64ce476be1b642c8f37e059c33423fea7 src/settings.rs
b8b3150abe1c6f61d3f593e169795aae397b578607710ea6fd887b90f37b872e src/settings/tests.rs b8b3150abe1c6f61d3f593e169795aae397b578607710ea6fd887b90f37b872e src/settings/tests.rs
566daf9c0e1a30da2dc18e51090541a85ced3320a0fc00ed1d1de3e2b4da5072 src/stream.rs 566daf9c0e1a30da2dc18e51090541a85ced3320a0fc00ed1d1de3e2b4da5072 src/stream.rs
46d9f63ce13a634e00f38d36ca3bdb217f2e37f04d75a4ef118e7dac9896a489 src/stream/model.rs 46d9f63ce13a634e00f38d36ca3bdb217f2e37f04d75a4ef118e7dac9896a489 src/stream/model.rs
@@ -429,15 +433,15 @@ b5e505a2c8e38ca5915dd3ea4f0b33a5bf9cc4ef3e0ec20054748bb1820502c3 src/update_che
e4b85f407103f941cc535116b61527be20e5489af0dfce5371fdfc17bb627844 src/upstream.rs e4b85f407103f941cc535116b61527be20e5489af0dfce5371fdfc17bb627844 src/upstream.rs
84bb415eac5d6d7965763681339f2608d2c72aa12c7b7f3ae530ff94740a9223 src/upstream/tests.rs 84bb415eac5d6d7965763681339f2608d2c72aa12c7b7f3ae530ff94740a9223 src/upstream/tests.rs
e4c1ff8f53112e7cfad101a266355f7e47b82895480db431662f4cc59a1d9702 src/vhost.rs e4c1ff8f53112e7cfad101a266355f7e47b82895480db431662f4cc59a1d9702 src/vhost.rs
bb64d0335559968b00a28b9b0088fc4738dfd0c30cc7a6a5314613a06735c23a src/vhost/model.rs 5cbd6b00b3c4294f24d59f5c141b392c32806eaf62de429b2da5f1146b918dec src/vhost/model.rs
fae5a47efda9a4ae4dc94711ecc5857e10b3c20aa2bf2c742c59651a85b9d978 src/vhost/tests.rs 7127e081d884863b21c06f700b54884f90ec04efdfce835591b930f2d210faf6 src/vhost/tests.rs
744f329e868a31a00c0e03c51a58397bf9e57aaeb5b7dac75c32e7eaccc9d7f0 src/vhost/validation.rs f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/validation.rs
8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs 8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs
34417031a2544bcc88ddee81a949da757787f0f1151a85ac9739da93fb9d95e5 src/web_server/config/compile.rs 34417031a2544bcc88ddee81a949da757787f0f1151a85ac9739da93fb9d95e5 src/web_server/config/compile.rs
9c1c8d6eedecb95eadbfb23c3caea562f6f7a995606ef988f25b58c2d03bcdd5 src/web_server/config/model.rs 9c1c8d6eedecb95eadbfb23c3caea562f6f7a995606ef988f25b58c2d03bcdd5 src/web_server/config/model.rs
6191f4d541476c4a3ae296e7dd28bdb9f8a123fc509b47619f6f44519c44d278 src/web_server/config/validation.rs 6191f4d541476c4a3ae296e7dd28bdb9f8a123fc509b47619f6f44519c44d278 src/web_server/config/validation.rs
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs 86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
5e3a0148ac9e2d50160c8e655b378c4a061f8912012d1cb272be50300ec8ce8a src/web_server/fastcgi/protocol.rs 1af493d339623045869226af7ab927e5f4b8ee2f4760ffe28b2a65af8041312b src/web_server/fastcgi/protocol.rs
134c4998f6a0aa27e121af1ae3355b7e335488b2ae867ac0a934c58b4eb77191 src/web_server/fastcgi/response.rs 134c4998f6a0aa27e121af1ae3355b7e335488b2ae867ac0a934c58b4eb77191 src/web_server/fastcgi/response.rs
8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs 8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs
0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs 0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs
@@ -447,14 +451,14 @@ fae5a47efda9a4ae4dc94711ecc5857e10b3c20aa2bf2c742c59651a85b9d978 src/vhost/test
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs 80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs 67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
d0c5802ed8b1d6248c3fe75f3c1853a649f2407324ec802cd1f066720c5468de src/web_server/response/file.rs fbc170c464ac6ae96113bfb667184008461016845086a0dece17f4dc99f8354f src/web_server/response/file.rs
9aa68251a5f549f4ce3b1bda88d4abb9efc629caf444989381b4117c9488e37b src/web_server/response/headers.rs 9aa68251a5f549f4ce3b1bda88d4abb9efc629caf444989381b4117c9488e37b src/web_server/response/headers.rs
2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs 2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs 400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs 3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs
cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs
c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs
f9135f6a387b9b163a96f8b3d7b3da75f13b4f3795be66c0ea7609912816221c src/web_server/tests/part_02.rs a62665d7d1d8305cc1a6bca9180b0584c8cd88ca4a6b862190c5c5953ccdbc52 src/web_server/tests/part_02.rs
e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs 1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
+1 -1
View File
@@ -1,6 +1,6 @@
# Debian packaging # Debian packaging
This directory builds the `rustproxy` binary package for RustProxy 0.25.2. This directory builds the `rustproxy` binary package for RustProxy 0.25.3.
The package installs: The package installs:
- `/usr/bin/rust-proxy` - `/usr/bin/rust-proxy`
+1 -1
View File
@@ -21,6 +21,6 @@ cargo fetch --locked
dpkg-buildpackage -us -uc -b dpkg-buildpackage -us -uc -b
mkdir -p dist mkdir -p dist
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.25.2_*.deb' -o -name 'rustproxy_0.25.2_*.changes' -o -name 'rustproxy_0.25.2_*.buildinfo' \) \ find .. -maxdepth 1 -type f \( -name 'rustproxy_0.25.3_*.deb' -o -name 'rustproxy_0.25.3_*.changes' -o -name 'rustproxy_0.25.3_*.buildinfo' \) \
-exec cp -f {} dist/ \; -exec cp -f {} dist/ \;
echo "Packages copied to $ROOT/dist" echo "Packages copied to $ROOT/dist"
+8
View File
@@ -1,3 +1,11 @@
rustproxy (0.25.3) stable; urgency=medium
* Add vhost/location HTTP method policy and location-level ACL/country controls.
* Forward hashed request IDs to origins for end-to-end correlation.
* Retain bounded manual-configuration history with a calendar-style dashboard viewer.
-- RustProxy Release <release@localhost> Sun, 04 Oct 2026 22:18:00 +0200
rustproxy (0.25.2) stable; urgency=medium rustproxy (0.25.2) stable; urgency=medium
* Allow consistent encrypted backup creation while the service is running. * Allow consistent encrypted backup creation while the service is running.
+2 -2
View File
@@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi
case "$mode" in case "$mode" in
help|-h|--help) help|-h|--help)
cat <<'HELP' cat <<'HELP'
RustProxy 0.25.2 RustProxy 0.25.3
./dev.sh Build and start proxy + local panel ./dev.sh Build and start proxy + local panel
./dev.sh run [serve flags] Same, with extra server options ./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 ./dev.sh admin setup-token Generate/print a one-time setup capability before first login
@@ -139,7 +139,7 @@ if [[ "$mode" == "test" ]]; then
printf 'Checking all Rust targets before tests...\n' >&2 printf 'Checking all Rust targets before tests...\n' >&2
cargo check --locked --all-targets cargo check --locked --all-targets
fi fi
printf 'Building RustProxy 0.25.2...\n' >&2 printf 'Building RustProxy 0.25.3...\n' >&2
cargo build --locked cargo build --locked
BIN="$CARGO_TARGET_DIR/debug/rust-proxy" BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
case "$mode" in case "$mode" in
+1 -1
View File
@@ -1,6 +1,6 @@
package main package main
const version = "0.25.2" const version = "0.25.3"
type config struct { type config struct {
Listen string Listen string
+4 -1
View File
@@ -65,6 +65,7 @@ framework extraction failures are not guaranteed to have that JSON shape.
|---|---| |---|---|
| `GET /healthz` | Public process health/version; not a backend health probe | | `GET /healthz` | Public process health/version; not a backend health probe |
| `GET /api/config` | `{revision, config}` | | `GET /api/config` | `{revision, config}` |
| `GET /api/config/history` | Retained revision metadata; `?revision=N` returns that document plus the previous retained document |
| `PUT /api/config` | Input `{expected_revision, config}`; returns the applied envelope; retained for atomic multi-section changes | | `PUT /api/config` | Input `{expected_revision, config}`; returns the applied envelope; retained for atomic multi-section changes |
| `PUT /api/config/{section}` | Input `{expected_revision, value}`; replaces one validated section and returns the applied envelope | | `PUT /api/config/{section}` | Input `{expected_revision, value}`; replaces one validated section and returns the applied envelope |
| `GET /api/backends` | Authenticated per-vhost backend observations and active/desired frontend plans | | `GET /api/backends` | Authenticated per-vhost backend observations and active/desired frontend plans |
@@ -86,6 +87,8 @@ and rechecks session validity. It does not queue an unlimited per-viewer event l
or guarantee replay. The browser falls back to polling. An active dashboard renews or guarantee replay. The browser falls back to polling. An active dashboard renews
idle activity, not the absolute session deadline. idle activity, not the absolute session deadline.
Configuration history retains the newest 512 successful manual/configuration revisions in SQLite schema 8. `GET /api/config/history` is session-protected like the rest of the administration API; `limit` is bounded to 512 and `revision` selects one retained revision for diff/inspection.
Configuration updates return 409 for a stale revision, 422 for validation failure Configuration updates return 409 for a stale revision, 422 for validation failure
and 429 when the eight-operation update admission bound is reached. Persistence and and 429 when the eight-operation update admission bound is reached. Persistence and
snapshot publication finish before a successful response. Disconnecting the caller snapshot publication finish before a successful response. Disconnecting the caller
@@ -475,7 +478,7 @@ when enabled. Transformed cached representations add the `transformed` Cache-Sta
extension parameter. `X-Cache-Status` is stripped if supplied by an origin but is not extension parameter. `X-Cache-Status` is stripped if supplied by an origin but is not
emitted by RustProxy. emitted by RustProxy.
`diagnostic_headers.hide_mock_origin` is removed and old documents containing it are rejected on `diagnostic_headers.hide_mock_origin` is removed and old documents containing it are rejected on
export; `request_id` is the only diagnostic-header switch. No revision response-header behavior remains. TLS listener export; `request_id` is the only diagnostic-header switch. The generated request ID is a 64-hex-character SHA-256 value and the same protected value is forwarded to the origin as `X-Request-ID`. No revision response-header behavior remains. TLS listener
http3 omission is default-on; explicit null is persisted off. Legacy listeners use http3 omission is default-on; explicit null is persisted off. Legacy listeners use
https_http3 with identical default/null behavior. https_http3 with identical default/null behavior.
+1 -1
View File
@@ -1,4 +1,4 @@
# Administration API access - 0.25.2 # Administration API access - 0.25.3
## Sign-in boundary ## Sign-in boundary
+4 -2
View File
@@ -91,9 +91,11 @@ Hop-by-hop/framing headers, forwarding identity headers, RustProxy request IDs a
## Access control ## Access control
A location can define its own IPv4/IPv6 CIDR ACL. The location ACL is additive: it cannot override a deny enforced by the host policy, reusable ACL profile or vhost ACL. A location can define its own IPv4/IPv6 CIDR ACL, select a reusable ACL profile, add a location-specific country block/allow-only rule, and configure an HTTP method policy (`off`, `allow` or `block`). Method names are normalized to uppercase and validated as HTTP method tokens.
Authentication, vhost rate limits, country policy, security headers, compression, custom error pages and WebSocket policy still apply through the normal vhost pipeline. All access layers are additive. The request must pass the vhost method policy, location method policy, host/profile/vhost IP ACLs, location profile ACL, inline location ACL and every applicable country policy. A location therefore cannot override a denial from its vhost. Method denial returns `405 Method Not Allowed` (with `Allow` for allow-list mode); IP/country denials retain the normal `403` pipeline. These checks occur before backend selection/connection.
Authentication, vhost rate limits, security headers, compression, custom error pages and WebSocket policy still apply through the normal vhost pipeline.
## What is intentionally not an Nginx parser ## What is intentionally not an Nginx parser
+1 -1
View File
@@ -1,4 +1,4 @@
# Installing and updating RustProxy 0.25.2 # Installing and updating RustProxy 0.25.3
The scripts prepare a Linux/systemd installation. This source archive is **not a The scripts prepare a Linux/systemd installation. This source archive is **not a
verified production build**: Rust/Cargo, real CA operations, live systemd startup verified production build**: Rust/Cargo, real CA operations, live systemd startup
+1 -1
View File
@@ -15,7 +15,7 @@ The CLI first probes the loopback administration `/healthz` endpoint over HTTP.
This fixes the previous reconnect loop where `--livelogs` treated the TLS listener's expected HTTP-to-HTTPS `308` response as a stream failure. The same local transport selection is used by CLI config and certificate API operations. This fixes the previous reconnect loop where `--livelogs` treated the TLS listener's expected HTTP-to-HTTPS `308` response as a stream failure. The same local transport selection is used by CLI config and certificate API operations.
The live path is separate from configured file/syslog sinks. Request processing publishes into a bounded Tokio broadcast channel without waiting for viewers. A slow viewer receives an explicit `gap` event with the number of entries lost from that viewer's live queue. The CLI also consults the retained history after connecting/reconnecting and de-duplicates entries by request id. The live path is separate from configured file/syslog sinks. Request processing publishes into a bounded Tokio broadcast channel without waiting for viewers. A slow viewer receives an explicit `gap` event with the number of entries lost from that viewer's live queue. The CLI also consults the retained history after connecting/reconnecting and de-duplicates entries by request id. Each request now carries a fresh 64-hex-character SHA-256 `request_id`; the same protected value is forwarded to the backend as `X-Request-ID` for correlation.
## User-Agent, country and Traffic analytics ## User-Agent, country and Traffic analytics
+6
View File
@@ -1,5 +1,11 @@
# Deployment and configuration migration history # Deployment and configuration migration history
## 0.25.2 -> 0.25.3
SQLite schema 7 is upgraded automatically to schema 8. The migration adds `configuration_history`; on first open the current revision is seeded into history, and subsequent successful configuration saves retain the newest 512 revisions for the dashboard revision-history view. No manual SQL is required.
New vhost/location access fields are additive. Missing `method_policy`, location `acl_profile_id`, location country policy and location inline ACL fields retain the prior behavior. RustProxy-generated request IDs are now SHA-256 values and are forwarded to origins as protected `X-Request-ID`; custom request-header rules cannot replace that correlation identifier.
## 0.25.1 -> 0.25.2 ## 0.25.1 -> 0.25.2
There is no SQLite schema or saved-configuration migration. Full backup creation may now run while the service is active; restore still requires RustProxy to be stopped. Backup archive version 2 preserves validated internal relative symlinks such as Certbot `live/` entries pointing into the profile `archive/` tree. Existing archive-version-1 backups remain accepted for restore. There is no SQLite schema or saved-configuration migration. Full backup creation may now run while the service is active; restore still requires RustProxy to be stopped. Backup archive version 2 preserves validated internal relative symlinks such as Certbot `live/` entries pointing into the profile `archive/` tree. Existing archive-version-1 backups remain accepted for restore.
+3 -2
View File
@@ -8,7 +8,8 @@ This is the canonical index for the current project documentation. The authentic
- [ROADMAP](../ROADMAP.md) - planned and completed release scope. - [ROADMAP](../ROADMAP.md) - planned and completed release scope.
- [CHANGELOG](../CHANGELOG.md) - user/operator-visible changes by release. - [CHANGELOG](../CHANGELOG.md) - user/operator-visible changes by release.
- [TODO](TODO.md) - open work only; completed work is not retained as TODO. - [TODO](TODO.md) - open work only; completed work is not retained as TODO.
- [0.25.2 release audit](V0.25.2_AUDIT.md) - live backup, internal symlink and resumed-origin recovery scope plus release gates. - [0.25.3 release audit](V0.25.3_AUDIT.md) - request-method policy, location ACL/country controls, hashed request IDs and configuration-history scope plus release gates.
- [0.25.2 release audit](V0.25.2_AUDIT.md) - historical live backup, internal symlink and resumed-origin recovery scope.
- [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards. - [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 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.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.
@@ -57,7 +58,7 @@ This is the canonical index for the current project documentation. The authentic
## Historical or version-labelled context ## Historical or version-labelled context
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone; they are not current-release documentation. The current audit is [V0.25.2_AUDIT.md](V0.25.2_AUDIT.md); [V0.25.1_AUDIT.md](V0.25.1_AUDIT.md) records the prior Traffic/module-split milestone and [V0.25.0_AUDIT.md](V0.25.0_AUDIT.md) remains the Web Server foundation audit. Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone; they are not current-release documentation. The current audit is [V0.25.3_AUDIT.md](V0.25.3_AUDIT.md); [V0.25.2_AUDIT.md](V0.25.2_AUDIT.md) records the prior backup/session hardening milestone and [V0.25.1_AUDIT.md](V0.25.1_AUDIT.md) records the prior Traffic/module-split milestone and [V0.25.0_AUDIT.md](V0.25.0_AUDIT.md) remains the Web Server foundation audit.
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above. 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.
+1
View File
@@ -35,6 +35,7 @@ This is a settings fragment, not a replacement for the existing routing/vhost do
A complete opt-in example is in `examples/cache-dashboard.json`. A complete opt-in example is in `examples/cache-dashboard.json`.
The dashboard exposes the complete raw JSON editor under **Settings -> Manual config**. The dashboard exposes the complete raw JSON editor under **Settings -> Manual config**.
The same card exposes **Revision history**, which reads the newest 512 retained configuration revisions from SQLite schema 8 and groups them by month/day. Selecting a revision shows timestamp, actor and a bounded path-level before/after diff against the previous retained revision.
The historical `/configuration` browser route is retained only as a compatibility alias and The historical `/configuration` browser route is retained only as a compatibility alias and
opens that Settings block; it is no longer a primary-navigation item. Prefer the structured opens that Settings block; it is no longer a primary-navigation item. Prefer the structured
section editors for routine changes so unrelated sections are not republished. section editors for routine changes so unrelated sections are not republished.
+10 -3
View File
@@ -1,11 +1,16 @@
# Verification report - RustProxy 0.25.2 # Verification report - RustProxy 0.25.3
## Scope ## Scope
RustProxy 0.25.2 is a focused live-backup, backup-symlink and resumed-admin-session hardening release on top of the 0.25.1 source tree. Web Services are independent configuration resources; vhosts and custom locations only choose a source type and reference a Web Service ID. The current development tree provides static GET/HEAD streaming, directory/index behavior, validators, single ranges, document-root confinement, ordered `try_files`, Web Service rewrites, a bounded migration-oriented `.htaccess` compatibility layer, expanded MIME policy, per-extension/MIME Cache-Control rules, fixed source headers, precompressed sidecars, forced-download extensions and optional bounded FastCGI/PHP-FPM. The Web Service editor is organized into General, Routing, .htaccess, MIME, Cache & headers, Delivery, PHP-FPM and Security tabs, with one-shot presets for Static/SPA/PHP/WordPress/Drupal/Laravel/Symfony/Joomla/PrestaShop/Downloads. Existing 0.25 work also includes repeatable directory listing, Web Server vhost-wide metrics, Logging sub-tabs, corrected structured-log source validation, bounded per-vhost Traffic analytics with effective-client-IP rankings/donut charts, User-Agent/optional-country logging, stable Web Service rendering after fast refreshes, generic/customizable 403 handling and automatic RSS relief for reclaimable allocator/HOT-cache memory. The `/docs` surface separates the Markdown Reader from API/OpenAPI and documents the implemented PHP-FPM runtime. RustProxy 0.25.3 adds vhost/location HTTP method policy, location-scoped ACL-profile/IP/country controls, hashed end-to-end request IDs and retained manual-configuration history on top of 0.25.2. SQLite advances from schema 7 to schema 8 through an additive `configuration_history` migration; existing configuration remains compatible and new policy fields default to disabled/inherited behavior. The dashboard adds a calendar-style revision-history dialog without changing the existing Manual config editor workflow.
## Source contracts reviewed ## Source contracts reviewed
- Vhost and location `method_policy` values validate and normalize HTTP methods; host and selected-location policies are cumulative and deny with 405 before upstream work. Allow-list denial includes `Allow`.
- Custom locations can select an `acl_profile_id` and add inline IP ACL plus country access policy. Profile and inline rules are compiled with the route and checked before authentication/backend selection; a location cannot weaken a vhost denial.
- Every observed exchange receives a fresh 64-hex SHA-256 `request_id`, overwrites inbound `X-Request-ID` before forwarding, and reuses the same value for diagnostics/logging. Custom header rules continue to protect `X-Request-ID`.
- SQLite schema 8 stores revision/time/actor/document in bounded `configuration_history`; successful CAS writes record history transactionally and retain the newest 512 revisions. The session-protected `/api/config/history` API supports metadata listing and one-revision detail with the previous retained document.
- The Manual config history UI groups retained revisions by month/day, lists revisions for a selected day and renders a bounded recursive path-level before/after diff using text-only DOM operations.
- `Config.web_services` is a separate persisted collection; vhost/location `source` defaults to Reverse Proxy for old documents and Web Server stores only `web_service_id`. - `Config.web_services` is a separate persisted collection; vhost/location `source` defaults to Reverse Proxy for old documents and Web Server stores only `web_service_id`.
- Full configuration compilation validates Web Service IDs, absolute/existing document roots, index names, duplicate IDs and every active Web Service reference. A pure static vhost does not require a backend pool; mixed Web Server and Reverse Proxy locations do. - Full configuration compilation validates Web Service IDs, absolute/existing document roots, index names, duplicate IDs and every active Web Service reference. A pure static vhost does not require a backend pool; mixed Web Server and Reverse Proxy locations do.
- Web Server dispatch occurs after normal frontend routing, ACL and Basic/Tinyauth authorization and before backend selection/DNS/cache/upstream work. Static responses continue through the existing custom error-page, response-header/security, compression and transport pipeline. - Web Server dispatch occurs after normal frontend routing, ACL and Basic/Tinyauth authorization and before backend selection/DNS/cache/upstream work. Static responses continue through the existing custom error-page, response-header/security, compression and transport pipeline.
@@ -44,10 +49,12 @@ RustProxy 0.25.2 is a focused live-backup, backup-symlink and resumed-admin-sess
## Implemented automated coverage ## Implemented automated coverage
Rust unit tests in `src/web_server/tests.rs` cover GET/HEAD/index/slash redirect, MIME/length, validators, valid/invalid range handling, If-Range, escaped/repeated directory listings, per-directory index-before-listing fallback, generic 403/HEAD behavior, MIME overrides/default MIME, cache rules, source headers, `try_files`, rewrite redirect/internal behavior, `.htaccess` WordPress-style front-controller routing, nested DirectoryIndex/MIME/Header/Expires behavior, strict-mode rejection and PHP-source protection, hardening probe/method blocking and fingerprint-header stripping, precompressed representations/ranges, download headers, PHP FastCGI fixture execution/failure-source-disclosure behavior, request-body 413 enforcement, custom FastCGI/PHP_VALUE validation and reserved-parameter rejection, direct traversal rejection and Unix/Linux symlink policy. `src/traffic/tests.rs` includes bounded-table and effective-client-IP view coverage. Configuration tests cover pure static vhosts without a pool, mixed Web Server/Reverse Proxy routing, reuse of one Web Service by multiple vhosts, unknown service references and backward-compatible default source behavior. ACL tests include encoded traversal/separator/malformed-encoding cases. Rust unit tests in `src/web_server/tests.rs` cover GET/HEAD/index/slash redirect, MIME/length, validators, valid/invalid range handling, If-Range, escaped/repeated directory listings, per-directory index-before-listing fallback, generic 403/HEAD behavior, MIME overrides/default MIME, cache rules, source headers, `try_files`, rewrite redirect/internal behavior, `.htaccess` WordPress-style front-controller routing, nested DirectoryIndex/MIME/Header/Expires behavior, strict-mode rejection and PHP-source protection, hardening probe/method blocking and fingerprint-header stripping, precompressed representations/ranges, download headers, PHP FastCGI fixture execution/failure-source-disclosure behavior, request-body 413 enforcement, custom FastCGI/PHP_VALUE validation and reserved-parameter rejection, direct traversal rejection and Unix/Linux symlink policy. `src/traffic/tests.rs` includes bounded-table and effective-client-IP view coverage. Configuration tests cover pure static vhosts without a pool, mixed Web Server/Reverse Proxy routing, reuse of one Web Service by multiple vhosts, unknown service references and backward-compatible default source behavior. Vhost tests cover method-policy normalization/allow/block behavior, and database tests cover retained configuration revision documents, actor metadata and previous-revision lookup. ACL tests include encoded traversal/separator/malformed-encoding cases.
Rust-independent release checks additionally validate UI/JSON/language/source contracts and reproducible packaging. Rust-independent release checks additionally validate UI/JSON/language/source contracts and reproducible packaging.
A compiler-enabled external build subsequently exposed one module-privacy error in the new history split (`CONFIG_HISTORY_CAPACITY` was private to `db::history`) and one unused `country_resolved` assignment in the location GeoIP branch. The source package now exposes the capacity to its parent module with `pub(super)`, references it through `history::CONFIG_HISTORY_CAPACITY`, and removes the dead assignment. This workspace still has no Rust/Cargo toolchain, so compiler confirmation of the corrected tree remains a release-host gate.
### Modular source-tree verification ### Modular source-tree verification
- `python3 scripts/rust_module_contracts.py`: PASS. Every repository `.rs` file is at most 500 lines. - `python3 scripts/rust_module_contracts.py`: PASS. Every repository `.rs` file is at most 500 lines.
+29
View File
@@ -0,0 +1,29 @@
# RustProxy 0.25.3 release audit
RustProxy 0.25.3 adds request-method policy, location-scoped access controls, end-to-end request correlation and retained manual-configuration history. SQLite moves from schema 7 to schema 8 through an additive migration.
## Access policy
| Requirement | Status | Evidence |
| --- | --- | --- |
| Allow/block HTTP methods per vhost | IMPLEMENTED | `Vhost.method_policy` validates normalized HTTP methods; the request path returns 405 before origin work when the host policy denies a method. |
| Allow/block HTTP methods per Custom location | IMPLEMENTED | `PathRoute.method_policy` is compiled and enforced after route selection and before authentication/upstream selection. |
| Reuse ACL profiles per Custom location | IMPLEMENTED | Locations accept `acl_profile_id`; profile IP and country policies are resolved during configuration compilation. |
| Add inline location IP/country policy | IMPLEMENTED | Location inline CIDR ACL and country allow/block policy compose with vhost/profile policy and fail before origin work. |
| Preserve additive security semantics | IMPLEMENTED | A location cannot weaken a denial made by vhost/profile policy; all applicable policies must allow the request. |
## Request correlation
Each accepted exchange receives a fresh 64-hex-character SHA-256 request identifier derived from an internal sequence plus cryptographic OS randomness. RustProxy overwrites the protected `X-Request-ID` request header with that value before origin forwarding. The same value is available to diagnostic response headers when enabled and to access logging as `request_id`.
## Manual configuration history
SQLite schema 8 adds `configuration_history`. On upgrade, the current configuration is seeded if no history row exists. Each successful CAS configuration replacement records revision, timestamp, actor and document in the same transaction and trims retained history to the newest 512 rows.
The authenticated `GET /api/config/history` endpoint returns retained revision metadata or a selected revision with its previous retained document. The Manual config dashboard provides a revision-history dialog grouped by month/day; selecting a revision renders actor/time metadata and bounded path-level before/after changes. The endpoint uses the existing administrator session guard.
## Compatibility and release gates
Missing new policy fields deserialize to disabled/default behavior. Existing schema-7 databases upgrade automatically. Old binaries may reject a configuration saved with new fields, so rollback should restore both the old source and the pre-upgrade data backup.
Rust-independent source, documentation, language, UI and release-smoke checks are part of the release tree. A follow-up compiler run outside this packaging workspace found and prompted correction of the history-capacity module visibility plus a dead location GeoIP assignment; the corrected source uses `history::CONFIG_HISTORY_CAPACITY` with parent-module visibility and no redundant assignment. Compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` remain mandatory on a release host with Rust installed.
+7 -6
View File
@@ -128,15 +128,16 @@ still validated, but create no pool or additional active listener.
## Paths, policies and migration ## Paths, policies and migration
`routes` contains per-vhost paths: `id`, `path_prefix`, `enabled`, `source`, nullable `cache`, `routes` contains per-vhost paths: `id`, match fields, `enabled`, `source`, nullable `cache`,
optional `acl`, `headers` and `backend_id`. `source` selects Reverse Proxy or a reusable Web Service; optional `acl_profile_id`, inline `acl`, `country_access`, `method_policy`, `headers` and `backend_id`. `source` selects Reverse Proxy or a reusable Web Service;
`backend_id` is valid only for Reverse Proxy. An empty array leaves the main vhost source as the fallback. Longest `backend_id` is valid only for Reverse Proxy. An empty array leaves the main vhost source as the fallback. Longest
segment-boundary prefix wins. Origins receive the original path/query; prefixes segment-boundary prefix wins. Origins receive the original path/query; prefixes
are not stripped. IDs must use ASCII letters, digits, hyphens and underscores. are not stripped. IDs must use ASCII letters, digits, hyphens and underscores.
Host-policy, reusable ACL-profile, vhost and route ACLs are ANDed before pool Host method policy, reusable ACL profiles, vhost ACL/country policy and route method/profile/inline
selection/DNS/cache. A vhost can attach one reusable `acl_profile_id`; the profile may ACL/country policies are cumulative before pool selection/DNS/cache. A vhost and each route can
contain an IP ACL plus a country block/allow-only rule. Inline vhost/location controls attach a reusable `acl_profile_id`; profiles may contain an IP ACL plus a country block/allow-only
remain independent and cannot be weakened by the profile. Their client identity is the rule. Inline controls remain independent and cannot be weakened by a profile. Method-policy denial
returns 405; allow-list mode also emits `Allow`. Their client identity is the
validated effective peer address, not an untrusted forwarding header. validated effective peer address, not an untrusted forwarding header.
There is no legacy routing UI/runtime or automatic importer in 0.8.0. Development There is no legacy routing UI/runtime or automatic importer in 0.8.0. Development
+9 -1
View File
@@ -89,6 +89,12 @@ blocking that address for the rest of the window. Busy hashing returns 503; a
blocked address returns 429 with Retry-After. This is abuse resistance, not MFA or blocked address returns 429 with Retry-After. This is abuse resistance, not MFA or
a replacement for perimeter rate limiting, especially for many clients behind NAT. a replacement for perimeter rate limiting, especially for many clients behind NAT.
## HTTP method policy
Each vhost can leave method filtering **Off**, define an **Allow only** list, or define a **Block** list. Methods are validated as HTTP method tokens and normalized to uppercase. A denied request returns `405 Method Not Allowed` before authentication, cache, DNS or origin connection; allow-list mode also returns an `Allow` header containing the configured methods.
A Custom location has the same independent method policy. Vhost and location policies are cumulative: a location cannot re-enable a method rejected by its vhost. This makes patterns such as `GET, HEAD, POST` allow-only at host level and a stricter `GET, HEAD` allow-only policy on a read-only location explicit and fail-closed.
## Custom locations ## Custom locations
The **Custom locations** tab supports ordinary prefix, priority prefix (`^~`), exact (`=`), The **Custom locations** tab supports ordinary prefix, priority prefix (`^~`), exact (`=`),
@@ -97,7 +103,9 @@ a selected priority prefix skips regex evaluation; regexes are tested in declara
longest ordinary prefix is the fallback. Exact paths preserve a meaningful trailing slash. longest ordinary prefix is the fallback. Exact paths preserve a meaningful trailing slash.
A proxy location can use the normal pool, pin one backend, inherit/override cache behavior and A proxy location can use the normal pool, pin one backend, inherit/override cache behavior and
apply an additional ACL. Path handling can preserve the public URL, strip a prefix, replace a apply an additional inline ACL. It can also select a reusable ACL profile, apply its own country
allow/block policy and define an HTTP method allow/block policy. These controls are cumulative with
the vhost policies and are enforced before a backend connection is opened. Path handling can preserve the public URL, strip a prefix, replace a
prefix or perform a capture-based regex rewrite. Query parameters are preserved. Repeated literal prefix or perform a capture-based regex rewrite. Query parameters are preserved. Repeated literal
slashes are normalized only for routing/ACL decisions; without an explicit rewrite the backend slashes are normalized only for routing/ACL decisions; without an explicit rewrite the backend
receives the original target, including `//`. Encoded separators, percent double-decoding and dot receives the original target, including `//`. Encoded separators, percent double-decoding and dot
+1 -1
View File
@@ -3672,7 +3672,7 @@
}, },
"timepicker": {}, "timepicker": {},
"timezone": "browser", "timezone": "browser",
"title": "RustProxy 0.25.2 - Operations", "title": "RustProxy 0.25.3 - Operations",
"uid": "rustproxy-0170", "uid": "rustproxy-0170",
"version": 2, "version": 2,
"weekStart": "" "weekStart": ""
+31 -1
View File
@@ -1913,6 +1913,36 @@
"trafficTabClients": "Clients", "trafficTabClients": "Clients",
"trafficTabRequests": "Requests", "trafficTabRequests": "Requests",
"trafficTabDelivery": "Delivery", "trafficTabDelivery": "Delivery",
"trafficVolume": "Traffic volume" "trafficVolume": "Traffic volume",
"vhMethodMode": "HTTP method policy",
"vhMethodList": "HTTP methods (one per line)",
"vhMethodHelp": "Allow only listed methods or block listed methods for this vhost. The policy stacks with location rules.",
"locationAclProfile": "ACL profile",
"locationMethodMode": "HTTP method policy",
"locationMethodList": "HTTP methods (one per line)",
"locationMethodAllow": "Allow only",
"locationMethodBlock": "Block listed",
"locationMethodHelp": "The location policy stacks with the vhost method policy. Example: GET, HEAD and POST.",
"locationCountryMode": "Country access policy",
"locationCountryList": "Country codes (one per line)",
"locationCountryHelp": "Location country policy stacks with vhost/profile policy. GeoIP is resolved only when required.",
"configHistory": "Revision history",
"configHistoryRetention": "SQLITE / LAST 512 REVISIONS",
"configHistoryHelp": "Select a marked day to inspect retained configuration revisions and their changes. History retention starts with 0.25.3.",
"configHistoryChanges": "CHANGES",
"configHistorySelectDay": "Select a day.",
"configHistoryRevision": "Revision",
"configHistoryRevisionShort": "rev",
"configHistoryBefore": "Before",
"configHistoryAfter": "After",
"configHistoryBaseline": "This is the oldest retained baseline; no earlier revision is available for comparison.",
"configHistoryNoChanges": "No configuration differences were found.",
"configHistoryDiffLimit": "Diff truncated after 200 changed paths.",
"configHistoryMissing": "(not present)",
"configHistoryEntries": "revisions",
"configHistorySummary": "{count} retained revisions",
"configHistoryEmpty": "No retained revisions.",
"configHistoryLoading": "Loading revision history...",
"configHistoryFailed": "Could not load revision history."
} }
} }
+31 -1
View File
@@ -1913,6 +1913,36 @@
"trafficTabClients": "Klienci", "trafficTabClients": "Klienci",
"trafficTabRequests": "Żądania", "trafficTabRequests": "Żądania",
"trafficTabDelivery": "Obsługa odpowiedzi", "trafficTabDelivery": "Obsługa odpowiedzi",
"trafficVolume": "Wolumen ruchu" "trafficVolume": "Wolumen ruchu",
"vhMethodMode": "Polityka metod HTTP",
"vhMethodList": "Metody HTTP (jedna na linię)",
"vhMethodHelp": "Zezwalaj tylko na podane metody albo blokuj podane metody dla tego vhosta. Reguła łączy się z polityką lokalizacji.",
"locationAclProfile": "Profil ACL",
"locationMethodMode": "Polityka metod HTTP",
"locationMethodList": "Metody HTTP (jedna na linię)",
"locationMethodAllow": "Zezwalaj tylko",
"locationMethodBlock": "Blokuj wskazane",
"locationMethodHelp": "Polityka lokalizacji łączy się z polityką vhosta. Przykład: GET, HEAD i POST.",
"locationCountryMode": "Polityka dostępu krajów",
"locationCountryList": "Kody krajów (jeden na linię)",
"locationCountryHelp": "Polityka krajów lokalizacji łączy się z polityką vhosta/profilu. GeoIP jest odczytywany tylko, gdy jest potrzebny.",
"configHistory": "Historia rewizji",
"configHistoryRetention": "SQLITE / OSTATNIE 512 REWIZJI",
"configHistoryHelp": "Wybierz oznaczony dzień, aby zobaczyć zachowane rewizje konfiguracji i ich zmiany. Historia jest zapisywana od wersji 0.25.3.",
"configHistoryChanges": "ZMIANY",
"configHistorySelectDay": "Wybierz dzień.",
"configHistoryRevision": "Rewizja",
"configHistoryRevisionShort": "rew.",
"configHistoryBefore": "Przed",
"configHistoryAfter": "Po",
"configHistoryBaseline": "To najstarsza zachowana baza; wcześniejsza rewizja nie jest dostępna do porównania.",
"configHistoryNoChanges": "Nie znaleziono różnic w konfiguracji.",
"configHistoryDiffLimit": "Podgląd różnic ograniczono do 200 zmienionych ścieżek.",
"configHistoryMissing": "(brak)",
"configHistoryEntries": "rewizji",
"configHistorySummary": "Zachowane rewizje: {count}",
"configHistoryEmpty": "Brak zachowanych rewizji.",
"configHistoryLoading": "Wczytywanie historii rewizji...",
"configHistoryFailed": "Nie udało się wczytać historii rewizji."
} }
} }
+1 -1
View File
@@ -88,7 +88,7 @@ def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.25.2.zip') parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.25.3.zip')
output = parser.parse_args().output.resolve() output = parser.parse_args().output.resolve()
paths = files() paths = files()
checksum = ROOT / 'SHA256SUMS' checksum = ROOT / 'SHA256SUMS'
+9
View File
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS configuration_history (
revision INTEGER PRIMARY KEY CHECK (revision >= 0),
time INTEGER NOT NULL,
actor TEXT NOT NULL,
document TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS configuration_history_time
ON configuration_history(time DESC, revision DESC);
+14 -13
View File
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
preflight(root) preflight(root)
manifest = tomllib.loads((root / "Cargo.toml").read_text()) manifest = tomllib.loads((root / "Cargo.toml").read_text())
assert manifest["package"]["version"] == "0.25.2" assert manifest["package"]["version"] == "0.25.3"
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12" assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
h3dep = manifest["dependencies"]["h3"] h3dep = manifest["dependencies"]["h3"]
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"] assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
lock_path = root / "Cargo.lock" lock_path = root / "Cargo.lock"
if lock_path.exists(): if lock_path.exists():
lock = lock_path.read_text() lock = lock_path.read_text()
assert 'name = "rust-proxy"\nversion = "0.25.2"' in lock assert 'name = "rust-proxy"\nversion = "0.25.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" assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
for release_doc, marker in { for release_doc, marker in {
"README.md": "# RustProxy 0.25.2", "README.md": "# RustProxy 0.25.3",
"CHANGELOG.md": "## 0.25.2", "CHANGELOG.md": "## 0.25.3",
"ROADMAP.md": "## 0.25.2", "ROADMAP.md": "## 0.25.3",
"AGENTS.md": "Current source version: 0.25.2.", "AGENTS.md": "Current source version: 0.25.3.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.25.2", "docs/TEST_REPORT.md": "# Verification report - RustProxy 0.25.3",
}.items(): }.items():
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}" assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable" assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
@@ -224,7 +224,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
assert '"StreamMetrics"' in openapi and '"streams"' in openapi assert '"StreamMetrics"' in openapi and '"streams"' in openapi
for marker in ['"version": "0.25.2"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']: for marker in ['"version": "0.25.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 assert marker in openapi, marker
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']: for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker assert marker in openapi, marker
@@ -565,7 +565,7 @@ assert 'Ok(line) if command_tx.send(line).is_ok()' not in main_source, 'do not m
assert 'Ok(line) => {' in main_source and 'command_tx.send(line).is_err()' in main_source assert 'Ok(line) => {' in main_source and 'command_tx.send(line).is_err()' in main_source
for marker in ['LIVE_STREAM_CAPACITY', 'broadcast::Sender<LogEntry>', 'pub fn subscribe']: for marker in ['LIVE_STREAM_CAPACITY', 'broadcast::Sender<LogEntry>', 'pub fn subscribe']:
assert marker in logging_source, marker assert marker in logging_source, marker
assert 'SCHEMA_VERSION: i64 = 7' 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 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
assert 'load_renewal_observations' not in acme_source and 'renewals.json' not in acme_source assert 'load_renewal_observations' not in acme_source and 'renewals.json' not in acme_source
for marker in ['pub kind: &\'static str', 'pub log_directory: String', 'pub(crate) fn certbot_log_directory', 'kind: "issue"', '"renewal-dry-run"']: for marker in ['pub kind: &\'static str', 'pub log_directory: String', 'pub(crate) fn certbot_log_directory', 'kind: "issue"', '"renewal-dry-run"']:
assert marker in acme_source, marker assert marker in acme_source, marker
@@ -1071,12 +1071,13 @@ connection.executescript((root / "migrations/0004_admin_preferences.sql").read_t
connection.executescript((root / "migrations/0005_certificate_api.sql").read_text()) connection.executescript((root / "migrations/0005_certificate_api.sql").read_text())
connection.executescript((root / "migrations/0006_certificate_api_credentials.sql").read_text()) connection.executescript((root / "migrations/0006_certificate_api_credentials.sql").read_text())
connection.executescript((root / "migrations/0007_secret_store.sql").read_text()) connection.executescript((root / "migrations/0007_secret_store.sql").read_text())
connection.execute("PRAGMA user_version = 7") connection.executescript((root / "migrations/0008_config_history.sql").read_text())
connection.execute("PRAGMA user_version = 8")
assert connection.execute("PRAGMA quick_check").fetchone()[0] == "ok" assert connection.execute("PRAGMA quick_check").fetchone()[0] == "ok"
assert connection.execute("SELECT COUNT(*) FROM auth_throttle WHERE id=1").fetchone()[0] == 1 assert connection.execute("SELECT COUNT(*) FROM auth_throttle WHERE id=1").fetchone()[0] == 1
assert connection.execute("SELECT COUNT(*) FROM certificate_renewals").fetchone()[0] == 0 assert connection.execute("SELECT COUNT(*) FROM certificate_renewals").fetchone()[0] == 0
assert connection.execute("SELECT COUNT(*) FROM admin_preferences").fetchone()[0] == 0 assert connection.execute("SELECT COUNT(*) FROM admin_preferences").fetchone()[0] == 0
assert connection.execute("PRAGMA user_version").fetchone()[0] == 7 assert connection.execute("PRAGMA user_version").fetchone()[0] == 8
connection.close() connection.close()
for marker_0223 in [ for marker_0223 in [
'"/api/certificates/reload"', 'certificate_reload', '"/api/certificates/reload"', 'certificate_reload',
@@ -1158,7 +1159,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241 assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
assert 'location.pathname === "/logging"' in settings_js_0241 assert 'location.pathname === "/logging"' in settings_js_0241
assert '"RuntimeInformation"' in openapi and '"version": "0.25.2"' in openapi assert '"RuntimeInformation"' in openapi and '"version": "0.25.3"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions. # 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs") tls_0241 = rust_source(root, "src/tls.rs")
@@ -1203,5 +1204,5 @@ for schema_name in ["RedirectHost", "NotFoundHost"]:
node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"] node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"]
assert "maxItems" not in node, schema_name assert "maxItems" not in node, schema_name
print("PASS: 0.25.2 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v7 migrations.") print("PASS: 0.25.3 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.") print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
+1 -1
View File
@@ -97,7 +97,7 @@ def digest(path: Path) -> str:
def fetch_https(url: str, max_bytes: int) -> bytes: def fetch_https(url: str, max_bytes: int) -> bytes:
if urllib.parse.urlparse(url).scheme != 'https': if urllib.parse.urlparse(url).scheme != 'https':
raise ValueError('GeoIP downloads require HTTPS.') raise ValueError('GeoIP downloads require HTTPS.')
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.25.2'}) request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.25.3'})
with urllib.request.urlopen(request, timeout=30) as response: with urllib.request.urlopen(request, timeout=30) as response:
if urllib.parse.urlparse(response.geturl()).scheme != 'https': if urllib.parse.urlparse(response.geturl()).scheme != 'https':
raise ValueError('GeoIP download redirected away from HTTPS.') raise ValueError('GeoIP download redirected away from HTTPS.')
+6 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Rust-independent documentation/Reader consistency checks for the 0.25.2 release.""" """Rust-independent documentation/Reader consistency checks for the 0.25.3 release."""
from pathlib import Path from pathlib import Path
import re import re
from rust_source import rust_source from rust_source import rust_source
@@ -14,6 +14,7 @@ required = [
DOCS / "README.md", DOCS / "README.md",
DOCS / "WEB_SERVER.md", DOCS / "WEB_SERVER.md",
DOCS / "TODO.md", DOCS / "TODO.md",
DOCS / "V0.25.3_AUDIT.md",
DOCS / "V0.25.2_AUDIT.md", DOCS / "V0.25.2_AUDIT.md",
DOCS / "V0.25.1_AUDIT.md", DOCS / "V0.25.1_AUDIT.md",
DOCS / "V0.25.0_AUDIT.md", DOCS / "V0.25.0_AUDIT.md",
@@ -25,7 +26,7 @@ for path in required:
assert path.is_file(), f"missing required documentation: {path.relative_to(ROOT)}" assert path.is_file(), f"missing required documentation: {path.relative_to(ROOT)}"
index = (DOCS / "README.md").read_text(encoding="utf-8") index = (DOCS / "README.md").read_text(encoding="utf-8")
for marker in ["Functional documentation", "Development and testing", "Technical documentation", "Historical or version-labelled context", "TODO.md", "V0.25.2_AUDIT.md", "V0.25.1_AUDIT.md", "V0.25.0_AUDIT.md", "CHANGELOG_TECHNICAL_ARCHIVE.md", "ROADMAP_HISTORY.md", "/docs"]: for marker in ["Functional documentation", "Development and testing", "Technical documentation", "Historical or version-labelled context", "TODO.md", "V0.25.3_AUDIT.md", "V0.25.2_AUDIT.md", "V0.25.1_AUDIT.md", "V0.25.0_AUDIT.md", "CHANGELOG_TECHNICAL_ARCHIVE.md", "ROADMAP_HISTORY.md", "/docs"]:
assert marker in index, marker assert marker in index, marker
for document in sorted(DOCS.glob("*.md")): for document in sorted(DOCS.glob("*.md")):
if document.name == "README.md" or (document.name.startswith("V") and document.name.endswith("_AUDIT.md")): if document.name == "README.md" or (document.name.startswith("V") and document.name.endswith("_AUDIT.md")):
@@ -52,13 +53,13 @@ assert '"/docs/content.json"' in admin
readme = (ROOT / "README.md").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8")
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8") roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert readme.startswith("# RustProxy 0.25.2") assert readme.startswith("# RustProxy 0.25.3")
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive" assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
assert roadmap.startswith("# RustProxy 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.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 "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
assert changelog.startswith("# Changelog\n") and "## 0.25.2" in changelog[:800] and "## 0.25.1" in changelog[:1200] assert changelog.startswith("# Changelog\n") and "## 0.25.3" in changelog[:800] and "## 0.25.2" in changelog[:1400]
assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog 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"]: 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}" assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}"
+3 -3
View File
@@ -73,12 +73,12 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
flat = module.parent.with_suffix(".rs") flat = module.parent.with_suffix(".rs")
if flat.is_file(): if flat.is_file():
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}") raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]: 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/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.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", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]:
if not (root / path).is_file(): if not (root / path).is_file():
raise ValueError(f"Incomplete RustProxy 0.25.2 source tree: missing {path}") raise ValueError(f"Incomplete RustProxy 0.25.3 source tree: missing {path}")
text = (root / "src/lib.rs").read_text(encoding="utf-8") text = (root / "src/lib.rs").read_text(encoding="utf-8")
if '#[path = "auth/mod.rs"]' not in text: if '#[path = "auth/mod.rs"]' not in text:
raise ValueError("src/lib.rs is not the 0.25.2 entry point; extract the complete package, not individual files.") raise ValueError("src/lib.rs is not the 0.25.3 entry point; extract the complete package, not individual files.")
return messages return messages
+12 -11
View File
@@ -58,16 +58,17 @@ def sql_and_backup_smoke(tmp: Path) -> None:
) )
connection.execute("PRAGMA user_version = 3") connection.execute("PRAGMA user_version = 3")
connection.commit() connection.commit()
# Simulate additive startup migrations through the current schema-v7. # Simulate additive startup migrations through the current schema-v8.
connection.executescript((ROOT / "migrations/0004_admin_preferences.sql").read_text(encoding="utf-8")) connection.executescript((ROOT / "migrations/0004_admin_preferences.sql").read_text(encoding="utf-8"))
connection.executescript((ROOT / "migrations/0005_certificate_api.sql").read_text(encoding="utf-8")) connection.executescript((ROOT / "migrations/0005_certificate_api.sql").read_text(encoding="utf-8"))
connection.executescript((ROOT / "migrations/0006_certificate_api_credentials.sql").read_text(encoding="utf-8")) connection.executescript((ROOT / "migrations/0006_certificate_api_credentials.sql").read_text(encoding="utf-8"))
connection.executescript((ROOT / "migrations/0007_secret_store.sql").read_text(encoding="utf-8")) connection.executescript((ROOT / "migrations/0007_secret_store.sql").read_text(encoding="utf-8"))
connection.executescript((ROOT / "migrations/0008_config_history.sql").read_text(encoding="utf-8"))
connection.execute( connection.execute(
"INSERT INTO admin_preferences(administrator_id,document) VALUES(1,?)", "INSERT INTO admin_preferences(administrator_id,document) VALUES(1,?)",
(json.dumps({"live_log_columns": ["time", "status"]}),), (json.dumps({"live_log_columns": ["time", "status"]}),),
) )
connection.execute("PRAGMA user_version = 7") connection.execute("PRAGMA user_version = 8")
connection.commit() connection.commit()
assert connection.execute("PRAGMA quick_check").fetchone()[0] == "ok" assert connection.execute("PRAGMA quick_check").fetchone()[0] == "ok"
tables = { tables = {
@@ -76,7 +77,7 @@ def sql_and_backup_smoke(tmp: Path) -> None:
"SELECT name FROM sqlite_master WHERE type='table'" "SELECT name FROM sqlite_master WHERE type='table'"
) )
} }
for table in ("configuration", "administrator", "admin_sessions", "admin_audit", "auth_throttle", "certificate_renewals", "admin_preferences", "certificate_api_credential", "certificate_api_credentials", "secret_store"): for table in ("configuration", "administrator", "admin_sessions", "admin_audit", "auth_throttle", "certificate_renewals", "admin_preferences", "certificate_api_credential", "certificate_api_credentials", "secret_store", "configuration_history"):
assert table in tables, table assert table in tables, table
finally: finally:
connection.close() connection.close()
@@ -87,7 +88,7 @@ def sql_and_backup_smoke(tmp: Path) -> None:
assert copied.is_file() assert copied.is_file()
metadata = json.loads((destination / "backup.json").read_text(encoding="utf-8")) metadata = json.loads((destination / "backup.json").read_text(encoding="utf-8"))
assert metadata["project"] == "RustProxy" assert metadata["project"] == "RustProxy"
assert metadata["schema"] == 7 assert metadata["schema"] == 8
assert metadata["configuration_revision"] == 4 assert metadata["configuration_revision"] == 4
restored = sqlite3.connect(copied) restored = sqlite3.connect(copied)
try: try:
@@ -122,7 +123,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert backup.is_dir() assert backup.is_dir()
assert (backup / "src/obsolete.rs").is_file() assert (backup / "src/obsolete.rs").is_file()
assert not (target / "src/obsolete.rs").exists() assert not (target / "src/obsolete.rs").exists()
assert 'version = "0.25.2"' in (target / "Cargo.toml").read_text(encoding="utf-8") assert 'version = "0.25.3"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n" assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n" assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
assert (target / "langs/site.json").is_file() assert (target / "langs/site.json").is_file()
@@ -133,7 +134,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert (target / "docker-agent/certificate.go").is_file() assert (target / "docker-agent/certificate.go").is_file()
assert (target / "docker-agent/server.go").is_file() assert (target / "docker-agent/server.go").is_file()
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8")) manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
assert manifest["to_version"] == "0.25.2" assert manifest["to_version"] == "0.25.3"
assert manifest["removed_obsolete_runtime_state"] is True assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists() assert not legacy.exists()
assert not (backup / "legacy-data").exists() assert not (backup / "legacy-data").exists()
@@ -544,7 +545,7 @@ def source_contract_smoke() -> None:
assert marker in admin, marker assert marker in admin, marker
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0] anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
assert '"/api/status"' not in anonymous_block assert '"/api/status"' not in anonymous_block
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.25.2"' in openapi_0241 assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.25.3"' in openapi_0241
assert 'id="panel-logging"' not in html assert 'id="panel-logging"' not in html
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']: for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
assert marker in html, marker assert marker in html, marker
@@ -919,7 +920,7 @@ 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 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") db_source = rust_source(ROOT, "src/db.rs")
acme_source = rust_source(ROOT, "src/acme.rs") acme_source = rust_source(ROOT, "src/acme.rs")
assert "SCHEMA_VERSION: i64 = 7" 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 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
assert "load_renewal_observations" not in acme_source and "renewals.json" not in acme_source assert "load_renewal_observations" not in acme_source and "renewals.json" not in acme_source
for marker in ["pub kind: &'static str", "pub log_directory: String", "pub(crate) fn certbot_log_directory", 'kind: "issue"', '"renewal-dry-run"']: for marker in ["pub kind: &'static str", "pub log_directory: String", "pub(crate) fn certbot_log_directory", 'kind: "issue"', '"renewal-dry-run"']:
assert marker in acme_source, marker assert marker in acme_source, marker
@@ -988,7 +989,7 @@ def source_contract_smoke() -> None:
assert "removed_obsolete_runtime_state" in upgrade_source and "legacy-data" not in upgrade_source and "certs_dir.is_symlink()" in upgrade_source assert "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 assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8") openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
for marker in ['"version": "0.25.2"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']: for marker in ['"version": "0.25.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 marker in openapi, marker
assert '"software_update"' in openapi and '"update_available"' in openapi assert '"software_update"' in openapi and '"update_available"' in openapi
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']: for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
@@ -1124,7 +1125,7 @@ def reproducible_archive_smoke(tmp: Path) -> None:
assert f"RustProxy/src/web_server/{module}" in names assert f"RustProxy/src/web_server/{module}" in names
assert "RustProxy/src/web_server.rs" not in names assert "RustProxy/src/web_server.rs" not in names
assert "RustProxy/src/admin/web/web-server.js" in names assert "RustProxy/src/admin/web/web-server.js" in names
for module in ["app/core.js", "app/runtime.js", "app/settings.js", "vhosts/core.js", "vhosts/editor.js", "vhosts/render.js"]: for module in ["app/core.js", "app/config-history.js", "app/runtime.js", "app/settings.js", "vhosts/core.js", "vhosts/editor.js", "vhosts/render.js"]:
assert f"RustProxy/src/admin/web/{module}" in names assert f"RustProxy/src/admin/web/{module}" in names
assert "RustProxy/src/admin/web/app.js" not in names and "RustProxy/src/admin/web/vhosts.js" not in names 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"]: for module in ["model.go", "docker.go", "network.go", "certificate.go", "server.go", "main.go", "main_test.go"]:
@@ -1163,7 +1164,7 @@ def main() -> None:
source_contract_smoke() source_contract_smoke()
container_contract_smoke() container_contract_smoke()
debian_grafana_contract_smoke() debian_grafana_contract_smoke()
print("PASS: 0.25.2 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.") print("PASS: 0.25.3 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
if __name__ == "__main__": if __name__ == "__main__":
+1 -1
View File
@@ -35,7 +35,7 @@ for pattern in ("deploy*.py", "migrate_npm*.py"):
for old in (ROOT / "src/admin/web/app.js", ROOT / "src/admin/web/vhosts.js"): for old in (ROOT / "src/admin/web/app.js", ROOT / "src/admin/web/vhosts.js"):
assert not old.exists(), f"oversized browser monolith returned: {old.relative_to(ROOT)}" assert not old.exists(), f"oversized browser monolith returned: {old.relative_to(ROOT)}"
for required in ( for required in (
"src/admin/web/app/core.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/app/core.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js",
"src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "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/model.go", "docker-agent/docker.go", "docker-agent/network.go",
"docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/certificate.go", "docker-agent/server.go",
+1 -1
View File
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
"""TLS verification uses Python's default trust store; never executes a shell.""" """TLS verification uses Python's default trust store; never executes a shell."""
started = time.monotonic() started = time.monotonic()
opener = build_opener(HttpsRedirects()) opener = build_opener(HttpsRedirects())
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.25.2"}) request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.25.3"})
with opener.open(request, timeout=30) as response, destination.open("wb") as output: with opener.open(request, timeout=30) as response, destination.open("wb") as output:
length = response.headers.get("Content-Length") length = response.headers.get("Content-Length")
if length and int(length) > MAX_ARCHIVE: if length and int(length) > MAX_ARCHIVE:
+3 -3
View File
@@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
for name in MANAGED: for name in MANAGED:
if (target / name).is_symlink(): if (target / name).is_symlink():
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.") raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
backup = target / ".upgrade-backup" / ("before-0.25.2-" + uuid.uuid4().hex[:12]) backup = target / ".upgrade-backup" / ("before-0.25.3-" + uuid.uuid4().hex[:12])
backup.mkdir(parents=True, mode=0o700) backup.mkdir(parents=True, mode=0o700)
installed: list[str] = [] installed: list[str] = []
moved: 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() remove_legacy = legacy.exists()
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()): if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
raise ValueError("Obsolete renewal observation sidecar must be a regular file.") raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.25.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.25.3", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
if remove_legacy: if remove_legacy:
legacy.unlink() legacy.unlink()
except BaseException: except BaseException:
@@ -115,7 +115,7 @@ def main() -> None:
backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir) backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir)
except (OSError, ValueError) as error: except (OSError, ValueError) as error:
parser.exit(2, f"Upgrade refused or rolled back: {error}\n") parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
print(f"Installed RustProxy 0.25.2. Previous managed source files: {backup}") print(f"Installed RustProxy 0.25.3. Previous managed source files: {backup}")
print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.") 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") print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh")
+1 -1
View File
@@ -1,6 +1,6 @@
from pathlib import Path from pathlib import Path
APP_PARTS = ("core.js", "runtime.js", "settings.js") APP_PARTS = ("core.js", "config-history.js", "runtime.js", "settings.js")
VHOST_PARTS = ("core.js", "editor.js", "render.js") VHOST_PARTS = ("core.js", "editor.js", "render.js")
def _read(root: Path, folder: str, parts: tuple[str, ...]) -> str: def _read(root: Path, folder: str, parts: tuple[str, ...]) -> str:
+2 -1
View File
@@ -96,7 +96,7 @@ need(web,
'HeaderValue::from_static("no-store")', 'HeaderValue::from_static("no-store")',
"builtin_error_response(StatusCode::FORBIDDEN, &method)", "builtin_error_response(StatusCode::FORBIDDEN, &method)",
"empty_index_can_go_directly_to_directory_listing", "empty_index_can_go_directly_to_directory_listing",
"RustProxy <b>0.25.2</b>", "RustProxy <b>0.25.3</b>",
"traversal_and_symlink_escape_are_not_served", "traversal_and_symlink_escape_are_not_served",
"try_files_rewrites_cache_rules_headers_and_downloads_work", "try_files_rewrites_cache_rules_headers_and_downloads_work",
"precompressed_sidecars_keep_mime_validators_and_range_semantics", "precompressed_sidecars_keep_mime_validators_and_range_semantics",
@@ -178,6 +178,7 @@ assert "Source" in openapi.get("components", {}).get("schemas", {})
assert "client_ips" in openapi["components"]["schemas"]["TrafficCounters"]["properties"] assert "client_ips" in openapi["components"]["schemas"]["TrafficCounters"]["properties"]
assert (ROOT / "docs/WEB_SERVER.md").is_file() assert (ROOT / "docs/WEB_SERVER.md").is_file()
assert (ROOT / "docs/PHP_FPM.md").is_file() assert (ROOT / "docs/PHP_FPM.md").is_file()
assert (ROOT / "docs/V0.25.3_AUDIT.md").is_file()
assert (ROOT / "docs/V0.25.2_AUDIT.md").is_file() assert (ROOT / "docs/V0.25.2_AUDIT.md").is_file()
assert (ROOT / "docs/V0.25.0_AUDIT.md").is_file() assert (ROOT / "docs/V0.25.0_AUDIT.md").is_file()
assert (ROOT / "docs/TODO.md").is_file() assert (ROOT / "docs/TODO.md").is_file()
+1
View File
@@ -72,6 +72,7 @@ static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
("docs/V0.24.8_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.8_AUDIT.md"))), ("docs/V0.24.8_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.24.8_AUDIT.md"))),
("docs/V0.25.0_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.0_AUDIT.md"))), ("docs/V0.25.0_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.0_AUDIT.md"))),
("docs/V0.25.1_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.1_AUDIT.md"))), ("docs/V0.25.1_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.1_AUDIT.md"))),
("docs/V0.25.3_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.3_AUDIT.md"))),
("docs/V0.25.2_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.2_AUDIT.md"))), ("docs/V0.25.2_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.2_AUDIT.md"))),
("docs/V0.25.0_SECURITY_PREPROD_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.0_SECURITY_PREPROD_AUDIT.md"))), ("docs/V0.25.0_SECURITY_PREPROD_AUDIT.md", "audits", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.25.0_SECURITY_PREPROD_AUDIT.md"))),
("docs/VHOSTS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/VHOSTS.md"))), ("docs/VHOSTS.md", "guides", include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/VHOSTS.md"))),
+164 -4
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3", "openapi": "3.0.3",
"info": { "info": {
"title": "RustProxy Administration API", "title": "RustProxy Administration API",
"version": "0.25.2", "version": "0.25.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." "description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
}, },
"servers": [ "servers": [
@@ -300,6 +300,17 @@
], ],
"nullable": true "nullable": true
}, },
"acl_profile_id": {
"type": "string",
"nullable": true,
"description": "Optional reusable ACL profile; profile and inline location policies are cumulative."
},
"country_access": {
"$ref": "#/components/schemas/CountryAccess"
},
"method_policy": {
"$ref": "#/components/schemas/MethodPolicy"
},
"headers": { "headers": {
"$ref": "#/components/schemas/HeaderRules" "$ref": "#/components/schemas/HeaderRules"
}, },
@@ -420,6 +431,9 @@
], ],
"nullable": true "nullable": true
}, },
"method_policy": {
"$ref": "#/components/schemas/MethodPolicy"
},
"basic_auth": { "basic_auth": {
"type": "object", "type": "object",
"properties": {}, "properties": {},
@@ -1230,6 +1244,56 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
"MethodPolicy": {
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": [
"off",
"allow",
"block"
],
"default": "off"
},
"methods": {
"type": "array",
"maxItems": 32,
"items": {
"type": "string",
"maxLength": 32
},
"default": []
}
},
"additionalProperties": false,
"description": "HTTP method policy. allow permits only listed methods; block rejects listed methods."
},
"CountryAccess": {
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": [
"off",
"block",
"allow"
],
"default": "off"
},
"countries": {
"type": "array",
"maxItems": 128,
"items": {
"type": "string",
"pattern": "^[A-Za-z]{2}$"
},
"default": []
}
},
"additionalProperties": false,
"description": "Country access policy evaluated against the verified effective client IP."
},
"AclProfile": { "AclProfile": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -1248,9 +1312,7 @@
"nullable": true "nullable": true
}, },
"country_access": { "country_access": {
"type": "object", "$ref": "#/components/schemas/CountryAccess"
"properties": {},
"additionalProperties": true
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -9019,6 +9081,104 @@
} }
} }
} }
},
"/api/config/history": {
"get": {
"summary": "Browse retained manual configuration revisions",
"description": "Returns up to 512 retained revision metadata entries, or one revision with its previous retained configuration for diff rendering.",
"tags": [
"Configuration"
],
"operationId": "get_api_config_history",
"parameters": [
{
"name": "revision",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"format": "int64",
"minimum": 0
},
"description": "Return one retained revision and its previous retained configuration."
},
{
"name": "limit",
"in": "query",
"required": false,
"schema": {
"type": "integer",
"minimum": 1,
"maximum": 512,
"default": 512
},
"description": "Maximum metadata entries when revision is omitted."
}
],
"responses": {
"200": {
"description": "Revision history or revision detail",
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": true
}
}
}
},
"401": {
"description": "Authentication required or revoked",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"403": {
"description": "Host, origin or CSRF validation failed",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"429": {
"description": "Bounded worker/session/rate limit",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"503": {
"description": "Control-plane storage/worker unavailable",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "Requested revision is not retained",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
} }
} }
} }
+2
View File
@@ -33,6 +33,7 @@ fn admin_app_javascript() -> &'static str {
static ASSET: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| { static ASSET: std::sync::LazyLock<String> = std::sync::LazyLock::new(|| {
[ [
include_str!("web/app/core.js"), include_str!("web/app/core.js"),
include_str!("web/app/config-history.js"),
include_str!("web/app/runtime.js"), include_str!("web/app/runtime.js"),
include_str!("web/app/settings.js"), include_str!("web/app/settings.js"),
].join("\n") ].join("\n")
@@ -337,6 +338,7 @@ pub fn router(state: Arc<App>) -> Router {
) )
.route("/api/cache/cold/cleanup/{id}", get(cache_cold_cleanup_job)) .route("/api/cache/cold/cleanup/{id}", get(cache_cold_cleanup_job))
.route("/api/config", get(configuration).merge(put(update))) .route("/api/config", get(configuration).merge(put(update)))
.route("/api/config/history", get(config_history))
.route("/api/config/{section}", put(update_section)) .route("/api/config/{section}", put(update_section))
.route("/api/logs", get(logs)) .route("/api/logs", get(logs))
.route("/api/live-logs", get(live_logs_stream)) .route("/api/live-logs", get(live_logs_stream))
+31
View File
@@ -60,6 +60,9 @@ fn status_payload(state: &App) -> Value {
"custom_pages", "custom_pages",
"rate_limit", "rate_limit",
"country_access", "country_access",
"method_policy",
"location_access_policy",
"configuration_history",
"cloudflare_client_ip", "cloudflare_client_ip",
"server_timing", "server_timing",
"static_cookie_cache", "static_cookie_cache",
@@ -332,6 +335,34 @@ async fn certificate_inventory(State(state): State<Arc<App>>) -> Json<Value> {
async fn configuration(State(state): State<Arc<App>>) -> Json<crate::config::Envelope> { async fn configuration(State(state): State<Arc<App>>) -> Json<crate::config::Envelope> {
Json(state.config.load().envelope.clone()) Json(state.config.load().envelope.clone())
} }
#[derive(Deserialize)]
struct ConfigHistoryQuery {
revision: Option<u64>,
limit: Option<usize>,
}
async fn config_history(
State(state): State<Arc<App>>,
Query(query): Query<ConfigHistoryQuery>,
) -> Result<Json<Value>, ApiError> {
if let Some(revision) = query.revision {
let detail = state
.store
.configuration_revision(revision)
.await
.map_err(auth::internal)?
.ok_or_else(|| ApiError(StatusCode::NOT_FOUND, "configuration revision not found".into()))?;
return Ok(Json(json!({"revision": detail})));
}
let limit = query.limit.unwrap_or(512).clamp(1, 512);
let revisions = state
.store
.configuration_history(limit)
.await
.map_err(auth::internal)?;
Ok(Json(json!({"revisions": revisions})))
}
async fn update( async fn update(
State(state): State<Arc<App>>, State(state): State<Arc<App>>,
Extension(session): Extension<Session>, Extension(session): Extension<Session>,
+1
View File
@@ -252,6 +252,7 @@
for path in [ for path in [
"/api/status", "/api/status",
"/api/config", "/api/config",
"/api/config/history",
"/api/backends", "/api/backends",
"/api/docker", "/api/docker",
"/api/traffic", "/api/traffic",
+107
View File
@@ -0,0 +1,107 @@
let configHistoryEntries = [];
function historyDate(value, options = {}) {
try { return new Intl.DateTimeFormat(locale(), options).format(new Date(Number(value) * 1000)); }
catch { return new Date(Number(value) * 1000).toLocaleString(); }
}
function historyDayKey(value) {
const date = new Date(Number(value) * 1000);
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
}
function historyMonthKey(value) { return historyDayKey(value).slice(0, 7); }
function historyValue(value) {
if (value === undefined) return t("configHistoryMissing");
const text = JSON.stringify(value, null, 2);
return text.length > 6000 ? `${text.slice(0, 6000)}\n...` : text;
}
function configDiff(before, after, path = "$", output = [], depth = 0) {
if (output.length >= 200) return output;
if (Object.is(before, after)) return output;
const beforeObject = before && typeof before === "object" && !Array.isArray(before);
const afterObject = after && typeof after === "object" && !Array.isArray(after);
if (beforeObject && afterObject && depth < 10) {
const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort();
for (const key of keys) configDiff(before[key], after[key], `${path}.${key}`, output, depth + 1);
return output;
}
if (JSON.stringify(before) !== JSON.stringify(after)) output.push({ path, before, after });
return output;
}
function renderConfigRevisionDetail(detail) {
const entry = detail?.revision;
if (!entry) return;
$("config-history-detail-title").textContent = `${t("configHistoryRevision")} ${entry.revision}`;
$("config-history-detail-meta").textContent = `${historyDate(entry.time, { dateStyle: "medium", timeStyle: "short" })} · ${entry.actor}`;
document.querySelectorAll(".config-history-revision").forEach(button => button.classList.toggle("active", Number(button.dataset.revision) === Number(entry.revision)));
const diff = configDiff(entry.previous_config, entry.config);
const box = $("config-history-diff"); box.replaceChildren();
if (!entry.previous_config) { box.append(el("p", t("configHistoryBaseline"), "empty")); return; }
if (!diff.length) { box.append(el("p", t("configHistoryNoChanges"), "empty")); return; }
for (const change of diff) {
const card = el("article", undefined, "config-history-change");
card.append(el("strong", change.path, "config-history-change-path"));
const values = el("div", undefined, "config-history-change-values");
for (const [label, value] of [[t("configHistoryBefore"), change.before], [t("configHistoryAfter"), change.after]]) {
const side = el("div"); side.append(el("span", label, "small-label"), el("pre", historyValue(value))); values.append(side);
}
card.append(values); box.append(card);
}
if (diff.length >= 200) box.append(el("p", t("configHistoryDiffLimit"), "fine"));
}
async function loadConfigRevision(revision) {
try { renderConfigRevisionDetail(await api(`/api/config/history?revision=${encodeURIComponent(revision)}`)); }
catch (error) { toast(error.message, true); }
}
function selectConfigHistoryDay(dayKey, entries) {
document.querySelectorAll(".config-history-day").forEach(button => button.classList.toggle("is-selected", button.dataset.day === dayKey));
const revisions = $("config-history-revisions"); revisions.replaceChildren();
const sorted = [...entries].sort((a, b) => b.revision - a.revision);
for (const entry of sorted) {
const button = el("button", `${t("configHistoryRevisionShort")} ${entry.revision}`, "quiet config-history-revision");
button.type = "button"; button.dataset.revision = entry.revision; button.title = `${entry.actor} · ${historyDate(entry.time, { dateStyle: "medium", timeStyle: "short" })}`;
button.addEventListener("click", () => void loadConfigRevision(entry.revision)); revisions.append(button);
}
if (sorted[0]) void loadConfigRevision(sorted[0].revision);
}
function renderConfigHistory() {
const calendar = $("config-history-calendar"); calendar.replaceChildren();
const byMonth = new Map(), byDay = new Map();
for (const entry of configHistoryEntries) {
const month = historyMonthKey(entry.time), day = historyDayKey(entry.time);
if (!byMonth.has(month)) byMonth.set(month, []); byMonth.get(month).push(entry);
if (!byDay.has(day)) byDay.set(day, []); byDay.get(day).push(entry);
}
$("config-history-summary").textContent = configHistoryEntries.length ? t("configHistorySummary").replace("{count}", configHistoryEntries.length) : t("configHistoryEmpty");
const months = [...byMonth.keys()].sort().reverse();
for (const key of months) {
const [yearText, monthText] = key.split("-"), year = Number(yearText), month = Number(monthText) - 1;
const panel = el("section", undefined, "config-history-month");
panel.append(el("h4", new Intl.DateTimeFormat(locale(), { month: "long", year: "numeric" }).format(new Date(year, month, 1))));
const weekdays = el("div", undefined, "config-history-weekdays");
for (let day = 0; day < 7; day++) weekdays.append(el("span", new Intl.DateTimeFormat(locale(), { weekday: "narrow" }).format(new Date(2024, 0, 7 + day))));
panel.append(weekdays);
const days = el("div", undefined, "config-history-days"), first = new Date(year, month, 1).getDay(), count = new Date(year, month + 1, 0).getDate();
for (let blank = 0; blank < first; blank++) days.append(el("span", "", "config-history-day"));
for (let day = 1; day <= count; day++) {
const dayKey = `${yearText}-${monthText}-${String(day).padStart(2, "0")}`, entries = byDay.get(dayKey) || [];
const node = el(entries.length ? "button" : "span", String(day), `config-history-day${entries.length ? " has-revisions" : ""}`);
if (entries.length) {
node.type = "button"; node.dataset.day = dayKey;
node.append(el("small", `${entries.length} ${t("configHistoryEntries")}`, "config-history-day-count"));
node.addEventListener("click", () => selectConfigHistoryDay(dayKey, entries));
}
days.append(node);
}
panel.append(days); calendar.append(panel);
}
if (!months.length) calendar.append(el("p", t("configHistoryEmpty"), "empty"));
}
async function loadConfigHistory() {
$("config-history-summary").textContent = t("configHistoryLoading");
try {
const result = await api("/api/config/history?limit=512"); configHistoryEntries = result.revisions || []; renderConfigHistory();
const latest = configHistoryEntries[0]; if (latest) selectConfigHistoryDay(historyDayKey(latest.time), configHistoryEntries.filter(entry => historyDayKey(entry.time) === historyDayKey(latest.time)));
} catch (error) { $("config-history-summary").textContent = t("configHistoryFailed"); toast(error.message, true); }
}
$("config-history-open").addEventListener("click", () => { const dialog = $("config-history-dialog"); if (!dialog.open) dialog.showModal(); void loadConfigHistory(); });
$("config-history-close").addEventListener("click", () => $("config-history-dialog").close());
$("config-history-reload").addEventListener("click", () => void loadConfigHistory());
+1 -1
View File
@@ -6,7 +6,7 @@ function renderSettings() {
if (!settingsDirty.has("response-form")) { if (!settingsDirty.has("response-form")) {
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {}; const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
$("expose-request-id").checked = !!diagnostics.request_id; $("expose-request-id").checked = !!diagnostics.request_id;
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.2"; $("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.3";
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name]; for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode(); $("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
} }
+26 -4
View File
@@ -1472,7 +1472,7 @@
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option> <option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
</select> </select>
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input <label data-i18n="serverValue" for="server-value">Custom Server value</label><input
id="server-value" maxlength="256" placeholder="RustProxy/v0.25.2" id="server-value" maxlength="256" placeholder="RustProxy/v0.25.3"
spellcheck="false" /> spellcheck="false" />
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3> <h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
<div class="switch-grid"> <div class="switch-grid">
@@ -2123,8 +2123,7 @@
<div class="card-heading"> <div class="card-heading">
<div><span class="small-label">DESIRED STATE / SQLITE</span> <div><span class="small-label">DESIRED STATE / SQLITE</span>
<h2 data-i18n="manualConfig">Manual config</h2> <h2 data-i18n="manualConfig">Manual config</h2>
</div><button class="outline" data-i18n="export" id="export-config">Export </div><div class="manual-config-tools"><button class="quiet" data-i18n="configHistory" id="config-history-open" type="button">Revision history</button><button class="outline" data-i18n="export" id="export-config" type="button">Export JSON</button></div>
JSON</button>
</div> </div>
<p class="fine" data-i18n="configHelp">A valid save creates one new revision. Invalid or <p class="fine" data-i18n="configHelp">A valid save creates one new revision. Invalid or
stale changes leave the running configuration untouched.</p> stale changes leave the running configuration untouched.</p>
@@ -2219,7 +2218,7 @@
</section> </section>
</div> </div>
</section> </section>
<footer class="page-footer"><span>RustProxy <b>0.25.2</b></span><span id="footer-status">API - / <footer class="page-footer"><span>RustProxy <b>0.25.3</b></span><span id="footer-status">API - /
dropped: 0</span></footer> dropped: 0</span></footer>
</div> </div>
</main> </main>
@@ -2355,6 +2354,29 @@
<p class="empty" data-i18n="tunnelsEmpty" id="tunnels-empty">No active tunnels.</p> <p class="empty" data-i18n="tunnelsEmpty" id="tunnels-empty">No active tunnels.</p>
</article> </article>
</dialog> </dialog>
<dialog aria-labelledby="config-history-title" class="config-history-dialog" id="config-history-dialog">
<div class="window-heading">
<div><span class="small-label" data-i18n="configHistoryRetention">SQLITE / LAST 512 REVISIONS</span>
<h2 data-i18n="configHistory" id="config-history-title">Revision history</h2>
</div>
<div class="window-tools"><button class="quiet" data-i18n="reload" id="config-history-reload" type="button">Reload</button><button class="outline" data-i18n="close" id="config-history-close" type="button">Close</button></div>
</div>
<div class="config-history-layout">
<section class="card config-history-browser">
<p class="fine" data-i18n="configHistoryHelp">Select a marked day to inspect the saved configuration revisions and their changes.</p>
<p class="fine" id="config-history-summary" role="status"></p>
<div class="config-history-months" id="config-history-calendar"></div>
</section>
<aside class="card config-history-detail">
<div class="config-history-detail-head">
<div><span class="small-label" data-i18n="configHistoryChanges">CHANGES</span><h3 id="config-history-detail-title">-</h3></div>
<span class="fine" id="config-history-detail-meta"></span>
</div>
<div class="config-history-revisions" id="config-history-revisions"></div>
<div class="config-history-diff" id="config-history-diff"><p class="empty" data-i18n="configHistorySelectDay">Select a day.</p></div>
</aside>
</div>
</dialog>
<dialog aria-labelledby="audit-dialog-title" class="audit-dialog" id="audit-dialog"> <dialog aria-labelledby="audit-dialog-title" class="audit-dialog" id="audit-dialog">
<div class="window-heading"> <div class="window-heading">
<div><span class="small-label" data-i18n="auditRetention">SQLITE / LAST 100 / RETAINED 10,000</span> <div><span class="small-label" data-i18n="auditRetention">SQLITE / LAST 100 / RETAINED 10,000</span>
+170
View File
@@ -7619,3 +7619,173 @@ body[data-density="compact"] .page-footer {
#web-service-dialog .web-service-page { #web-service-dialog .web-service-page {
padding-top: 2px; padding-top: 2px;
} }
.manual-config-tools {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.config-history-dialog {
color: var(--text);
background: var(--bg);
border: 1px solid var(--line);
padding: 0;
width: min(1180px, calc(100vw - 24px));
max-width: calc(100vw - 24px);
max-height: calc(100dvh - 28px);
overflow: hidden;
border-radius: 7px;
box-shadow: 0 18px 70px var(--shadow-strong);
}
.config-history-dialog::backdrop {
background: var(--shadow-strong);
backdrop-filter: blur(2px);
}
.config-history-layout {
display: grid;
grid-template-columns: minmax(380px, .95fr) minmax(420px, 1.05fr);
gap: 10px;
padding: 10px;
max-height: calc(100dvh - 90px);
}
.config-history-browser,
.config-history-detail {
min-width: 0;
overflow: auto;
}
.config-history-months {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 10px;
}
.config-history-month {
border: 1px solid var(--line);
border-radius: 6px;
padding: 9px;
background: var(--panel);
}
.config-history-month h4 {
margin: 0 0 8px;
font-size: 12px;
}
.config-history-weekdays,
.config-history-days {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 3px;
}
.config-history-weekdays span {
color: var(--muted);
font: 9px var(--mono);
text-align: center;
padding: 2px 0;
}
.config-history-day {
min-height: 34px;
padding: 4px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: var(--muted);
font: 10px var(--mono);
text-align: left;
}
button.config-history-day.has-revisions {
cursor: pointer;
color: var(--text);
border-color: var(--line-strong, var(--line));
background: var(--surface, var(--bg));
}
button.config-history-day.has-revisions:hover,
button.config-history-day.is-selected {
border-color: var(--accent);
color: var(--accent);
}
.config-history-day-count {
display: block;
margin-top: 3px;
font-size: 8px;
color: var(--muted);
}
.config-history-detail-head {
display: flex;
justify-content: space-between;
gap: 10px;
align-items: flex-start;
margin-bottom: 8px;
}
.config-history-detail-head h3 {
margin: 3px 0 0;
}
.config-history-revisions {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 10px;
}
.config-history-revision {
font: 10px var(--mono);
}
.config-history-revision.active {
border-color: var(--accent);
color: var(--accent);
}
.config-history-diff {
display: grid;
gap: 8px;
}
.config-history-change {
border: 1px solid var(--line);
border-radius: 5px;
padding: 8px;
background: var(--panel);
}
.config-history-change-path {
display: block;
margin-bottom: 6px;
font: 10px var(--mono);
overflow-wrap: anywhere;
}
.config-history-change-values {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.config-history-change pre {
margin: 3px 0 0;
max-height: 160px;
overflow: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
font: 10px var(--mono);
}
@media (max-width: 860px) {
.config-history-layout { grid-template-columns: 1fr; overflow: auto; }
.config-history-browser, .config-history-detail { overflow: visible; }
.config-history-change-values { grid-template-columns: 1fr; }
}
+24 -4
View File
@@ -47,7 +47,7 @@
frontend: { hosts: [], listener_ids: [] }, frontend: { hosts: [], listener_ids: [] },
source: { type: "reverse_proxy" }, source: { type: "reverse_proxy" },
backends: [{ id: "origin-1", url: "", weight: 1, enabled: true, pool_eligible: true, allow_private: true, accept_local_listener_risk: false, preserve_host: true, protocol: "http1", tls_verify: false }], backends: [{ id: "origin-1", url: "", weight: 1, enabled: true, pool_eligible: true, allow_private: true, accept_local_listener_risk: false, preserve_host: true, protocol: "http1", tls_verify: false }],
cache: { enabled: false }, transform: { enabled: false }, rate_limit: {}, routes: [], headers: {}, acl_profile_id: null, acl: null, tls_policy: null, proxy_timeouts: defaultProxyTimeouts() cache: { enabled: false }, transform: { enabled: false }, rate_limit: {}, method_policy: { mode: "off", methods: [] }, routes: [], headers: {}, acl_profile_id: null, acl: null, tls_policy: null, proxy_timeouts: defaultProxyTimeouts()
}); });
function activateTab(name) { document.querySelectorAll("#vhost-tabs button").forEach(b => { const active = b.dataset.tab === name; b.classList.toggle("active", active); b.setAttribute("aria-pressed", String(active)); b.setAttribute("aria-selected", String(active)); b.tabIndex = active ? 0 : -1; }); container.querySelectorAll(".vh-group").forEach(g => g.hidden = g.dataset.tab !== name); } function activateTab(name) { document.querySelectorAll("#vhost-tabs button").forEach(b => { const active = b.dataset.tab === name; b.classList.toggle("active", active); b.setAttribute("aria-pressed", String(active)); b.setAttribute("aria-selected", String(active)); b.tabIndex = active ? 0 : -1; }); container.querySelectorAll(".vh-group").forEach(g => g.hidden = g.dataset.tab !== name); }
@@ -296,7 +296,11 @@
source: action === "web_server" ? { type: "web_server", web_service_id: str("location-web-service", r) } : { type: "reverse_proxy" }, source: action === "web_server" ? { type: "web_server", web_service_id: str("location-web-service", r) } : { type: "reverse_proxy" },
backend_id: action === "proxy" ? (str("location-backend", r) || null) : null, backend_id: action === "proxy" ? (str("location-backend", r) || null) : null,
enabled: yes("location-enabled", r), cache: action === "proxy" ? (str("location-cache", r) === "" ? null : str("location-cache", r) === "on") : false, enabled: yes("location-enabled", r), cache: action === "proxy" ? (str("location-cache", r) === "" ? null : str("location-cache", r) === "on") : false,
acl: r.readAcl(), headers: r.readHeaders(), acl_profile_id: str("location-acl-profile", r) || null,
acl: r.readAcl(),
country_access: { mode: str("location-country-mode", r), countries: lines(str("location-country-list", r)).map(value => value.toUpperCase()) },
method_policy: { mode: str("location-method-mode", r), methods: lines(str("location-method-list", r)).map(value => value.toUpperCase()) },
headers: r.readHeaders(),
rewrite: { mode: handled ? rewriteMode : "preserve", value: handled && rewriteMode !== "preserve" ? read("location-rewrite-value", r).value.trim() : "" }, rewrite: { mode: handled ? rewriteMode : "preserve", value: handled && rewriteMode !== "preserve" ? read("location-rewrite-value", r).value.trim() : "" },
response: action === "return" ? { status: Number(str("location-response-status", r)), content_type: str("location-response-type", r), body: read("location-response-body", r).value } : null response: action === "return" ? { status: Number(str("location-response-status", r)), content_type: str("location-response-type", r), body: read("location-response-body", r).value } : null
}; };
@@ -328,7 +332,18 @@
field(returnBox, "location-response-body", "locationResponseBody", value.response?.body || "", "textarea", { rows: 5, maxLength: 65536 }); field(returnBox, "location-response-body", "locationResponseBody", value.response?.body || "", "textarea", { rows: 5, maxLength: 65536 });
note(handlingBody, "locationRewriteHelp"); note(handlingBody, "locationRewriteHelp");
const extra = el("details", undefined, "vh-wide location-section"), accessBody = el("div", undefined, "vh-grid"); extra.append(text("summary", "locationAccess"), accessBody); body.append(extra); const acl = aclEditor(accessBody, value.acl, sync); row.readAcl = () => acl.read(); const extra = el("details", undefined, "vh-wide location-section"), accessBody = el("div", undefined, "vh-grid"); extra.append(text("summary", "locationAccess"), accessBody); body.append(extra);
const locationProfile = field(accessBody, "location-acl-profile", "locationAclProfile", value.acl_profile_id || "", "select");
const locationNoProfile = text("option", "vhAclProfileNone"); locationNoProfile.value = "";
locationProfile.append(locationNoProfile, ...(envelope?.config.acl_profiles || []).map(profile => { const option = el("option", profile.name || profile.id); option.value = profile.id; return option; }));
locationProfile.value = value.acl_profile_id || "";
field(accessBody, "location-method-mode", "locationMethodMode", value.method_policy?.mode || "off", "select", { values: [["off", "vhOff"], ["allow", "locationMethodAllow"], ["block", "locationMethodBlock"]] });
field(accessBody, "location-method-list", "locationMethodList", (value.method_policy?.methods || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
note(accessBody, "locationMethodHelp");
field(accessBody, "location-country-mode", "locationCountryMode", value.country_access?.mode || "off", "select", { values: [["off", "vhOff"], ["block", "vhCountryBlock"], ["allow", "vhCountryAllow"]] });
field(accessBody, "location-country-list", "locationCountryList", (value.country_access?.countries || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
note(accessBody, "locationCountryHelp");
const acl = aclEditor(accessBody, value.acl, sync); row.readAcl = () => acl.read();
const headerBox = el("details", undefined, "vh-wide location-section location-header-editor"), headerBody = el("div", undefined, "vh-grid"); headerBox.append(text("summary", "locationHeaders"), headerBody); body.append(headerBox); const headerBox = el("details", undefined, "vh-wide location-section location-header-editor"), headerBody = el("div", undefined, "vh-grid"); headerBox.append(text("summary", "locationHeaders"), headerBody); body.append(headerBox);
const headerToolbar = el("div", undefined, "vh-wide location-header-toolbar"), visualPane = el("div", undefined, "vh-wide vh-grid"), jsonPane = el("div", undefined, "vh-wide vh-grid"); jsonPane.hidden = true; const headerToolbar = el("div", undefined, "vh-wide location-header-toolbar"), visualPane = el("div", undefined, "vh-wide vh-grid"), jsonPane = el("div", undefined, "vh-wide vh-grid"); jsonPane.hidden = true;
const visualButton = button("headersVisualBuilder", () => { visualPane.hidden = false; jsonPane.hidden = true; }), jsonButton = button("headersAdvancedJson", () => { visualPane.hidden = true; jsonPane.hidden = false; }), docsButton = button("documentation", () => openLocationHelp("headers")); const visualButton = button("headersVisualBuilder", () => { visualPane.hidden = false; jsonPane.hidden = true; }), jsonButton = button("headersAdvancedJson", () => { visualPane.hidden = true; jsonPane.hidden = false; }), docsButton = button("documentation", () => openLocationHelp("headers"));
@@ -381,7 +396,12 @@
const replacement = read("location-rewrite-value", row).value.trim(); const replacement = read("location-rewrite-value", row).value.trim();
previewValues.rewrite.textContent = `${t(rewriteLabels[rewriteMode.value] || "locationRewritePreserve")}${replacement && !["preserve", "strip_prefix"].includes(rewriteMode.value) ? `: ${replacement}` : ""} · ${t("locationPreviewQueryPreserved")}`; previewValues.rewrite.textContent = `${t(rewriteLabels[rewriteMode.value] || "locationRewritePreserve")}${replacement && !["preserve", "strip_prefix"].includes(rewriteMode.value) ? `: ${replacement}` : ""} · ${t("locationPreviewQueryPreserved")}`;
} }
previewValues.access.textContent = yes("acl-use", row) ? t("locationPreviewCustomAcl") : t("locationPreviewVhostAcl"); const accessParts = [];
const profile = str("location-acl-profile", row); if (profile) accessParts.push(`${t("locationAclProfile")}: ${profile}`);
const methodMode = str("location-method-mode", row); if (methodMode !== "off") accessParts.push(`${t("locationMethodMode")}: ${t(methodMode === "allow" ? "locationMethodAllow" : "locationMethodBlock")} ${lines(str("location-method-list", row)).join(", ")}`);
const countryMode = str("location-country-mode", row); if (countryMode !== "off") accessParts.push(`${t("locationCountryMode")}: ${t(countryMode === "allow" ? "vhCountryAllow" : "vhCountryBlock")} ${lines(str("location-country-list", row)).join(", ")}`);
if (yes("acl-use", row)) accessParts.push(t("locationPreviewCustomAcl"));
previewValues.access.textContent = accessParts.length ? accessParts.join(" · ") : t("locationPreviewVhostAcl");
try { try {
const headerValue = editor.read(), requestRules = headerValue.request?.length || 0, responseRules = headerValue.response?.length || 0; const headerValue = editor.read(), requestRules = headerValue.request?.length || 0, responseRules = headerValue.response?.length || 0;
previewValues.headers.textContent = `${requestRules} ${t("locationPreviewRequestRules")} / ${responseRules} ${t("locationPreviewResponseRules")}`; previewValues.headers.textContent = `${requestRules} ${t("locationPreviewRequestRules")} / ${responseRules} ${t("locationPreviewResponseRules")}`;
+5 -1
View File
@@ -145,10 +145,13 @@
g = group("vhSticky"); field(g, "sticky-mode", "vhStickyMode", sticky.mode || "off", "select", { values: [["off", "vhOff"], ["cookie", "vhCookie"], ["ip_hash", "vhIpHash"]] }); g = group("vhSticky"); field(g, "sticky-mode", "vhStickyMode", sticky.mode || "off", "select", { values: [["off", "vhOff"], ["cookie", "vhCookie"], ["ip_hash", "vhIpHash"]] });
field(g, "cookie-name", "vhCookieName", sticky.cookie_name || "", "text", { maxLength: 96, pattern: "[A-Za-z0-9_-]*" }); field(g, "cookie-name", "vhCookieName", sticky.cookie_name || "", "text", { maxLength: 96, pattern: "[A-Za-z0-9_-]*" });
field(g, "cookie-ttl", "vhCookieTtl", sticky.ttl_seconds ?? 3600, "number", { required: true, min: 60, max: 604800 }); note(g, "vhStickyNote"); field(g, "cookie-ttl", "vhCookieTtl", sticky.ttl_seconds ?? 3600, "number", { required: true, min: 60, max: 604800 }); note(g, "vhStickyNote");
const rate = value.rate_limit || {}, access = rate.country_access || {}; const rate = value.rate_limit || {}, access = rate.country_access || {}, methodPolicy = value.method_policy || {};
g = group("vhTrafficPolicy"); g = group("vhTrafficPolicy");
const aclProfile = field(g, "acl-profile-id", "vhAclProfile", value.acl_profile_id || "", "select"); const aclProfile = field(g, "acl-profile-id", "vhAclProfile", value.acl_profile_id || "", "select");
const noProfile = text("option", "vhAclProfileNone"); noProfile.value = ""; aclProfile.append(noProfile, ...(envelope?.config.acl_profiles || []).map(profile => { const option = el("option", profile.name || profile.id); option.value = profile.id; return option; })); aclProfile.value = value.acl_profile_id || ""; note(g, "vhAclProfileHelp"); const noProfile = text("option", "vhAclProfileNone"); noProfile.value = ""; aclProfile.append(noProfile, ...(envelope?.config.acl_profiles || []).map(profile => { const option = el("option", profile.name || profile.id); option.value = profile.id; return option; })); aclProfile.value = value.acl_profile_id || ""; note(g, "vhAclProfileHelp");
field(g, "method-mode", "vhMethodMode", methodPolicy.mode || "off", "select", { values: [["off", "vhOff"], ["allow", "locationMethodAllow"], ["block", "locationMethodBlock"]] });
field(g, "method-list", "vhMethodList", (methodPolicy.methods || []).join("\n"), "textarea", { rows: 3, maxLength: 1024 });
note(g, "vhMethodHelp");
field(g, "rate-enabled", "vhRateEnabled", rate.enabled === true, "checkbox"); field(g, "rate-enabled", "vhRateEnabled", rate.enabled === true, "checkbox");
field(g, "rate-requests", "vhRateRequests", rate.requests ?? 120, "number", { required: true, min: 1, max: 1000000 }); field(g, "rate-requests", "vhRateRequests", rate.requests ?? 120, "number", { required: true, min: 1, max: 1000000 });
field(g, "rate-window", "vhRateWindow", rate.window_seconds ?? 60, "number", { required: true, min: 1, max: 3600 }); field(g, "rate-window", "vhRateWindow", rate.window_seconds ?? 60, "number", { required: true, min: 1, max: 3600 });
@@ -309,6 +312,7 @@
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, force_ssl: yes("force-ssl"), forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: nullable("acl-profile-id"), id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, force_ssl: yes("force-ssl"), forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: nullable("acl-profile-id"),
proxy_timeouts: { connect_timeout_seconds: num("proxy-connect-timeout"), response_header_timeout_seconds: num("proxy-header-timeout"), read_timeout_seconds: num("proxy-read-timeout"), send_timeout_seconds: num("proxy-send-timeout") }, proxy_timeouts: { connect_timeout_seconds: num("proxy-connect-timeout"), response_header_timeout_seconds: num("proxy-header-timeout"), read_timeout_seconds: num("proxy-read-timeout"), send_timeout_seconds: num("proxy-send-timeout") },
rate_limit: { enabled: yes("rate-enabled"), requests: num("rate-requests"), window_seconds: num("rate-window"), burst: num("rate-burst"), country_access: { mode: str("country-mode"), countries: lines(str("country-list")).map(value => value.toUpperCase()) }, country_limits: countryRatesReader() }, rate_limit: { enabled: yes("rate-enabled"), requests: num("rate-requests"), window_seconds: num("rate-window"), burst: num("rate-burst"), country_access: { mode: str("country-mode"), countries: lines(str("country-list")).map(value => value.toUpperCase()) }, country_limits: countryRatesReader() },
method_policy: { mode: str("method-mode"), methods: lines(str("method-list")).map(value => value.toUpperCase()) },
server_header: str("server-mode") === "inherit" ? null : { mode: str("server-mode"), value: str("server-value") }, server_header: str("server-mode") === "inherit" ? null : { mode: str("server-mode"), value: str("server-value") },
diagnostic_headers: yes("diag-inherit") ? null : { request_id: yes("diag-request_id") }, diagnostic_headers: yes("diag-inherit") ? null : { request_id: yes("diag-request_id") },
tinyauth: { enabled: yes("auth-enabled"), url: str("auth-url"), login_url: str("auth-login"), allow_private: yes("auth-private"), redirect_forbidden: yes("auth-redirect-forbidden"), connect_timeout_seconds: num("auth-connect"), timeout_seconds: num("auth-timeout"), request_headers: lines(str("auth-request-headers")), response_headers: lines(str("auth-response-headers")), address_allowlist: lines(str("auth-allowlist")), tls_ca_pem: nullable("auth-ca") }, backends: members, routes: paths, acl, headers, tinyauth: { enabled: yes("auth-enabled"), url: str("auth-url"), login_url: str("auth-login"), allow_private: yes("auth-private"), redirect_forbidden: yes("auth-redirect-forbidden"), connect_timeout_seconds: num("auth-connect"), timeout_seconds: num("auth-timeout"), request_headers: lines(str("auth-request-headers")), response_headers: lines(str("auth-response-headers")), address_allowlist: lines(str("auth-allowlist")), tls_ca_pem: nullable("auth-ca") }, backends: members, routes: paths, acl, headers,
+6
View File
@@ -191,7 +191,12 @@ pub struct CompiledRoute {
pub response: Option<crate::vhost::LocationResponse>, pub response: Option<crate::vhost::LocationResponse>,
pub source: CompiledSource, pub source: CompiledSource,
pub backend: Option<Arc<CompiledBackend>>, pub backend: Option<Arc<CompiledBackend>>,
pub acl_profile_id: Option<String>,
pub profile_acl: Option<CompiledAcl>,
pub profile_country_access: crate::rate_limit::CountryAccess,
pub acl: Option<CompiledAcl>, pub acl: Option<CompiledAcl>,
pub country_access: crate::rate_limit::CountryAccess,
pub method_policy: crate::vhost::MethodPolicy,
pub headers: CompiledHeaders, pub headers: CompiledHeaders,
} }
#[derive(Debug)] #[derive(Debug)]
@@ -214,6 +219,7 @@ pub struct CompiledFrontend {
pub backend_header: bool, pub backend_header: bool,
pub backend_timing_header: bool, pub backend_timing_header: bool,
pub rate_limit: crate::rate_limit::Settings, pub rate_limit: crate::rate_limit::Settings,
pub method_policy: crate::vhost::MethodPolicy,
pub basic_auth: Option<crate::basic_auth::Compiled>, pub basic_auth: Option<crate::basic_auth::Compiled>,
pub metrics: Arc<crate::metrics::VhostMetrics>, pub metrics: Arc<crate::metrics::VhostMetrics>,
pub server_header: crate::settings::ServerHeader, pub server_header: crate::settings::ServerHeader,
+22
View File
@@ -171,6 +171,7 @@ for host in &mut config.vhosts {
backend_header: host.backend_header, backend_header: host.backend_header,
backend_timing_header: host.backend_timing_header, backend_timing_header: host.backend_timing_header,
rate_limit: host.rate_limit.clone(), rate_limit: host.rate_limit.clone(),
method_policy: host.method_policy.clone(),
basic_auth: host.basic_auth.compile()?, basic_auth: host.basic_auth.compile()?,
server_header: host server_header: host
.server_header .server_header
@@ -226,7 +227,12 @@ for host in &mut config.vhosts {
response: None, response: None,
source: fallback_source, source: fallback_source,
backend: fallback_backend, backend: fallback_backend,
acl_profile_id: None,
profile_acl: None,
profile_country_access: crate::rate_limit::CountryAccess::default(),
acl: None, acl: None,
country_access: crate::rate_limit::CountryAccess::default(),
method_policy: crate::vhost::MethodPolicy::default(),
headers: crate::headers::CompiledHeaders::default(), headers: crate::headers::CompiledHeaders::default(),
}; };
for name in &host.frontend.hosts { for name in &host.frontend.hosts {
@@ -282,6 +288,17 @@ for host in &mut config.vhosts {
); );
validate_route_rewrite(route, path_regex.as_ref())?; validate_route_rewrite(route, path_regex.as_ref())?;
validate_location_response(route.response.as_ref())?; validate_location_response(route.response.as_ref())?;
let (route_profile_acl, route_profile_country_access) = match route.acl_profile_id.as_deref() {
Some(id) => acl_profiles.get(id).cloned().ok_or_else(|| {
anyhow::anyhow!(
"vhost {} location {} references unknown ACL profile {}",
host.id,
route.id,
id
)
})?,
None => (None, crate::rate_limit::CountryAccess::default()),
};
let acl = route.acl.as_ref().map(Acl::compile).transpose()?; let acl = route.acl.as_ref().map(Acl::compile).transpose()?;
let headers = route.headers.compile()?; let headers = route.headers.compile()?;
if route.source.is_reverse_proxy() { if route.source.is_reverse_proxy() {
@@ -339,7 +356,12 @@ for host in &mut config.vhosts {
response: route.response.clone(), response: route.response.clone(),
source, source,
backend, backend,
acl_profile_id: route.acl_profile_id.clone(),
profile_acl: route_profile_acl,
profile_country_access: route_profile_country_access,
acl, acl,
country_access: route.country_access.clone(),
method_policy: route.method_policy.clone(),
headers, headers,
}; };
for name in &host.frontend.hosts { for name in &host.frontend.hosts {
+33 -4
View File
@@ -1,6 +1,8 @@
//! SQLite stores desired configuration, administrator state and bounded audit history. //! SQLite stores desired configuration, administrator state and bounded audit history.
//! No database call is made from the public HTTP forwarding path. //! No database call is made from the public HTTP forwarding path.
use crate::config::{Config, Envelope}; use crate::config::{Config, Envelope};
mod history;
pub use history::{ConfigurationHistoryDetail, ConfigurationRevision};
use anyhow::{ensure, Context, Result}; use anyhow::{ensure, Context, Result};
use serde::Serialize; use serde::Serialize;
use sqlx::{ use sqlx::{
@@ -9,7 +11,7 @@ use sqlx::{
}; };
use std::{path::Path, time::Duration}; use std::{path::Path, time::Duration};
pub const SCHEMA_VERSION: i64 = 7; pub const SCHEMA_VERSION: i64 = 8;
pub const CERTIFICATE_API_CREDENTIAL_CAPACITY: i64 = 32; pub const CERTIFICATE_API_CREDENTIAL_CAPACITY: i64 = 32;
pub const AUDIT_CAPACITY: i64 = 10_000; pub const AUDIT_CAPACITY: i64 = 10_000;
@@ -94,11 +96,20 @@ impl Store {
sqlx::raw_sql(include_str!("../migrations/0007_secret_store.sql")) sqlx::raw_sql(include_str!("../migrations/0007_secret_store.sql"))
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
sqlx::raw_sql(include_str!("../migrations/0008_config_history.sql"))
.execute(&mut *tx)
.await?;
sqlx::query("INSERT OR IGNORE INTO configuration(id, revision, document) VALUES(1, 0, ?)") sqlx::query("INSERT OR IGNORE INTO configuration(id, revision, document) VALUES(1, 0, ?)")
.bind(serde_json::to_string(initial)?) .bind(serde_json::to_string(initial)?)
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
sqlx::query("PRAGMA user_version = 7") sqlx::query(
"INSERT OR IGNORE INTO configuration_history(revision, time, actor, document) SELECT revision, ?, 'migration', document FROM configuration WHERE id = 1",
)
.bind(crate::auth::now())
.execute(&mut *tx)
.await?;
sqlx::query("PRAGMA user_version = 8")
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
tx.commit().await?; tx.commit().await?;
@@ -172,22 +183,40 @@ impl Store {
pub async fn replace_as(&self, expected: u64, config: &Config, actor: &str) -> Result<bool> { pub async fn replace_as(&self, expected: u64, config: &Config, actor: &str) -> Result<bool> {
let expected: i64 = expected.try_into().context("revision overflow")?; let expected: i64 = expected.try_into().context("revision overflow")?;
ensure!(expected < i64::MAX, "revision overflow"); ensure!(expected < i64::MAX, "revision overflow");
let document = serde_json::to_string(config)?;
let mut tx = self.pool.begin().await?; let mut tx = self.pool.begin().await?;
let result = sqlx::query("UPDATE configuration SET revision = revision + 1, document = ? WHERE id = 1 AND revision = ?") let result = sqlx::query("UPDATE configuration SET revision = revision + 1, document = ? WHERE id = 1 AND revision = ?")
.bind(serde_json::to_string(config)?).bind(expected).execute(&mut *tx).await?; .bind(&document).bind(expected).execute(&mut *tx).await?;
if result.rows_affected() == 0 { if result.rows_affected() == 0 {
return Ok(false); return Ok(false);
} }
let revision = expected + 1;
sqlx::query(
"INSERT INTO configuration_history(revision, time, actor, document) VALUES(?, ?, ?, ?)",
)
.bind(revision)
.bind(crate::auth::now())
.bind(actor)
.bind(&document)
.execute(&mut *tx)
.await?;
sqlx::query(
"DELETE FROM configuration_history WHERE revision NOT IN (SELECT revision FROM configuration_history ORDER BY revision DESC LIMIT ?)",
)
.bind(history::CONFIG_HISTORY_CAPACITY)
.execute(&mut *tx)
.await?;
audit_tx( audit_tx(
&mut tx, &mut tx,
actor, actor,
"config.apply", "config.apply",
&format!("revision={}", expected + 1), &format!("revision={revision}"),
) )
.await?; .await?;
tx.commit().await?; tx.commit().await?;
Ok(true) Ok(true)
} }
pub async fn audit(&self, actor: &str, action: &str, detail: &str) -> Result<()> { pub async fn audit(&self, actor: &str, action: &str, detail: &str) -> Result<()> {
let mut tx = self.pool.begin().await?; let mut tx = self.pool.begin().await?;
audit_tx(&mut tx, actor, action, detail).await?; audit_tx(&mut tx, actor, action, detail).await?;
+96
View File
@@ -0,0 +1,96 @@
use super::Store;
use crate::config::Config;
use anyhow::{ensure, Context, Result};
use serde::Serialize;
use sqlx::Row;
pub(super) const CONFIG_HISTORY_CAPACITY: i64 = 512;
#[derive(Debug, Clone, Serialize)]
pub struct ConfigurationRevision {
pub revision: u64,
pub time: i64,
pub actor: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ConfigurationHistoryDetail {
pub revision: u64,
pub time: i64,
pub actor: String,
pub config: Config,
pub previous_revision: Option<u64>,
pub previous_config: Option<Config>,
}
impl Store {
pub async fn configuration_history(&self, limit: usize) -> Result<Vec<ConfigurationRevision>> {
let limit = i64::try_from(limit.min(CONFIG_HISTORY_CAPACITY as usize))?;
let rows = sqlx::query(
"SELECT revision, time, actor FROM configuration_history ORDER BY revision DESC LIMIT ?",
)
.bind(limit)
.fetch_all(&self.pool)
.await?;
rows.into_iter()
.map(|row| {
Ok(ConfigurationRevision {
revision: row.try_get::<i64, _>("revision")?.try_into()?,
time: row.try_get("time")?,
actor: row.try_get("actor")?,
})
})
.collect()
}
pub async fn configuration_revision(
&self,
revision: u64,
) -> Result<Option<ConfigurationHistoryDetail>> {
let revision_i64: i64 = revision.try_into().context("revision overflow")?;
let Some(row) = sqlx::query(
"SELECT revision, time, actor, document FROM configuration_history WHERE revision = ?",
)
.bind(revision_i64)
.fetch_optional(&self.pool)
.await? else {
return Ok(None);
};
let document: String = row.try_get("document")?;
ensure!(
document.len() <= crate::config::MAX_CONFIG_BYTES,
"stored configuration history exceeds size limit"
);
let previous = sqlx::query(
"SELECT revision, document FROM configuration_history WHERE revision < ? ORDER BY revision DESC LIMIT 1",
)
.bind(revision_i64)
.fetch_optional(&self.pool)
.await?;
let (previous_revision, previous_config) = if let Some(previous) = previous {
let previous_document: String = previous.try_get("document")?;
ensure!(
previous_document.len() <= crate::config::MAX_CONFIG_BYTES,
"stored configuration history exceeds size limit"
);
(
Some(previous.try_get::<i64, _>("revision")?.try_into()?),
Some(serde_json::from_str(&previous_document).context(
"invalid previous configuration history document",
)?),
)
} else {
(None, None)
};
Ok(Some(ConfigurationHistoryDetail {
revision: row.try_get::<i64, _>("revision")?.try_into()?,
time: row.try_get("time")?,
actor: row.try_get("actor")?,
config: serde_json::from_str(&document)
.context("invalid configuration history document")?,
previous_revision,
previous_config,
}))
}
}
+22
View File
@@ -36,6 +36,28 @@ mod tests {
assert_eq!(data.config.transform.max_inflight, 64); assert_eq!(data.config.transform.max_inflight, 64);
reopened.close().await; reopened.close().await;
} }
#[tokio::test]
async fn configuration_history_keeps_revision_documents_and_actor() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("history.sqlite3");
let store = Store::open(&file, &Config::default()).await.unwrap();
let mut first = Config::default();
first.server_header.value = "first".into();
assert!(store.replace_as(0, &first, "alice").await.unwrap());
let mut second = first.clone();
second.server_header.value = "second".into();
assert!(store.replace_as(1, &second, "bob").await.unwrap());
let history = store.configuration_history(10).await.unwrap();
assert_eq!(history[0].revision, 2);
assert_eq!(history[0].actor, "bob");
assert_eq!(history[1].revision, 1);
let detail = store.configuration_revision(2).await.unwrap().unwrap();
assert_eq!(detail.config.server_header.value, "second");
assert_eq!(detail.previous_revision, Some(1));
assert_eq!(detail.previous_config.unwrap().server_header.value, "first");
store.close().await;
}
#[tokio::test] #[tokio::test]
async fn certificate_renewal_observations_survive_reopen_in_schema_v3() { async fn certificate_renewal_observations_survive_reopen_in_schema_v3() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
+2
View File
@@ -247,6 +247,7 @@ pub struct LogEntry {
pub transform_effective_enabled: Option<bool>, pub transform_effective_enabled: Option<bool>,
pub backend: Option<String>, pub backend: Option<String>,
pub id: u64, pub id: u64,
pub request_id: String,
pub time_ms: u64, pub time_ms: u64,
pub method: String, pub method: String,
pub host: String, pub host: String,
@@ -398,6 +399,7 @@ fn value_for(entry: &LogEntry, name: &str) -> Value {
.map(Value::String) .map(Value::String)
.unwrap_or(Value::Null), .unwrap_or(Value::Null),
"id" => json!(entry.id), "id" => json!(entry.id),
"request_id" => json!(&entry.request_id),
"time_ms" => json!(entry.time_ms), "time_ms" => json!(entry.time_ms),
"method" => json!(&entry.method), "method" => json!(&entry.method),
"host" => json!(&entry.host), "host" => json!(&entry.host),
+1
View File
@@ -235,6 +235,7 @@ fn source_value_name(name: &str) -> bool {
| "transform_effective_enabled" | "transform_effective_enabled"
| "backend" | "backend"
| "id" | "id"
| "request_id"
| "time_ms" | "time_ms"
| "method" | "method"
| "host" | "host"
+1
View File
@@ -9,6 +9,7 @@ mod tests {
transform_effective_enabled: Some(false), transform_effective_enabled: Some(false),
backend: Some("origin".into()), backend: Some("origin".into()),
id: 7, id: 7,
request_id: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
time_ms: 1_700_000_000_000, time_ms: 1_700_000_000_000,
method: "GET".into(), method: "GET".into(),
host: "app.example".into(), host: "app.example".into(),
+1 -1
View File
@@ -41,7 +41,7 @@ use tracing_subscriber::prelude::*;
#[command( #[command(
name = "rust-proxy", name = "rust-proxy",
version, version,
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.25.2" about = "RustProxy - independent Rust reverse proxy and static Web Server 0.25.3"
)] )]
struct Cli { struct Cli {
#[arg( #[arg(
+1 -1
View File
@@ -226,7 +226,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
} }
}); });
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal(); let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
println!("RustProxy live logs 0.25.2 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits."); println!("RustProxy live logs 0.25.3 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
if interactive { print!("> "); io::stdout().flush()?; } if interactive { print!("> "); io::stdout().flush()?; }
let mut filters = LiveLogFilters::default(); let mut filters = LiveLogFilters::default();
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new(); let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
+2
View File
@@ -29,6 +29,7 @@ pub struct Exchange {
pub transform_effective_enabled: Mutex<Option<bool>>, pub transform_effective_enabled: Mutex<Option<bool>>,
pub backend: Mutex<Option<Arc<crate::balance::Lease>>>, pub backend: Mutex<Option<Arc<crate::balance::Lease>>>,
pub id: u64, pub id: u64,
pub request_id: String,
pub method: String, pub method: String,
pub host: String, pub host: String,
pub path: String, pub path: String,
@@ -179,6 +180,7 @@ impl Drop for Exchange {
transform_effective_enabled, transform_effective_enabled,
backend, backend,
id: self.id, id: self.id,
request_id: self.request_id.clone(),
time_ms: now_ms(), time_ms: now_ms(),
method: self.method.clone(), method: self.method.clone(),
host: self.host.clone(), host: self.host.clone(),
+1
View File
@@ -15,6 +15,7 @@ mod tests {
transform_effective_enabled: Mutex::new(None), transform_effective_enabled: Mutex::new(None),
backend: Mutex::new(None), backend: Mutex::new(None),
id: 1, id: 1,
request_id: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".into(),
method: "POST".into(), method: "POST".into(),
host: "localhost".into(), host: "localhost".into(),
path: "/echo".into(), path: "/echo".into(),
+19 -2
View File
@@ -120,6 +120,23 @@ fn error(status: StatusCode, message: &'static str) -> Response {
response response
} }
fn method_policy_denial(
policy: &crate::vhost::MethodPolicy,
method: &Method,
) -> Option<Response> {
if policy.allows(method) {
return None;
}
let mut response = error(
StatusCode::METHOD_NOT_ALLOWED,
"RustProxy: HTTP method denied by access policy\n",
);
if let Some(value) = policy.allow_header() {
response.headers_mut().insert(http::header::ALLOW, value);
}
Some(response)
}
fn local_location_response(spec: &crate::vhost::LocationResponse, method: &Method) -> Response { fn local_location_response(spec: &crate::vhost::LocationResponse, method: &Method) -> Response {
let status = StatusCode::from_u16(spec.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR); let status = StatusCode::from_u16(spec.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let body_allowed = !matches!(status.as_u16(), 100..=199 | 204 | 304) && *method != Method::HEAD; let body_allowed = !matches!(status.as_u16(), 100..=199 | 204 | 304) && *method != Method::HEAD;
@@ -211,7 +228,7 @@ fn finish_headers(
frontend: Option<&crate::config::CompiledFrontend>, frontend: Option<&crate::config::CompiledFrontend>,
local: Option<std::net::SocketAddr>, local: Option<std::net::SocketAddr>,
tls: bool, tls: bool,
id: u64, request_id: &str,
active: &[crate::vhost::Listener], active: &[crate::vhost::Listener],
) { ) {
if let Some(frontend) = frontend { if let Some(frontend) = frontend {
@@ -224,7 +241,7 @@ fn finish_headers(
frontend frontend
.map(|f| &f.diagnostic_headers) .map(|f| &f.diagnostic_headers)
.unwrap_or(&snapshot.envelope.config.diagnostic_headers) .unwrap_or(&snapshot.envelope.config.diagnostic_headers)
.apply(response.headers_mut(), id); .apply(response.headers_mut(), request_id);
// Authenticated content must not leak through an external shared cache either. // Authenticated content must not leak through an external shared cache either.
if frontend.is_some_and(|f| f.tinyauth.is_some() || f.basic_auth.is_some()) { if frontend.is_some_and(|f| f.tinyauth.is_some() || f.basic_auth.is_some()) {
response.headers_mut().insert( response.headers_mut().insert(
+4
View File
@@ -1,6 +1,10 @@
{ {
// Both scopes run before DNS, TCP, TLS or reuse of an existing upstream connection. // Both scopes run before DNS, TCP, TLS or reuse of an existing upstream connection.
if !snapshot.host_allowed(&host, context.client_ip) if !snapshot.host_allowed(&host, context.client_ip)
|| !route
.profile_acl
.as_ref()
.is_none_or(|acl| acl.allows(context.client_ip))
|| !route || !route
.acl .acl
.as_ref() .as_ref()
+52 -16
View File
@@ -141,6 +141,11 @@ async fn forward(
"RustProxy: RFC 9220 WebSocket transport is not enabled for this vhost\n", "RustProxy: RFC 9220 WebSocket transport is not enabled for this vhost\n",
); );
} }
if let Some(response) = frontend
.and_then(|frontend| method_policy_denial(&frontend.method_policy, request.method()))
{
return response;
}
if !tls { if !tls {
if let Some(response) = frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri())) { if let Some(response) = frontend.and_then(|frontend| force_ssl_redirect(frontend, &host, request.uri())) {
return response; return response;
@@ -175,16 +180,19 @@ async fn forward(
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() { if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) = Some(context.client_ip.to_string()); *exchange.client_ip.lock().unwrap_or_else(|e| e.into_inner()) = Some(context.client_ip.to_string());
} }
// GeoIP stays off the normal request path. Resolve lazily for any vhost or
// location policy that actually needs a country code, or for log capture.
let capture_country = state.logs.capture_country();
let mut country_resolved = false;
let mut country: Option<String> = None;
if let Some(frontend) = frontend { if let Some(frontend) = frontend {
// GeoIP is deliberately off the default request path. Resolve only when if capture_country
// a country policy/rate-limit requires it or the operator explicitly
// enables country capture for access logs + traffic analytics.
let capture_country = state.logs.capture_country();
let country = (capture_country
|| frontend.profile_country_access.needs_geoip() || frontend.profile_country_access.needs_geoip()
|| frontend.rate_limit.needs_geoip()) || frontend.rate_limit.needs_geoip()
.then(|| state.geoip.country(context.client_ip)) {
.flatten(); country = state.geoip.country(context.client_ip);
country_resolved = true;
}
if capture_country { if capture_country {
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() { if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.country.lock().unwrap_or_else(|e| e.into_inner()) = country.clone(); *exchange.country.lock().unwrap_or_else(|e| e.into_inner()) = country.clone();
@@ -246,14 +254,42 @@ async fn forward(
return response; return response;
} }
let selected = snapshot.select_on(&host, &path, local.0, tls); let selected = snapshot.select_on(&host, &path, local.0, tls);
if !selected if let Some(route) = selected {
.and_then(|r| r.acl.as_ref()) if let Some(response) = method_policy_denial(&route.method_policy, request.method()) {
.is_none_or(|acl| acl.allows(context.client_ip)) return response;
{ }
return error( if !route
StatusCode::FORBIDDEN, .profile_acl
"RustProxy: denied by access policy\n", .as_ref()
); .is_none_or(|acl| acl.allows(context.client_ip))
|| !route
.acl
.as_ref()
.is_none_or(|acl| acl.allows(context.client_ip))
{
return error(
StatusCode::FORBIDDEN,
"RustProxy: denied by access policy\n",
);
}
if (route.profile_country_access.needs_geoip() || route.country_access.needs_geoip())
&& !country_resolved
{
country = state.geoip.country(context.client_ip);
if capture_country {
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
*exchange.country.lock().unwrap_or_else(|e| e.into_inner()) = country.clone();
}
}
}
if !route.profile_country_access.allows(country.as_deref())
|| !route.country_access.allows(country.as_deref())
{
return error(
StatusCode::FORBIDDEN,
"RustProxy: location country access policy denied this request\n",
);
}
} }
// The admin cookie is never ordinary application state. Strip it BEFORE // The admin cookie is never ordinary application state. Strip it BEFORE
// auth/cache and keep only the removed fragment locally. A verified self-admin // auth/cache and keep only the removed fragment locally. A verified self-admin
+18 -3
View File
@@ -1,3 +1,16 @@
fn request_id_hash(sequence: u64) -> String {
use rand::RngCore;
use sha2::{Digest, Sha256};
let mut nonce = [0u8; 32];
let mut rng = rand::rngs::OsRng;
rng.fill_bytes(&mut nonce);
let mut hash = Sha256::new();
hash.update(sequence.to_be_bytes());
hash.update(nonce);
format!("{:x}", hash.finalize())
}
async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response { async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next) -> Response {
let snapshot = state.config.load_full(); let snapshot = state.config.load_full();
let (frontend_context, route_context) = response_context(&snapshot, &request); let (frontend_context, route_context) = response_context(&snapshot, &request);
@@ -32,6 +45,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
state.metrics.http3_requests.fetch_add(1, Ordering::Relaxed); state.metrics.http3_requests.fetch_add(1, Ordering::Relaxed);
} }
let id = state.metrics.requests.fetch_add(1, Ordering::Relaxed) + 1; let id = state.metrics.requests.fetch_add(1, Ordering::Relaxed) + 1;
let request_id = request_id_hash(id);
if let Some(frontend) = &frontend_context { if let Some(frontend) = &frontend_context {
frontend.metrics.requests.fetch_add(1, Ordering::Relaxed); frontend.metrics.requests.fetch_add(1, Ordering::Relaxed);
} }
@@ -70,7 +84,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
frontend_context.as_deref(), frontend_context.as_deref(),
local, local,
transport_tls, transport_tls,
id, &request_id,
&state.active_listeners, &state.active_listeners,
); );
return response; return response;
@@ -118,6 +132,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
transform_effective_enabled: Mutex::new(None), transform_effective_enabled: Mutex::new(None),
backend: Mutex::new(None), backend: Mutex::new(None),
id, id,
request_id: request_id.clone(),
method: request.method().to_string(), method: request.method().to_string(),
host, host,
path: request.uri().path().chars().take(256).collect(), path: request.uri().path().chars().take(256).collect(),
@@ -148,7 +163,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
traffic: state.traffic.clone(), traffic: state.traffic.clone(),
_permit: permit, _permit: permit,
}); });
if let Ok(value) = HeaderValue::from_str(&id.to_string()) { if let Ok(value) = HeaderValue::from_str(&request_id) {
request.headers_mut().insert("x-request-id", value); request.headers_mut().insert("x-request-id", value);
} }
request.extensions_mut().insert(exchange.clone()); request.extensions_mut().insert(exchange.clone());
@@ -223,7 +238,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
frontend_context.as_deref(), frontend_context.as_deref(),
local, local,
transport_tls, transport_tls,
id, &request_id,
&state.active_listeners, &state.active_listeners,
); );
if response.extensions().get::<RedirectResponse>().is_some_and(|redirect| !redirect.http3_enabled) { if response.extensions().get::<RedirectResponse>().is_some_and(|redirect| !redirect.http3_enabled) {
+2 -2
View File
@@ -364,10 +364,10 @@ pub struct DiagnosticHeaders {
pub request_id: bool, pub request_id: bool,
} }
impl DiagnosticHeaders { impl DiagnosticHeaders {
pub fn apply(&self, headers: &mut HeaderMap, id: u64) { pub fn apply(&self, headers: &mut HeaderMap, request_id: &str) {
headers.remove("x-request-id"); headers.remove("x-request-id");
if self.request_id { if self.request_id {
if let Ok(value) = id.to_string().parse() { if let Ok(value) = request_id.parse() {
headers.insert("x-request-id", value); headers.insert("x-request-id", value);
} }
} }
+65
View File
@@ -45,6 +45,59 @@ impl Default for ProxyTimeouts {
} }
} }
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum MethodPolicyMode {
#[default]
Off,
Allow,
Block,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(default, deny_unknown_fields)]
pub struct MethodPolicy {
pub mode: MethodPolicyMode,
pub methods: Vec<String>,
}
impl MethodPolicy {
pub fn validate(&mut self) -> Result<()> {
ensure!(self.methods.len() <= 32, "method policy allows at most 32 methods");
let mut seen = std::collections::HashSet::new();
for method in &mut self.methods {
*method = method.trim().to_ascii_uppercase();
ensure!(
!method.is_empty() && method.len() <= 32 && method.parse::<http::Method>().is_ok(),
"invalid HTTP method in method policy"
);
ensure!(seen.insert(method.clone()), "duplicate HTTP method in method policy");
}
ensure!(
self.mode == MethodPolicyMode::Off || !self.methods.is_empty(),
"method allow/block policy requires at least one method"
);
Ok(())
}
pub fn allows(&self, method: &http::Method) -> bool {
let listed = self
.methods
.iter()
.any(|candidate| candidate.eq_ignore_ascii_case(method.as_str()));
match self.mode {
MethodPolicyMode::Off => true,
MethodPolicyMode::Allow => listed,
MethodPolicyMode::Block => !listed,
}
}
pub fn allow_header(&self) -> Option<http::HeaderValue> {
(self.mode == MethodPolicyMode::Allow)
.then(|| self.methods.join(", ").parse::<http::HeaderValue>().ok())
.flatten()
}
}
impl ProxyTimeouts { impl ProxyTimeouts {
pub fn validate(&self) -> Result<()> { pub fn validate(&self) -> Result<()> {
for (name, value, max) in [ for (name, value, max) in [
@@ -204,6 +257,9 @@ pub struct Vhost {
pub backend_timing_header: bool, pub backend_timing_header: bool,
#[serde(default)] #[serde(default)]
pub rate_limit: crate::rate_limit::Settings, pub rate_limit: crate::rate_limit::Settings,
/// Optional allow/block policy for HTTP methods. Vhost and location policies stack.
#[serde(default)]
pub method_policy: MethodPolicy,
#[serde(default)] #[serde(default)]
pub basic_auth: crate::basic_auth::Settings, pub basic_auth: crate::basic_auth::Settings,
pub id: String, pub id: String,
@@ -403,8 +459,17 @@ pub struct PathRoute {
pub enabled: bool, pub enabled: bool,
#[serde(default)] #[serde(default)]
pub cache: Option<bool>, pub cache: Option<bool>,
/// Optional reusable ACL profile applied in addition to vhost policy.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub acl_profile_id: Option<String>,
#[serde(default)] #[serde(default)]
pub acl: Option<Acl>, pub acl: Option<Acl>,
/// Location-scoped country allow/block policy, stacked with vhost/profile policy.
#[serde(default)]
pub country_access: crate::rate_limit::CountryAccess,
/// Location-scoped HTTP method allow/block policy, stacked with vhost policy.
#[serde(default)]
pub method_policy: MethodPolicy,
#[serde(default)] #[serde(default)]
pub headers: HeaderRules, pub headers: HeaderRules,
/// Optional origin-path transformation. Query parameters are preserved. /// Optional origin-path transformation. Query parameters are preserved.
+24
View File
@@ -82,6 +82,30 @@ mod default_listener_tests {
); );
} }
#[test]
fn method_policy_normalizes_and_enforces_allow_and_block_modes() {
let mut allow: MethodPolicy = serde_json::from_value(json!({
"mode": "allow",
"methods": ["get", "HEAD", "post"]
}))
.unwrap();
allow.validate().unwrap();
assert_eq!(allow.methods, vec!["GET", "HEAD", "POST"]);
assert!(allow.allows(&http::Method::GET));
assert!(!allow.allows(&http::Method::DELETE));
assert_eq!(allow.allow_header().unwrap().to_str().unwrap(), "GET, HEAD, POST");
let mut block: MethodPolicy = serde_json::from_value(json!({
"mode": "block",
"methods": ["delete"]
}))
.unwrap();
block.validate().unwrap();
assert!(!block.allows(&http::Method::DELETE));
assert!(block.allows(&http::Method::GET));
assert!(block.allow_header().is_none());
}
#[test] #[test]
fn h2c_is_cleartext_only_and_defaults_off() { fn h2c_is_cleartext_only_and_defaults_off() {
let plain: Listener = let plain: Listener =
+14 -1
View File
@@ -8,7 +8,10 @@ impl Default for PathRoute {
match_type: PathMatch::Prefix, match_type: PathMatch::Prefix,
enabled: true, enabled: true,
cache: None, cache: None,
acl_profile_id: None,
acl: None, acl: None,
country_access: crate::rate_limit::CountryAccess::default(),
method_policy: MethodPolicy::default(),
headers: HeaderRules::default(), headers: HeaderRules::default(),
rewrite: PathRewrite::default(), rewrite: PathRewrite::default(),
response: None, response: None,
@@ -28,7 +31,11 @@ impl PathRoute {
&& self.source == Source::ReverseProxy && self.source == Source::ReverseProxy
&& self.backend_id.is_none() && self.backend_id.is_none()
&& self.cache.is_none() && self.cache.is_none()
&& self.acl_profile_id.is_none()
&& self.acl.is_none() && self.acl.is_none()
&& self.country_access.mode == crate::rate_limit::CountryMode::Off
&& self.country_access.countries.is_empty()
&& self.method_policy == MethodPolicy::default()
&& self.headers.is_empty() && self.headers.is_empty()
&& self.rewrite.mode == RewriteMode::Preserve && self.rewrite.mode == RewriteMode::Preserve
&& self.rewrite.value.is_empty() && self.rewrite.value.is_empty()
@@ -67,6 +74,7 @@ impl Vhost {
if let Some(id) = &self.acl_profile_id { if let Some(id) = &self.acl_profile_id {
ensure!(crate::config::valid_id(id), "invalid ACL profile id"); ensure!(crate::config::valid_id(id), "invalid ACL profile id");
} }
self.method_policy.validate()?;
ensure!( ensure!(
!self.frontend.listener_ids.is_empty(), !self.frontend.listener_ids.is_empty(),
"vhost requires at least one named listener" "vhost requires at least one named listener"
@@ -111,8 +119,13 @@ impl Vhost {
// Normalize the untouched pre-0.16 synthetic root route out of the custom // Normalize the untouched pre-0.16 synthetic root route out of the custom
// location list so adding/removing Custom locations cannot delete fallback. // location list so adding/removing Custom locations cannot delete fallback.
self.routes.retain(|route| !route.is_legacy_main_fallback()); self.routes.retain(|route| !route.is_legacy_main_fallback());
for route in &self.routes { for route in &mut self.routes {
route.source.validate()?; route.source.validate()?;
if let Some(id) = &route.acl_profile_id {
ensure!(crate::config::valid_id(id), "invalid location ACL profile id");
}
route.country_access.validate()?;
route.method_policy.validate()?;
ensure!( ensure!(
route.source.is_reverse_proxy() || route.backend_id.is_none(), route.source.is_reverse_proxy() || route.backend_id.is_none(),
"Web Server location cannot pin a reverse-proxy backend" "Web Server location cannot pin a reverse-proxy backend"
+1 -1
View File
@@ -156,7 +156,7 @@ pub(super) fn fastcgi_params(
} }
params.insert("HTTPS".into(), if context.tls { "on" } else { "off" }.into()); params.insert("HTTPS".into(), if context.tls { "on" } else { "off" }.into());
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into()); params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.25.2".into()); params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.25.3".into());
params.insert("REDIRECT_STATUS".into(), "200".into()); params.insert("REDIRECT_STATUS".into(), "200".into());
if let Some(value) = headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()) { if let Some(value) = headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()) {
params.insert("CONTENT_TYPE".into(), value.into()); params.insert("CONTENT_TYPE".into(), value.into());
+1 -1
View File
@@ -180,7 +180,7 @@ pub(super) fn directory_listing(path: &str, entries: &[DirectoryEntry]) -> Vec<u
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>" "<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
)); ));
} }
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.25.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.25.3</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
html.into_bytes() html.into_bytes()
} }
+1 -1
View File
@@ -202,7 +202,7 @@ async fn empty_index_can_go_directly_to_directory_listing() {
let body = std::str::from_utf8(&body).unwrap(); let body = std::str::from_utf8(&body).unwrap();
assert!(body.contains("file.txt")); assert!(body.contains("file.txt"));
assert!(body.contains("DIRECTORY LISTING")); assert!(body.contains("DIRECTORY LISTING"));
assert!(body.contains("RustProxy <b>0.25.2</b>")); assert!(body.contains("RustProxy <b>0.25.3</b>"));
} }
#[tokio::test] #[tokio::test]