This commit is contained in:
Mateusz Gruszczyński committed 2026-10-06 23:08:43 +02:00
1 parent 5d2aaeabe6
commit 9d6614c1b4
62 files changed
+558 -230

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.26.6. compatibility. Current source version: 0.26.7.
## Delivery ## Delivery
+11
View File
@@ -4,6 +4,17 @@
> 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.26.7
- Add HOT and COLD cache occupancy progress bars on `/cache` for configured object and byte limits, with explicit percentage/limit details.
- Verify cache `Age` progression on stored HITs, protect `Age` from custom response-header rules, and clarify that successful validator revalidation legitimately restarts the HTTP freshness age while `X-Cache-Resident-Age` continues to grow.
- Restore downstream compression for cookie-bearing responses only when RustProxy is serving a shared-cache HIT already accepted by cache policy or a local static Web Server response; privacy-sensitive dynamic/cookie responses remain excluded.
- Keep `Content-Encoding`/`Vary: Accept-Encoding` negotiation for gzip, Brotli, Zstandard and deflate, and make the bundled compression tester stop forcing `Cache-Control: no-cache` unless explicitly requested.
- Preserve compatible backend upstream clients across unrelated active-snapshot rebuilds so generation age, generation request count and rotation totals do not reset spuriously.
- Keep age-based and request-count upstream client recycling active while making long-running rotation diagnostics reliable.
- Update the OCI image to ship Certbot with the supported Cloudflare, OVH and RFC2136 DNS plugins in one pinned Python environment, and refresh container/install documentation.
- Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.6.
## 0.26.6 ## 0.26.6
- Persist and reapply backend diagnostics column and sorting preferences as account-scoped user preferences. - Persist and reapply backend diagnostics column and sorting preferences as account-scoped user preferences.
Generated
+1 -1
View File
@@ -1688,7 +1688,7 @@ dependencies = [
[[package]] [[package]]
name = "rust-proxy" name = "rust-proxy"
version = "0.26.6" version = "0.26.7"
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.26.6" version = "0.26.7"
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"
+9 -2
View File
@@ -7,7 +7,13 @@ RUN if [ ! -f Cargo.lock ]; then cargo generate-lockfile; fi \
FROM debian:trixie-slim AS runtime FROM debian:trixie-slim AS runtime
RUN apt-get update \ RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates python3 certbot openssl \ && apt-get install -y --no-install-recommends ca-certificates python3 python3-venv openssl \
&& python3 -m venv /opt/rustproxy-certbot \
&& /opt/rustproxy-certbot/bin/pip install --no-cache-dir \
'certbot==5.8.0' \
'certbot-dns-cloudflare==5.8.0' \
'certbot-dns-ovh==5.8.0' \
'certbot-dns-rfc2136==5.8.0' \
&& rm -rf /var/lib/apt/lists/* \ && rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid 10001 rustproxy \ && groupadd --system --gid 10001 rustproxy \
&& useradd --system --uid 10001 --gid rustproxy --home /var/lib/rustproxy --shell /usr/sbin/nologin rustproxy \ && useradd --system --uid 10001 --gid rustproxy --home /var/lib/rustproxy --shell /usr/sbin/nologin rustproxy \
@@ -17,7 +23,8 @@ COPY --from=build /build/target/release/rust-proxy /usr/local/bin/rust-proxy
USER rustproxy:rustproxy USER rustproxy:rustproxy
ENV RUST_PROXY_DATA_DIR=/var/lib/rustproxy \ ENV RUST_PROXY_DATA_DIR=/var/lib/rustproxy \
RUST_PROXY_PROXY_LISTEN=0.0.0.0:8080 \ RUST_PROXY_PROXY_LISTEN=0.0.0.0:8080 \
RUST_PROXY_ADMIN_LISTEN=127.0.0.1:9090 RUST_PROXY_ADMIN_LISTEN=127.0.0.1:9090 \
PATH=/opt/rustproxy-certbot/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
VOLUME ["/var/lib/rustproxy"] VOLUME ["/var/lib/rustproxy"]
EXPOSE 8080 8443 9090 EXPOSE 8080 8443 9090
ENTRYPOINT ["/usr/local/bin/rust-proxy"] ENTRYPOINT ["/usr/local/bin/rust-proxy"]
+28 -13
View File
@@ -1,9 +1,9 @@
# RustProxy 0.26.6 # RustProxy 0.26.7
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.26.6 release source.** **0.26.7 release source.**
## Layer-4 streams ## Layer-4 streams
@@ -11,7 +11,18 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners. Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
## What changed in 0.26.6 ## What changed in 0.26.7
- Show explicit HOT and COLD cache occupancy progress on `/cache`, including object-count and byte/disk limits from Cache settings plus percentage usage.
- Preserve compatible upstream client pools across unrelated active-snapshot rebuilds so generation age, request count and `Rotations` diagnostics do not restart at zero unless the backend connection policy actually changes.
- Keep age-based backend rotation in the maintenance loop and retain request-count rotation; compatible snapshot reuse now makes the existing counters observable over long-running processes.
- Refresh container documentation and ship the OCI image with one pinned Certbot environment containing all RustProxy-supported DNS plugins: Cloudflare, OVH and RFC2136.
- Review current installation/container/cache/backend lifecycle documentation for stale version or behavior statements.
- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.6 requires no data migration.
0.26.7 retains the 0.26.6 access-policy and administration fixes below.
### Retained 0.26.6 release notes
- Keep backend diagnostics visible-column selection, sort key and sort direction synchronized with the account-scoped administrator preferences whenever preferences are loaded or updated. - Keep backend diagnostics visible-column selection, sort key and sort direction synchronized with the account-scoped administrator preferences whenever preferences are loaded or updated.
- Clear the ACME certificate-creation form after the issuance job finishes successfully, so successful certificates do not leave stale values while failed jobs keep the non-secret form values available for correction. - Clear the ACME certificate-creation form after the issuance job finishes successfully, so successful certificates do not leave stale values while failed jobs keep the non-secret form values available for correction.
@@ -66,7 +77,7 @@ Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost
- Replace backend sort text (`A-Z` / `Z-A`) with direction arrows (`↑` / `↓`). - Replace backend sort text (`A-Z` / `Z-A`) with direction arrows (`↑` / `↓`).
- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.1 requires no data migration. - Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.1 requires no data migration.
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.26.6 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md). The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.26.7 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
## Documentation ## Documentation
@@ -144,7 +155,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.26.6 source archive preserves the resolved `Cargo.lock` supplied with the source tree and The 0.26.7 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
@@ -292,17 +303,21 @@ demotes LRU/idle hot objects. Cold objects survive process restart for the same
configuration revision and are promoted to RAM on a fresh hit. Large fill buffers grow configuration revision and are promoted to RAM on a fresh hit. Large fill buffers grow
incrementally and consume multiple permits. Actual process RSS also includes in-flight incrementally and consume multiple permits. Actual process RSS also includes in-flight
responses, compression buffers and runtime allocations. HIT/MISS/BYPASS, hit count and responses, compression buffers and runtime allocations. HIT/MISS/BYPASS, hit count and
standardized RFC 9211 `Cache-Status` diagnostics are independently optional; required Age remains on cache hits. Optional `X-Cache-Resident-Age` tracks RustProxy residency and now survives revalidation/tier movement. standardized RFC 9211 `Cache-Status` diagnostics are independently optional; required `Age` advances on ordinary cache hits even when the origin does not send `Age`. A successful origin `304 Not Modified` starts a new freshness-age calculation, so the next `Age` may legitimately be `0`; optional `X-Cache-Resident-Age` tracks RustProxy residency across revalidation and tier movement.
Purge is authenticated, CSRF-protected, audited and invalidates both tiers plus older Purge is authenticated, CSRF-protected, audited and invalidates both tiers plus older
fills. See [cache behavior and limits](docs/CACHE.md) and fills. See [cache behavior and limits](docs/CACHE.md) and
[cache diagnostics](docs/CACHE_DIAGNOSTICS.md). [cache diagnostics](docs/CACHE_DIAGNOSTICS.md).
Response compression is streamed and negotiated with Accept-Encoding; enabled Response compression is streamed and negotiated with `Accept-Encoding`; enabled
algorithms are gzip, Brotli, Zstandard and deflate. Privacy-sensitive, ranged, algorithms are gzip, Brotli, Zstandard and deflate, and the selected algorithm is
upgraded, pre-encoded and no-transform responses are excluded. Strong ETags are reported in `Content-Encoding` with `Vary: Accept-Encoding`. Compression is disabled
weakened after transformation, and obsolete content digests are removed. Server by default and must be enabled in Settings. Privacy-sensitive, ranged, upgraded,
policy is applied after generic response rules. Protected framing/identity headers pre-encoded and `no-transform` responses are excluded. Cookie-bearing requests are
cannot be edited through generic rules. also excluded unless the response is an already accepted shared-cache HIT or a local
static Web Server response. Strong ETags are weakened after transformation, and
obsolete content digests are removed. Server policy is applied after generic response
rules. Protected framing/identity headers and cache-owned `Age` cannot be edited
through generic rules.
With the separate demo enabled, `/cache-demo` returns an explicitly public 30-second With the separate demo enabled, `/cache-demo` returns an explicitly public 30-second
object. Enable cache and X-Cache in Settings, then issue this twice without cookies: object. Enable cache and X-Cache in Settings, then issue this twice without cookies:
@@ -383,7 +398,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.26.6.zip python3 make_zip.py /tmp/RustProxy-0.26.7.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.26.7 - Cache occupancy and backend rotation continuity
Current direction: 0.26.6 -> 0.26.7
- [x] Show HOT/COLD cache occupancy progress against configured entry and byte limits on `/cache`.
- [x] Preserve compatible upstream client generations across unrelated runtime snapshot rebuilds so rotation age/counters remain continuous.
- [x] Keep request-count and age-based backend client recycling covered by tests and visible diagnostics.
- [x] Refresh container documentation and include all supported Certbot DNS plugins in the shipped OCI image.
- [x] Review current installation/cache/backend lifecycle documentation for stale behavior statements.
- [x] Keep SQLite schema 8 and saved proxy configuration compatibility with 0.26.6.
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
## 0.26.6 - Access-policy composition and administration fixes ## 0.26.6 - Access-policy composition and administration fixes
Current direction: 0.26.5 -> 0.26.6 Current direction: 0.26.5 -> 0.26.6
+61 -60
View File
@@ -1,19 +1,19 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
e73f72bb57de20471e00ff480c6d253fda2332fae5c7cac8b03e9d41c46107c2 AGENTS.md c39fd74d633dc36cd7caa8d9b6785151b116752b79bd629d4d810cb03e1096cc AGENTS.md
772c0029a0fffae09543aeeefa8f2eb515d335e2999c91b569b828bfc6dd3326 CHANGELOG.md abe0ebe15057929bec551cc1b92cfa25fb7aca616ba7e3bc8d23a4f02082a446 CHANGELOG.md
c4a563a2ede2b8296b5b6b6da21bd0ca47b5763c7eab3cefd3194e8db09c89cb Cargo.lock 91e57afc931cf4cb929c85a5dca872ce1388f93d38645fbeb4bfd77ba89b2787 Cargo.lock
4fdd0088ff59114a81e3ab45f4d1a3cbb5e6f36f86177bd2af7e3ddc5ec08d5c Cargo.toml bd39e691ce35a1677c6c8a540423f83e51351d7aa9bddd090c9a92408ec2fc20 Cargo.toml
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile 239713948df0d54504989f1ab30f5bf0128b90b4e5ac45d50a132aa033323baf Dockerfile
a08f6efc32fa4bd581f75aa35d9d7a01bdd3e9ddbe9fcf1c92ce8a8e5a28167f README.md e88ff59b0e79d0c468178a4d9cbe6405175b61d7201112855c83b72b6e4f0fad README.md
df48dd2780d331a320af6f22a7da07a5b0d35415a254bfd83647d1bad7fdc782 ROADMAP.md f47bd96e96b4e87ab5fb7d319b96f3a57f9e24ce34ab9758aaaa17b255d74390 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs 928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
058955ec300a750b0db9112e6f6ea36cbdbb933c6e3ba53380e6530fc20bca4f debian/README.md 834e8de4387354b23a2cb8c2d484f0f641dfed09472a9db12efcd0e683969a06 debian/README.md
1ed3d7b8c943dd93c3c9089f8220e97fdb7b7ead6702a2ab35c21e356cfc2198 debian/build-package.sh b8963ff89cd98d3232e09449f34efe4a40874a640acc0779e64218b16b195bdf debian/build-package.sh
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh 0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
eba5f2168bae827254421241375dc9fc44f7f49fa88a7a52eb86ac99f2bbc939 debian/changelog d7cd8cfea6ea52aef826e27d6970a700b1b8d1654c8628bbbc8ad3ea9c5b42b8 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
77ff6db7a664f5b70fbf41535c428190960451ceae89a85e2efdc5025e3632b6 dev.sh 5d864e5cc179055e8e9f065923800b8169bb4d79367c2bbeccc24053151981e5 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,25 +33,25 @@ 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
3b04db48df3b6923a534edf9de06753157602c02b07e4663078eb1b20f8dbd90 docker-agent/model.go a5b63a02952283d80ce729fe827d7a4d65e7ea9507229a7b433187d2a4e7c756 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 e2d3f7a9a3d5d47bada8bb50a9e7b3e1c237463197665c97aea83fb288a46a0d docs/ACME_PROVIDERS.md
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
d699f2261a0d7854e49e9b8bae44fa37008a7d2f6340a73cdc37e840722ea211 docs/API.md d699f2261a0d7854e49e9b8bae44fa37008a7d2f6340a73cdc37e840722ea211 docs/API.md
94072c2a60790991a67f1415bf095d2025cb1f65e0a44a3832fa4412ae96a070 docs/API_ACCESS.md 96f0cdc4af8961b69c19d0f8204d467b2b70abf3882f1fe1a540c82cac9c063b docs/API_ACCESS.md
0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md 0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md
cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md fec156479641ffa02c5702dd54f79de8efa1e9768557f2d38e44cd6868bba9e4 docs/CACHE.md
9cb338ed07ac1f92ad491e2099a7e3af63e66262417b1fef24d98c6a791d13fa docs/CACHE_DIAGNOSTICS.md 6266163e17d2921468abdae92c1620266af8351321530e2c9b7dca6f3dd89c88 docs/CACHE_DIAGNOSTICS.md
f8f6cd9b726bfb950a15ac07af4f80a3b6c6131440b018c00563d18aa397af12 docs/CHANGELOG_TECHNICAL_ARCHIVE.md c6003dcbc0b95e2cfa9523b7017186cc8b4826248b588af77111ac5eb15174f9 docs/CHANGELOG_TECHNICAL_ARCHIVE.md
db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER.md d99fddfc982316c4e915cc6437fe339385350c49a2d06d570d42fd0100227e78 docs/CONTAINER.md
9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md 9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md 7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
86bb62f288f2e999994c302716fc9986dba1ac7a26e598dc97cb56a7bc21cfb5 docs/ERROR_LOGGING.md 86bb62f288f2e999994c302716fc9986dba1ac7a26e598dc97cb56a7bc21cfb5 docs/ERROR_LOGGING.md
76a3ad4a6665920896f48717c65e63e3600b51b52c5c359a1fc8cc93a6e124b9 docs/FILE_LOGS.md 27b452643076c390775a88b7c57ee9efbf19f150c18dba22e7fe6e1b67fc7027 docs/FILE_LOGS.md
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
4d794a195fbefeca3c31abf8e28343191197f350a2bed13174fb54dc0cfabcfc docs/INSTALL.md af555f092a554f803c72be533f9a7b1167df1c963d150199ba9b1b55ae440d9e docs/INSTALL.md
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md 3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md 4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md 7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
@@ -62,17 +62,17 @@ e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND
0c2241828e01b12f749562e2ac206a4519908fff63f46eba207b1cffa932dc5b docs/NPM_MIGRATION.md 0c2241828e01b12f749562e2ac206a4519908fff63f46eba207b1cffa932dc5b docs/NPM_MIGRATION.md
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md 712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md 572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
d6b3719013c7aee06d009eb2145d778a38e52ba3aa7d4371f48100962aec3f62 docs/README.md 628d02baad016a6a39e30258d917521a3262daa6063cbc8573fe7ec84d593a8a 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
585ef0ac4492e03adca46949823e5a7538f56d9768090cd4dc080bfce4305b52 docs/ROADMAP_HISTORY.md 585ef0ac4492e03adca46949823e5a7538f56d9768090cd4dc080bfce4305b52 docs/ROADMAP_HISTORY.md
4bf7b3ea0bfe6c6b0ae0b7884b48b10ba29ff3a71611ec96957a3327d3c6ec88 docs/SECRET_STORE_BACKUP.md 4bf7b3ea0bfe6c6b0ae0b7884b48b10ba29ff3a71611ec96957a3327d3c6ec88 docs/SECRET_STORE_BACKUP.md
a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.md 63e033f08acafa0bb6ac3c6ff58ba56a185e34c9ad29f94725837aba4f36b2be docs/SECURITY.md
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md 620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
f8baa8bdc8a67bf4e4a6e6d233435f5a43075a09bf08b08b597fcef1853ce5c5 docs/SETTINGS.md 0c312c26930153a5e7ee77431d3df4d544b5aadbfcbbb1c8aba2a2d0a7d5e656 docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md 3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
57ab62ba11a468b34bbed5bd6604e0a416d030354062596da119f47eedecfbcd docs/TEST_REPORT.md 365d85700ce3672afd419f74f3dfbf774278a08fddfed34e43b35bc168a42eca docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md 8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md
@@ -106,7 +106,8 @@ bf9b4cf4fec2c8ce287e1b3c2e7d4828e101dafe4d6c173f099819f22493d144 docs/V0.25.5_A
da5ec87b9482f2fc506c4334d9365f3bfaec89254a14e5585dd6460326b20e10 docs/V0.26.4_IMPLEMENTATION.md da5ec87b9482f2fc506c4334d9365f3bfaec89254a14e5585dd6460326b20e10 docs/V0.26.4_IMPLEMENTATION.md
0ddf5215c40c8366844350bc6462d8013dba747b6f3e8fbf477985af215e5e8d docs/V0.26.5_IMPLEMENTATION.md 0ddf5215c40c8366844350bc6462d8013dba747b6f3e8fbf477985af215e5e8d docs/V0.26.5_IMPLEMENTATION.md
dd9b5dcdbd53a04fd32458caaf03b56aa335f0153b1a0e332fdf230fec12680e docs/V0.26.6_IMPLEMENTATION.md dd9b5dcdbd53a04fd32458caaf03b56aa335f0153b1a0e332fdf230fec12680e docs/V0.26.6_IMPLEMENTATION.md
ce259052c023e90a7bfb6e39fe82e52b845a8b2a051bfa29883d29e87a9301a2 docs/VHOSTS.md d21acc458442278628e524ba0b45eb7baa06a46633376a4abbcc35a8c34e5ff2 docs/V0.26.7_IMPLEMENTATION.md
0f0e0e2920c76b2f82f38c6e60ed992a532c63851f593f2a1fa59e8d0014bb41 docs/VHOSTS.md
09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md 09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md
f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md
c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md
@@ -120,14 +121,14 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json 5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
75d054c99671e6830b56a6e38a61abdfc33af610c6f91f085de0af3a3f542d9b grafana/README.md 04b72dfad46afa2efbc9ca31bbc8a3598c087d6d37c3511fcc2a2b371f54d801 grafana/README.md
184d2578e0cd93031790613d91e964c588e9caa237e7f1cae7708dc6107ef1ea grafana/rustproxy-overview.json c294a5dcb94c1cf7edf72243bb533cc487a17452a7c0ac8c72f23cce0ec42fc6 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
5a36a455c9ed45749aca05fe28b3d3036e49cd748c6b89c2334973e7b070f2cb langs/en_US.json 5a36a455c9ed45749aca05fe28b3d3036e49cd748c6b89c2334973e7b070f2cb langs/en_US.json
ec860b4940fd237b8336154824b40b8f09fc009d3baa505771fc56c9ba2bda9e langs/pl_pl.json ec860b4940fd237b8336154824b40b8f09fc009d3baa505771fc56c9ba2bda9e langs/pl_pl.json
b72dc4f3ada8a61070e46d7d08ba01cd4736a3c37ea043088eafeafacc673622 make_zip.py 8b258ac4654a046dc649c7fa8cef273c0f9431c9a12ea9b772dd0a1c95bb281f 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
@@ -146,11 +147,11 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py 20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
af078d88007da9a72e8470e9205a2f360b1e665a519114f9a5ed1951cf948d45 scripts/check_source.py f79fa03aa2b5f0c04d37c108af55d8c43f9faf402f16553d5348c33a088e6108 scripts/check_source.py
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py 187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
b3488a5ea97a54a8bc6dd766e76ffb3f41aa9011a6731042bffc473d2d5f84fd scripts/deploy_core.py fe28d6d580a53d95a0fc318ebc4da5bbc71a23781bb827158848934985477035 scripts/deploy_core.py
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
985a2aea844b2848ee410ec7798a431689b8a2f7709adaa79ce236ed2318f3b0 scripts/docs_check.py 6288336261c8affd24de4456eafe84e2d9d82efebfb7c9b59d4deffc0779fd45 scripts/docs_check.py
d391681dc3f6786552ca44929979286a5c76a2b86785635871b59d2eb4d73389 scripts/langs_check.py d391681dc3f6786552ca44929979286a5c76a2b86785635871b59d2eb4d73389 scripts/langs_check.py
2e35b7c78887fd56f83ca9017caa168630963a9f47da71dfc53b1a8f9ad436c2 scripts/memory_contracts.py 2e35b7c78887fd56f83ca9017caa168630963a9f47da71dfc53b1a8f9ad436c2 scripts/memory_contracts.py
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py
@@ -160,8 +161,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
6fd9faad0842dae9ccde41eea0c5368a234b6aba396b73620cd5eeee83d6786b scripts/preflight.py d9238a75848a10621d518c26d5a00b76aedb393502e8e7c907e7ebc11ef3765a scripts/preflight.py
167369ca944994a71323e07ef097a4f1d7d93a6a6aa42beb86ced05fb882d5aa scripts/release_smoke.py 9d92089846dd1d850652bc464e78496d37bd987dfac8ecf8878c799986288272 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
@@ -173,11 +174,11 @@ ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_s
1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py 1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py
295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py 295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py
6fefb5c459aacdef3dcabe9c5e6e7718a2145a0190b165c29d7a46216ac53bad scripts/source_module_contracts.py 6fefb5c459aacdef3dcabe9c5e6e7718a2145a0190b165c29d7a46216ac53bad scripts/source_module_contracts.py
0b4dd62fee4205a17b9ccca27e5a1e91bc91bddbc31ed59d099315b4e1035964 scripts/swagger_assets.py d22d858bceb7434fdc228c293c05a677316146861e3adca81a4d9fca92ee826d scripts/swagger_assets.py
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py 538b5f6cfa738355a729940ee3d92e7ab3dc25ca69ae330ddaa08f831775b8ba scripts/test_compression.py
712564171eea271b7e5e11a5cb72301a58674eb0f36e30d09defba6be7136583 scripts/upgrade.py f220969197c7f70e2f68b8527ecb66f57d2c267f9960543e8bb786e4ab8d5838 scripts/upgrade.py
0aaa80453e25aae7ea953fadb91b3ae3eaff627977c81c9ab5a2779f9a93582f scripts/web_assets.py 0aaa80453e25aae7ea953fadb91b3ae3eaff627977c81c9ab5a2779f9a93582f scripts/web_assets.py
05653715b1c3f95a9c1160da4493333b63422db57ce15b41f5de1bd28c5b814b scripts/web_server_contracts.py 00a80203c63b16cc2ccf0fe95260555352adb2ce22930593165262cd2e10769d scripts/web_server_contracts.py
ced042b5e283fd40fe25c261457dceb57d946dffebd89876cd80d69b92823932 scripts/web_service_ui_test.mjs ced042b5e283fd40fe25c261457dceb57d946dffebd89876cd80d69b92823932 scripts/web_service_ui_test.mjs
ad62f73bb42906d617cc1b479f02d4648c7324cb89fc11d2c6aa6bf13774eec9 src/acl.rs ad62f73bb42906d617cc1b479f02d4648c7324cb89fc11d2c6aa6bf13774eec9 src/acl.rs
ea272433e7f92e983509a8ab1aa54242440abef21df526c2f99944ef279e09b9 src/acl/tests.rs ea272433e7f92e983509a8ab1aa54242440abef21df526c2f99944ef279e09b9 src/acl/tests.rs
@@ -190,13 +191,13 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi
55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs 55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs
85f00f8157e83a152cc531f4cfecd0b1472ce269be9c8eb1ac17ad0693748b42 src/admin/certificates.rs 85f00f8157e83a152cc531f4cfecd0b1472ce269be9c8eb1ac17ad0693748b42 src/admin/certificates.rs
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs 1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
1af365b1a2c511be05e424a9f2195c0d6afce2751c94f1648e1d30f5c9de80b8 src/admin/documentation/documents.rs 953f88620657c0e3cc347d29d2589f8c150b5ef7c6b9403561329a52a7d6c53b src/admin/documentation/documents.rs
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs 67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs 002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs
d3210e037481e1f95e840550428e088ffd83ec9a42f1810e4c976298fb555a9a src/admin/file_logs.rs d3210e037481e1f95e840550428e088ffd83ec9a42f1810e4c976298fb555a9a src/admin/file_logs.rs
beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs
58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs 58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs
1bc2505f79753c81c7a378ba6bb12a52b9034c19d272c7b200c97a26dccba4be src/admin/openapi.json 64a8ceff5f5919b78b7cddd64c2cd47f28c776044fecabf9efcca93e0ae74a75 src/admin/openapi.json
a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs
e9ea015aabdfdbb2f8dd72ea734002b5d648dc1bbef4cc4a0b85d2bcc059e9a5 src/admin/preferences.rs e9ea015aabdfdbb2f8dd72ea734002b5d648dc1bbef4cc4a0b85d2bcc059e9a5 src/admin/preferences.rs
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
@@ -217,8 +218,8 @@ f869e4da5de149fc04ce1aab658befeaf3208c862f149ca976235d15832c3cdb src/admin/test
9eccd075880950e9100c7a8cca936d92f0c7106ce5140515deb3cc57f03c2bf0 src/admin/web/app/core.js 9eccd075880950e9100c7a8cca936d92f0c7106ce5140515deb3cc57f03c2bf0 src/admin/web/app/core.js
6f15cbcfa47adf98b5da962811fbbc6731880bf99f128b372b6f9f41391eeed1 src/admin/web/app/preferences.js 6f15cbcfa47adf98b5da962811fbbc6731880bf99f128b372b6f9f41391eeed1 src/admin/web/app/preferences.js
5891cbb5bfe7219ecb85d6f8d1794be1c3e4593fd3caa0668b53bd4121d083ce src/admin/web/app/runtime.js 5891cbb5bfe7219ecb85d6f8d1794be1c3e4593fd3caa0668b53bd4121d083ce src/admin/web/app/runtime.js
31e716be0ea45b84a56fc656b3d7529853d3d0a56698592a16375ff689b90956 src/admin/web/app/settings.js 7968cb16d0f2da862261f3dfafd98392d639827a4562c8b2df6b345f9533cf53 src/admin/web/app/settings.js
642d013e2e18dc8a8eccc7a2fe93d6570521dc9a0644beac7af4bbe8c5cea1ee src/admin/web/cache-panel.js 74777adaba5a448f3a9abf3a46d91efc6eb81ff6f3b5441e5b99a9e1d6c09af0 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
d4aef376cb44b2a13174dca1cb9383f64c04b0b93279f4f0b4805e76002548a1 src/admin/web/docs.css d4aef376cb44b2a13174dca1cb9383f64c04b0b93279f4f0b4805e76002548a1 src/admin/web/docs.css
@@ -234,7 +235,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js
631860ab3793ffd827c7cf19ddd049d14419509f2c2d8b27994b0e54baa0e32a src/admin/web/host404.js 631860ab3793ffd827c7cf19ddd049d14419509f2c2d8b27994b0e54baa0e32a src/admin/web/host404.js
471592f1d49c09f7fcae6c9918974f365c786b92d5a7090ebe0fcf8da76375e1 src/admin/web/index.html 1167c964000bb5dae1ba92b850ff76162570af340029d8788296160adc9f67b5 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
@@ -246,7 +247,7 @@ f1ab20afa09f75f3635511756e36afea46a7917f04937d6f4e89c4e9f3f43204 src/admin/web/
f5831c2188233e425beecedc9e5e2829b620f80fc5326b73198024d27b81d1e7 src/admin/web/style.css f5831c2188233e425beecedc9e5e2829b620f80fc5326b73198024d27b81d1e7 src/admin/web/style.css
a8a73a4fa849ae49391fcd6143e900c515c8b577f71b55cbae9701fa9f5bbe47 src/admin/web/traffic-panel.js a8a73a4fa849ae49391fcd6143e900c515c8b577f71b55cbae9701fa9f5bbe47 src/admin/web/traffic-panel.js
e686d68fd6a368110a85dc4c5dc64e8093950d20d63e9aa44790c03dea6fc193 src/admin/web/transform-panel.js e686d68fd6a368110a85dc4c5dc64e8093950d20d63e9aa44790c03dea6fc193 src/admin/web/transform-panel.js
6856c230c4f6b18145e16efb6636433f8b21ce3ec5d34246d6561f0150ea0554 src/admin/web/vhosts/core.js 106db335a6dd3e326955241e1d67caa5562a427b0356e7433cd0d92087342ce8 src/admin/web/vhosts/core.js
4d4ee863a3d77e5213ae4bdcad1f533a6222353b9c32e0b5f12f7ea60ab62bea src/admin/web/vhosts/editor.js 4d4ee863a3d77e5213ae4bdcad1f533a6222353b9c32e0b5f12f7ea60ab62bea src/admin/web/vhosts/editor.js
e0011edc708d245cb514be170c1bbe846a0298b81d86276fe29af05010d215a4 src/admin/web/vhosts/render.js e0011edc708d245cb514be170c1bbe846a0298b81d86276fe29af05010d215a4 src/admin/web/vhosts/render.js
b3d8d3e22e4cbab10c5d588c268da6eb3075a9ec857728ce6b5d56e3ffd3c9a6 src/admin/web/web-server.js b3d8d3e22e4cbab10c5d588c268da6eb3075a9ec857728ce6b5d56e3ffd3c9a6 src/admin/web/web-server.js
@@ -263,12 +264,12 @@ c8ee6afde0e463828757a9c2aca25506eb0a7b3b9369f4ec955857ab04b1f9d1 src/backup/val
426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs 426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs
3d4b3986f54bed99212063b6d135d40a202d556a3e4d253e24c74be4cb9e8ebc src/balance/health.rs 3d4b3986f54bed99212063b6d135d40a202d556a3e4d253e24c74be4cb9e8ebc src/balance/health.rs
dc9335e018d61cddfe3b4c9fa480913f4444c83e4fb1384e3b046cc40940a2c3 src/balance/lease_runtime.rs dc9335e018d61cddfe3b4c9fa480913f4444c83e4fb1384e3b046cc40940a2c3 src/balance/lease_runtime.rs
91c5cef82ece69765858c22c61feb2fb90f38d292cd5bce7ee4fca8f4cb5cd42 src/balance/model.rs 12b2a755b18fa4bdd00d1ddd05ae5ef0aabcaf2158e298ee28d6171691cef449 src/balance/model.rs
b5bb2e5d389fc0290c362af8b28c7d625c836876e308bbdcdd7cffcfee2e5e6d src/balance/pool_core.rs f96f359d24fab77f37889a6f802c1e77d5278a826bbf6fb6f822b262fe41c82d src/balance/pool_core.rs
477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs 477132dd381e23f1be197c32ff58a0925f41d3e6aa63fe77c10973cefb2b76f6 src/balance/pool_selection.rs
d3ee9cc9869fd0934d1f00fd86943979bac9251630ccf7a2b07e684c7955967e src/balance/pool_view.rs d3ee9cc9869fd0934d1f00fd86943979bac9251630ccf7a2b07e684c7955967e src/balance/pool_view.rs
7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/balance/tests.rs 7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/balance/tests.rs
7c48afa4289beb37e238d4fbc1d9e6f903e7fe3f05d1d7b2cfbdd9103cbedd89 src/balance/tests/part_01.rs 2cf358291fe824c9809e43c6085a3388a294c6bf313a6e82a3733c0d31b9edc9 src/balance/tests/part_01.rs
490b38a8b1472dbd9fcb823ec0ed0e92498fb9528b982bcee340c8579e89ae36 src/balance/tests/part_02.rs 490b38a8b1472dbd9fcb823ec0ed0e92498fb9528b982bcee340c8579e89ae36 src/balance/tests/part_02.rs
ccb3e1988d6ae5825450e894eea896d53ae9fac150d0175ea5e742413e55105a src/basic_auth.rs ccb3e1988d6ae5825450e894eea896d53ae9fac150d0175ea5e742413e55105a src/basic_auth.rs
9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs 9a8858b7ff461a4d961ba701ca3ed6eaec15c3f7f05eef8e0e30edc63617605a src/basic_auth/tests.rs
@@ -308,18 +309,18 @@ fb3074fa87b19a66db66842f2ab3a0728646293ca2154ab26f7865a58cda56ac src/cache/test
578b18cac918ba51fad775eca188f645b228500e59eb1790f783a0b8d0245b7b src/cache/tests/tests.rs 578b18cac918ba51fad775eca188f645b228500e59eb1790f783a0b8d0245b7b src/cache/tests/tests.rs
c33013089bf9e75a07a5b43f1030acaba8aba4098796e393ed4bad784f4fff96 src/cache/tests/tests/part_01.rs c33013089bf9e75a07a5b43f1030acaba8aba4098796e393ed4bad784f4fff96 src/cache/tests/tests/part_01.rs
2f579a67e73817907525651f32c65db9f4175d55c9ea34773e988a7764a7fc8b src/cache/tests/tests/part_02.rs 2f579a67e73817907525651f32c65db9f4175d55c9ea34773e988a7764a7fc8b src/cache/tests/tests/part_02.rs
02d1b147db43a5f8750ba63ceeb71d127ff55da045079b2b0c1fb4f8aef3b867 src/cache/tests/tests/part_03.rs f4c56db79cf939a58c25253ce9abb18faeb90fe1454bfafec7c1f2e3385f78ce src/cache/tests/tests/part_03.rs
cdc41a7241739b89297494323f29e0230fa06abbd74e219fedfda11df1db4334 src/cache/tests/tests/part_04.rs cdc41a7241739b89297494323f29e0230fa06abbd74e219fedfda11df1db4334 src/cache/tests/tests/part_04.rs
541a62b749440aa1099cf0fc8a2bd31eac0c082ccef35df22c1e049e7928e216 src/cache/tests/v022_cache_tests.rs 541a62b749440aa1099cf0fc8a2bd31eac0c082ccef35df22c1e049e7928e216 src/cache/tests/v022_cache_tests.rs
15e85357c3ddf4fc2283d621f5f4cd8ca309275e06677020299d4105f0475949 src/cache/tests/v022_cache_tests/part_01.rs 15e85357c3ddf4fc2283d621f5f4cd8ca309275e06677020299d4105f0475949 src/cache/tests/v022_cache_tests/part_01.rs
7ee652a781081c0b53d39dff213c2bda38e405a18a3d0b908f97cd3ac51d556f src/cache/tests/v022_cache_tests/part_02.rs 7ee652a781081c0b53d39dff213c2bda38e405a18a3d0b908f97cd3ac51d556f src/cache/tests/v022_cache_tests/part_02.rs
f9a3c4f0c7363819a0904860531078df89e2ee1ed5bcff7047986e40c9f38a0c src/cache/tickets.rs f9a3c4f0c7363819a0904860531078df89e2ee1ed5bcff7047986e40c9f38a0c src/cache/tickets.rs
992e62f8240aa462b9c670eea6e46e86391d4eb82402f3278c23ad73acc89b1f src/compression.rs 04c2e149ee74e1ae0294e60803d111b85dcccb84381653616f7c0ba4ee35d484 src/compression.rs
ad47db6519e471ec53a408129bb10a794033430e28fcd3d9a4cf273669900829 src/compression/tests.rs 574f8ad7de9366f6d836d5879ba601c46305367cf5ec4ee84c3587722cc5910e src/compression/tests.rs
7921ae451ccd65d647165b02919bd97399399da471454a6a1a1f63e682b76fc8 src/config.rs 7921ae451ccd65d647165b02919bd97399399da471454a6a1a1f63e682b76fc8 src/config.rs
36c8774fe6fdc44d13286b7688726ceaa9b55cee4c63113689df63456cbecaac src/config/backend.rs 7ba52d3de4d77d1f3cad1b10de418953adc33901727a0791980847ae223929aa src/config/backend.rs
8b39e92594133c32b6e113c67849fc10f51ceec9e3bbf69211779f6628d0e001 src/config/compile.rs 8b39e92594133c32b6e113c67849fc10f51ceec9e3bbf69211779f6628d0e001 src/config/compile.rs
bb1059f07a0e7bbffeb2575334bbe5e95d1f3167c9fe0769696f585523a42471 src/config/model.rs 02be5c799ad0dece8b15957e056294f02807e58af1060c0d9c1998003ef7d6d2 src/config/model.rs
4d2a4a078e890a9d1582e2cc6f9b8f0e8c2c676c4c94bc927bffdbe55e932610 src/config/routing.rs 4d2a4a078e890a9d1582e2cc6f9b8f0e8c2c676c4c94bc927bffdbe55e932610 src/config/routing.rs
9662674d17740df4999f28bec42a2fbd3453a93e591c88f0796edc396bb23a18 src/config/snapshot.rs 9662674d17740df4999f28bec42a2fbd3453a93e591c88f0796edc396bb23a18 src/config/snapshot.rs
7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/config/tests.rs 7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/config/tests.rs
@@ -339,7 +340,7 @@ fe571c85a924c827097f4be01b3a827a16bc5dd17a55fbf991ee3ad016641305 src/docker/tes
057e652f216a1478638cbb53b1c64ac88c7f413a65825caacbf893c5af004a63 src/forwarding/tests.rs 057e652f216a1478638cbb53b1c64ac88c7f413a65825caacbf893c5af004a63 src/forwarding/tests.rs
72215ef6911893f55cc82278169268f03d869b94728f118bf007fc555d609fde src/geoip.rs 72215ef6911893f55cc82278169268f03d869b94728f118bf007fc555d609fde src/geoip.rs
e40171e72327bc4350ba95cedacde2f3c9148c26a5a232d9f790f4449f20a685 src/geoip/tests.rs e40171e72327bc4350ba95cedacde2f3c9148c26a5a232d9f790f4449f20a685 src/geoip/tests.rs
650818b60f76e1ae1a26c6fbf71d7ba63bc85dcb88754a592e8da87b5938e5af src/headers.rs 4e009ec10151a068fa83cf93cac78d2254d152e0386442586d6f5860ab2710b4 src/headers.rs
01cb98570ec388170420883e4ba18b2bf85ad1af97854dbc3a12b135db511c5c src/headers/tests.rs 01cb98570ec388170420883e4ba18b2bf85ad1af97854dbc3a12b135db511c5c src/headers/tests.rs
d0cd8589321595309834690fa9ddb98488886bc98f34ea45aa0ccb3035412fb5 src/http3.rs d0cd8589321595309834690fa9ddb98488886bc98f34ea45aa0ccb3035412fb5 src/http3.rs
2165a0a4b27d621985e2030326ad52b8bbd59d7b9e4f092f72f7e92b4e58b497 src/http3/body.rs 2165a0a4b27d621985e2030326ad52b8bbd59d7b9e4f092f72f7e92b4e58b497 src/http3/body.rs
@@ -353,11 +354,11 @@ a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/fo
17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs 17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs 3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
46732ed6d9cf2a33031624f941b8318d2676fb1b6bb5142e96820301f9fa1ac4 src/main/admin_runtime.rs 46732ed6d9cf2a33031624f941b8318d2676fb1b6bb5142e96820301f9fa1ac4 src/main/admin_runtime.rs
62b865444f0479cd4d1adcef16f6b2509c089bb306520c31812f39aaffcb0d6b src/main/bootstrap.rs aba35a32fac19aaa7f1da172e995a21d0d3a40724b56b50e60c3897ac6bb75fc src/main/bootstrap.rs
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs 5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs 4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs 0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
0d65d9c4c1f030db5a1e0ba19f8691426bde0882408a5f2085b05825dc928982 src/main/tasks.rs e5cd6ac48b927bb864991ebf10fe0e21ac918fbf1b4334709a0df250142ccd51 src/main/tasks.rs
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs 49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs 201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs 49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs
@@ -388,7 +389,7 @@ c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/head
f6aef54204b5bb9305dd1367818c7ffd3cb273008404c9fab0317af1d13ba7a6 src/proxy/mod.rs f6aef54204b5bb9305dd1367818c7ffd3cb273008404c9fab0317af1d13ba7a6 src/proxy/mod.rs
d75ff3e38affa49f9a8b9d6801454c1449458e5fbf07e08d51aa3f42abebafd5 src/proxy/tests/authority_tests.rs d75ff3e38affa49f9a8b9d6801454c1449458e5fbf07e08d51aa3f42abebafd5 src/proxy/tests/authority_tests.rs
622d44511b82883a34830d0ae3ea665fd40b58eaa85653f58588db92d55a17c3 src/proxy/types.rs 622d44511b82883a34830d0ae3ea665fd40b58eaa85653f58588db92d55a17c3 src/proxy/types.rs
4ba5a477e6f95d59d5413e6026ba90d1d1b132b0bdf47d344b0a66aab2f9db2a src/proxy/upstream_helpers.rs 352260ef9e956534bc91b5cb888e3f8e881913c0bb988e6af95e49933d98814d src/proxy/upstream_helpers.rs
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs 69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
d05e655ba0b3b322c657aaa9c2cbe7a0181064a896026b5eeaca347c4a0b7831 src/proxy/websocket/handshake.rs d05e655ba0b3b322c657aaa9c2cbe7a0181064a896026b5eeaca347c4a0b7831 src/proxy/websocket/handshake.rs
b21dc9cf3817dd4e3273ce25887d63d7a772c477b86ca57f8d5b72b8154456e7 src/proxy/websocket/tests.rs b21dc9cf3817dd4e3273ce25887d63d7a772c477b86ca57f8d5b72b8154456e7 src/proxy/websocket/tests.rs
@@ -451,8 +452,8 @@ a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/
76b3cb4ebdba9cbac8e388d3a9d74bd2055db564d8e31869a60e7a439b46f7b3 src/transform/tests.rs 76b3cb4ebdba9cbac8e388d3a9d74bd2055db564d8e31869a60e7a439b46f7b3 src/transform/tests.rs
639ed68b2c62a9c8ef23beac480ed4ecb0a7d31672177c9e0bde6c223137fac1 src/update_check.rs 639ed68b2c62a9c8ef23beac480ed4ecb0a7d31672177c9e0bde6c223137fac1 src/update_check.rs
c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs
0399f6211c6e33fa12298acc40957f1532ace235c3950c89050a3450ee127299 src/upstream.rs a67e58a597a5eeb7efe605ae6e438a8f84ff6a1cedf8cd228141cb95551872a8 src/upstream.rs
19d707f8b14ea1b2828a8d51bbacf7505bb0fad79c458d855b80297b879e9ccc src/upstream/client.rs 225e3078e6775ffaa6dc6f12bc87503a123983cbb347edf38cae4c37ff02a5db src/upstream/client.rs
4aa2420a92d33c50f7027f57ac4eccdc050bdb7e9ec8fa6e0eaa809228b71cd8 src/upstream/client_tests.rs 4aa2420a92d33c50f7027f57ac4eccdc050bdb7e9ec8fa6e0eaa809228b71cd8 src/upstream/client_tests.rs
3a4b097c0d5c0d88fbe881d84bd26a89789f68e4c46ea68b9e4a923dd4977d0b src/upstream/tests.rs 3a4b097c0d5c0d88fbe881d84bd26a89789f68e4c46ea68b9e4a923dd4977d0b src/upstream/tests.rs
e4c1ff8f53112e7cfad101a266355f7e47b82895480db431662f4cc59a1d9702 src/vhost.rs e4c1ff8f53112e7cfad101a266355f7e47b82895480db431662f4cc59a1d9702 src/vhost.rs
@@ -464,7 +465,7 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
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
4895bc9f2730eb9ccd9f5a8ef98d69835c6303972d579ad540157141385853b5 src/web_server/fastcgi/protocol.rs 50a04408a8c2f947f7866542e528f2e07e626ea618be4d9ef3f3848cc0c1f321 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
@@ -474,14 +475,14 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
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
18d8c2e09b1883a3b27c261631dcdf91d5c33697b2ece716393ef4b698a3deb7 src/web_server/response/file.rs 768fb0efa96fa1c3201c02cd8b9ae867bfa4267581884670012ca959e86c6a7c 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
5a50123c9d49bc6a8e7f06bf00220a8308a5e14527e81ae9a5cba155294a7cec src/web_server/tests/part_02.rs 4d8749d78e2d44dd502da21d35acc3a6192644de0f43c341b993d068e6570ded 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.26.6. This directory builds the `rustproxy` binary package for RustProxy 0.26.7.
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.26.6_*.deb' -o -name 'rustproxy_0.26.6_*.changes' -o -name 'rustproxy_0.26.6_*.buildinfo' \) \ find .. -maxdepth 1 -type f \( -name 'rustproxy_0.26.7_*.deb' -o -name 'rustproxy_0.26.7_*.changes' -o -name 'rustproxy_0.26.7_*.buildinfo' \) \
-exec cp -f {} dist/ \; -exec cp -f {} dist/ \;
echo "Packages copied to $ROOT/dist" echo "Packages copied to $ROOT/dist"
+9
View File
@@ -1,3 +1,12 @@
rustproxy (0.26.7) stable; urgency=medium
* Show HOT/COLD cache occupancy against configured limits.
* Preserve compatible backend clients so rotation diagnostics remain continuous.
* Refresh container Certbot/plugin packaging and documentation.
* Keep schema 8 and saved configuration compatibility with 0.26.6.
-- RustProxy Maintainers <noreply@example.invalid> Tue, 06 Oct 2026 22:20:00 +0200
rustproxy (0.26.6) stable; urgency=medium rustproxy (0.26.6) stable; urgency=medium
* Fix composed ACL allow semantics for IP ranges and allow-only countries. * Fix composed ACL allow semantics for IP ranges and allow-only countries.
+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.26.6 RustProxy 0.26.7
./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.26.6...\n' >&2 printf 'Building RustProxy 0.26.7...\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.26.6" const version = "0.26.7"
type config struct { type config struct {
Listen string Listen string
+4
View File
@@ -33,6 +33,10 @@ or execute sudo. Check the CLI installation with `bash dev.sh acme --doctor` and
DNS uses the API/plugin appropriate to the selected provider, not a Cloudflare DNS uses the API/plugin appropriate to the selected provider, not a Cloudflare
Tunnel integration or a hand-written replacement for ACME. Tunnel integration or a hand-written replacement for ACME.
The shipped OCI image already contains Certbot plus the Cloudflare, OVH and RFC2136
plugins in one dedicated Python environment. Host/source installations remain under
the operator's package-management policy and must provide the plugins they use.
## HTTP-01 requirements and scope ## HTTP-01 requirements and scope
Public TCP port **80** for every requested name must reach an active plaintext Public TCP port **80** for every requested name must reach an active plaintext
+1 -1
View File
@@ -1,4 +1,4 @@
# Administration API access - 0.26.6 # Administration API access - 0.26.7
## Sign-in boundary ## Sign-in boundary
+5 -3
View File
@@ -246,9 +246,11 @@ existing deduplication guard.
`GET /api/cache` reports settings, HIT/MISS/BYPASS, hot/cold counts and bytes, `GET /api/cache` reports settings, HIT/MISS/BYPASS, hot/cold counts and bytes,
configured COLD byte/object limits, promotions/demotions, errors, fill pressure, object configured COLD byte/object limits, promotions/demotions, errors, fill pressure, object
views and per-domain reason counters. The overview dashboard shows COLD disk occupancy views and per-domain reason counters. The `/cache` dashboard shows HOT and COLD
separately from RAM accounting. Hit ratio is `hits / (hits + misses)` and intentionally occupancy against the configured object-count and byte limits with progress bars and
excludes BYPASS traffic. explicit percentages. HOT byte occupancy uses the charged/allocated HOT value rather
than logical body bytes; COLD byte occupancy uses persisted storage bytes. Hit ratio is
`hits / (hits + misses)` and intentionally excludes BYPASS traffic.
`POST /api/cache/purge` requires the existing administrator session, same-origin marker `POST /api/cache/purge` requires the existing administrator session, same-origin marker
and CSRF token. `?vhost=<id>` purges only that vhost scope; omitting it purges all cache and CSRF token. `?vhost=<id>` purges only that vhost scope; omitting it purges all cache
+11 -5
View File
@@ -42,14 +42,20 @@ explicit invalidation.
These are different clocks: These are different clocks:
- `Age` is HTTP freshness age. It can already be thousands of seconds when RustProxy - `Age` is HTTP freshness age. It can already be thousands of seconds when RustProxy
receives an object because the origin/CDN `Date`/`Age` contributes to it. receives an object because the origin/CDN `Date`/`Age` contributes to it. On an ordinary
fresh HIT it advances from RustProxy's stored receive time even when the backend never
supplied an `Age` header. A successful validator revalidation (backend `304`) starts a
new freshness calculation, so the next `Age` can legitimately be `0`. Requests carrying
`Cache-Control: no-cache`, `max-age=0` or `Pragma: no-cache` can therefore make repeated
validated HITs appear with `Age: 0`.
- `X-Cache-Resident-Age` is RustProxy-specific and counts whole seconds that the stored - `X-Cache-Resident-Age` is RustProxy-specific and counts whole seconds that the stored
representation has remained resident in this cache. representation has remained resident in this cache.
In 0.11.5 resident age is preserved across a successful backend 304 and across Resident age is preserved across a successful backend 304 and across hot-to-cold/cold-to-hot
hot-to-cold/cold-to-hot movement. An immediate first HIT can legitimately show `0` movement. An immediate first HIT can legitimately show `0` because less than one full second
because less than one full second has elapsed; a long-lived object should then keep has elapsed; a long-lived resident object should then keep increasing rather than repeatedly
increasing rather than repeatedly resetting to zero. resetting to zero. Use this header when the diagnostic question is cache residency rather than
HTTP freshness.
## Practical HIT test ## Practical HIT test
+14
View File
@@ -7,6 +7,20 @@
# Changelog # Changelog
## 0.26.7
### Backend client lifecycle and cache diagnostics
- Preserve compiled upstream clients when an active-snapshot rebuild leaves the backend origin, address policy, keepalive policy and connect timeout unchanged. This retains Hyper connection pools together with generation age, per-generation request counters and cumulative rotation totals instead of resetting those diagnostics on unrelated configuration rebuilds.
- Keep incompatible backend changes isolated: changing the origin or any client-construction input still creates a fresh upstream client and fresh generation state.
- Add a pool regression proving compatible snapshot rebuilds reuse the existing backend/client object; existing upstream-client tests continue to cover age-triggered recycling.
- Extend `/cache` with explicit HOT and COLD occupancy meters for both configured object limits and byte/disk budgets. Percentages are clamped for progress rendering while the raw counters remain visible.
### Container certificate tooling
- Build the OCI runtime image with a dedicated Certbot virtual environment containing the Cloudflare, OVH and RFC2136 DNS plugins supported by RustProxy, all pinned with the same Certbot release.
- Refresh container documentation so it no longer tells operators that those provider plugins must be added separately.
## 0.26.6 ## 0.26.6
### Compiler follow-up ### Compiler follow-up
+7 -6
View File
@@ -9,7 +9,7 @@ each Docker host, so the proxy container never needs Docker-socket access. See
Build locally with a normal OCI builder: Build locally with a normal OCI builder:
```bash ```bash
docker build -t rustproxy:0.22.3 . docker build -t rustproxy:0.26.7 .
``` ```
Persist `/var/lib/rustproxy`; it contains the SQLite database, certificates and the Persist `/var/lib/rustproxy`; it contains the SQLite database, certificates and the
@@ -22,7 +22,7 @@ docker volume create rustproxy-data
docker run --rm -d --name rustproxy \ docker run --rm -d --name rustproxy \
-p 8080:8080 \ -p 8080:8080 \
-v rustproxy-data:/var/lib/rustproxy \ -v rustproxy-data:/var/lib/rustproxy \
rustproxy:0.22.3 rustproxy:0.26.7
docker exec rustproxy rust-proxy admin setup-token docker exec rustproxy rust-proxy admin setup-token
``` ```
@@ -32,10 +32,11 @@ RustProxy configuration and choose a certificate, then bind the administration
listener to a non-loopback address. Publish the admin port only after HTTPS is listener to a non-loopback address. Publish the admin port only after HTTPS is
active. Plain remote HTTP keeps the legacy explicit risk gate. active. Plain remote HTTP keeps the legacy explicit risk gate.
The runtime image includes Python, Certbot and OpenSSL for the built-in HTTP-01 The runtime image includes Python, OpenSSL and a dedicated Certbot environment with
certificate helper. Provider-specific Certbot DNS plugins are not installed in the all DNS plugins currently supported by RustProxy: Cloudflare, OVH and RFC2136.
baseline image; extend the image with the required plugin package before using a DNS HTTP-01 needs no DNS plugin. The Certbot core and all three DNS plugins are pinned to
provider. the same release so they stay in one compatible Python environment; no image extension
is required for the built-in certificate providers.
For access-log files, prefer a writable path under `/var/lib/rustproxy` or mount a For access-log files, prefer a writable path under `/var/lib/rustproxy` or mount a
separate writable log volume. The built-in JSON file sink is disabled by default. separate writable log volume. The built-in JSON file sink is disabled by default.
+1 -1
View File
@@ -1,6 +1,6 @@
# Stored log explorer # Stored log explorer
RustProxy 0.26.6 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent. RustProxy 0.26.7 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent.
## Separation from Live Logs ## Separation from Live Logs
+1 -1
View File
@@ -1,4 +1,4 @@
# Installing and updating RustProxy 0.26.6 # Installing and updating RustProxy 0.26.7
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
+2 -1
View File
@@ -15,6 +15,7 @@ This is the canonical index for the current project documentation. The authentic
- [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards. - [0.25.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.
- [0.26.7 implementation note](V0.26.7_IMPLEMENTATION.md) - HOT/COLD occupancy meters, backend rotation continuity and container Certbot packaging.
- [0.26.6 implementation note](V0.26.6_IMPLEMENTATION.md) - composed ACL allow semantics, certificate-form reset, backend table preferences and NPM migration helper documentation. - [0.26.6 implementation note](V0.26.6_IMPLEMENTATION.md) - composed ACL allow semantics, certificate-form reset, backend table preferences and NPM migration helper documentation.
- [0.26.5 implementation note](V0.26.5_IMPLEMENTATION.md) - interface-scaled form/select alignment and corrected Streams listener navigation. - [0.26.5 implementation note](V0.26.5_IMPLEMENTATION.md) - interface-scaled form/select alignment and corrected Streams listener navigation.
- [0.26.4 implementation note](V0.26.4_IMPLEMENTATION.md) - 404 drafts, consistent modal controls, compact Overview settings and idle age-based backend rotation. - [0.26.4 implementation note](V0.26.4_IMPLEMENTATION.md) - 404 drafts, consistent modal controls, compact Overview settings and idle age-based backend rotation.
@@ -66,7 +67,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. The current release implementation note is [V0.26.6_IMPLEMENTATION.md](V0.26.6_IMPLEMENTATION.md); [V0.26.5_IMPLEMENTATION.md](V0.26.5_IMPLEMENTATION.md) is retained as the prior interface-scaling/listener-navigation milestone, [V0.26.4_IMPLEMENTATION.md](V0.26.4_IMPLEMENTATION.md) as the prior administration/backend-lifecycle milestone, [V0.26.3_IMPLEMENTATION.md](V0.26.3_IMPLEMENTATION.md) as the prior appearance/UI milestone, [V0.26.2_IMPLEMENTATION.md](V0.26.2_IMPLEMENTATION.md) as the prior Logs/UI milestone and [V0.26.1_IMPLEMENTATION.md](V0.26.1_IMPLEMENTATION.md) as the prior protocol-attribution milestone. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain historical evidence. Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.26.7_IMPLEMENTATION.md](V0.26.7_IMPLEMENTATION.md); [V0.26.6_IMPLEMENTATION.md](V0.26.6_IMPLEMENTATION.md) is retained as the prior access-policy/administration milestone; [V0.26.5_IMPLEMENTATION.md](V0.26.5_IMPLEMENTATION.md) is retained as the prior interface-scaling/listener-navigation milestone, [V0.26.4_IMPLEMENTATION.md](V0.26.4_IMPLEMENTATION.md) as the prior administration/backend-lifecycle milestone, [V0.26.3_IMPLEMENTATION.md](V0.26.3_IMPLEMENTATION.md) as the prior appearance/UI milestone, [V0.26.2_IMPLEMENTATION.md](V0.26.2_IMPLEMENTATION.md) as the prior Logs/UI milestone and [V0.26.1_IMPLEMENTATION.md](V0.26.1_IMPLEMENTATION.md) as the prior protocol-attribution milestone. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain historical evidence.
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.
+2 -2
View File
@@ -121,8 +121,8 @@ update and purge paths. Purge is authenticated, CSRF-protected and audited, not
public PURGE/BAN capability. public PURGE/BAN capability.
Compression is opt-in, with authenticated/private/no-transform exclusions. Response Compression is opt-in, with authenticated/private/no-transform exclusions. Response
rules cannot overwrite protected framing and identity fields. Required Age remains rules cannot overwrite protected framing/identity fields or the cache-controlled `Age`
on cache hits even when debug headers are disabled. Server hiding is cosmetic and header. Required Age remains on cache hits even when debug headers are disabled. Server hiding is cosmetic and
must not be considered an access-control or vulnerability mitigation substitute. must not be considered an access-control or vulnerability mitigation substitute.
Standard public 80/443 ports are supported without globally forbidding remote Standard public 80/443 ports are supported without globally forbidding remote
+7 -5
View File
@@ -232,11 +232,13 @@ can be streamed without pre-buffering merely to discover their size.
Accept-Encoding quality negotiation is delegated to tower-http. Text, JSON, Accept-Encoding quality negotiation is delegated to tower-http. Text, JSON,
JavaScript, XML, SVG and wasm are eligible; already encoded, SSE, range, upgrade, JavaScript, XML, SVG and wasm are eligible; already encoded, SSE, range, upgrade,
HEAD/204/304 and privacy-sensitive responses are excluded. Cookies/authorization HEAD/204/304 and privacy-sensitive responses are excluded. Authorization and arbitrary
bypass even when an origin marks a response public. Origin no-transform/privacy cookie-bearing dynamic responses remain excluded. A cookie-bearing request may be
exclusions cannot be defeated by deleting a response header in the editor. Cached compressed only when RustProxy is already serving a shared-cache HIT accepted by cache
identity bodies can be streamed through compression on HIT; origin encodings are policy (including the configured static-cookie exception) or a local static Web Server
passed through without recompression. response. Origin `private`, `no-store`, `no-transform` and `Set-Cookie` exclusions still
win. Cached identity bodies can be streamed through compression on HIT; origin encodings
are passed through without recompression.
Vary: Accept-Encoding is added by compression; content length is removed for the Vary: Accept-Encoding is added by compression; content length is removed for the
transformed body. Strong ETags are weakened, and identity-body content digests and transformed body. Strong ETags are weakened, and identity-body content digests and
+13 -13
View File
@@ -1,25 +1,26 @@
# Verification report - RustProxy 0.26.6 # Verification report - RustProxy 0.26.7
## Scope ## Scope
RustProxy 0.26.6 is a compatibility-preserving access-policy and administration workflow patch on top of 0.26.5. SQLite remains schema 8 and the saved proxy configuration format is unchanged. RustProxy 0.26.7 is a compatibility-preserving cache observability, backend-client lifecycle and documentation patch on top of 0.26.6. SQLite remains schema 8 and the saved proxy configuration format is unchanged.
The release keeps backend diagnostics column/sort state synchronized with account-scoped administrator preferences, clears the certificate-creation form only after an ACME issue job reaches `ready`, documents all Python modules required by the NPM migrator, and composes explicit IP/country exceptions inside one ACL policy scope while retaining deny precedence and cumulative profile/vhost/location policies. The ACL profile UI now exposes guided block-by-default and allow-by-default modes with a live OR-policy preview, plus custom mode for mixed rules. Plain host IPs are accepted in addition to CIDRs. Linux Process diagnostics also classify the runtime environment and, only for detected LXC, replace host-oriented uptime/load collection with PID 1 age and container-visible `/proc/loadavg`. The release adds explicit HOT/COLD cache occupancy meters on `/cache` for configured object and byte limits. It also preserves compatible upstream client instances across unrelated active-snapshot rebuilds, preventing generation age, generation request counts and cumulative `Rotations` from being reset by configuration publication. Backend changes that alter origin, address policy, keepalive settings or connect timeout still create fresh clients. The OCI/container documentation now matches the shipped certificate environment: Certbot plus the Cloudflare, OVH and RFC2136 DNS plugins are provided together. The same 0.26.7 source now also hardens cache `Age` ownership and aligns downstream compression with the static-cookie cache exception.
## Source-level regression coverage ## Regression coverage
Rust-independent contracts require the shared preference application path to reapply backend table state, the ACME UI to defer form reset until the matching issue job succeeds, the NPM migration documentation to ship and mount all three helper modules, frontend/location routing to use the composed IP/country ACL evaluator, and the ACL profile editor to retain its guided modes, preview and IP normalization path. Rust unit coverage in `src/acl/tests.rs` exercises allow-country OR allow-IP, block-country OR deny-IP, explicit deny precedence and plain IPv4/IPv6 host rules, but those Rust tests require a compiler-enabled release host. Static source contracts require all four `/cache` occupancy meters (HOT objects, HOT bytes, COLD objects and COLD bytes) and their configured-limit inputs. Browser JavaScript syntax is checked with Node.
Source contracts additionally require the Linux environment classifier, LXC-only uptime/load override, PID 1 start-time parser and Process UI `Linux / <environment>` composition. Rust unit coverage includes LXC-vs-VM precedence, VM/host classification, LXCFS proc detection, `/proc/loadavg` parsing and PID 1 stat parsing. The existing upstream-client unit coverage retains age-triggered rotation without user traffic. A new pool regression, `compatible_pool_rebuild_reuses_upstream_clients`, verifies that a compatible rebuild reuses the previous compiled backend/client object so rotation state remains continuous. Source contracts also require the `reusable_with` compatibility check, maintenance calls for normal/WebSocket clients and backend reuse path. These Rust tests require a compiler-enabled release host.
Existing source, security, memory, Web Server, documentation, packaging, upgrade, SQLite migration and administration JavaScript contracts remain enabled. Cache regressions now require a stored HIT/dashboard object to advance `Age`, while compression regressions require `Age` to survive encoding and require cookie-bearing compression to stay blocked unless the internal cache/static safety marker is present. Source contracts protect the `Age` header from custom response rules and require the bounded HIT/static cookie-compression exception. The bundled compression tester no longer forces `Cache-Control: no-cache` unless `--revalidate` is requested.
A compiler follow-up also removes a redundant route-level `country_resolved = true` write reported as `unused_assignments`; the write occurred after the flag's final read and therefore does not change GeoIP or ACL behavior. Documentation checks cover the 0.26.7 release note, Reader index, local Markdown links, cache diagnostics and container/ACME provider guidance. Packaging smoke tests verify the 0.26.7 release identity and deterministic source archive contract.
## Verification status in this workspace ## Verification status in this workspace
| Check | Status | | Check | Status |
|---|---| |---|---|
| `node --check` for changed administration JavaScript | PASS |
| `python3 scripts/langs_check.py` | PASS | | `python3 scripts/langs_check.py` | PASS |
| `python3 scripts/memory_contracts.py` | PASS | | `python3 scripts/memory_contracts.py` | PASS |
| `python3 scripts/web_server_contracts.py` | PASS | | `python3 scripts/web_server_contracts.py` | PASS |
@@ -28,17 +29,16 @@ A compiler follow-up also removes a redundant route-level `country_resolved = tr
| `python3 scripts/source_module_contracts.py` | PASS | | `python3 scripts/source_module_contracts.py` | PASS |
| `python3 scripts/rust_module_contracts.py` | PASS | | `python3 scripts/rust_module_contracts.py` | PASS |
| `python3 scripts/docs_check.py` | PASS | | `python3 scripts/docs_check.py` | PASS |
| `python3 scripts/check_source.py` | PASS; explicitly reports Rust parsing/types/runtime as not verified | | `python3 scripts/release_smoke.py` | PASS |
| `python3 scripts/release_smoke.py` | PASS, including SQLite migrations, stopped-source upgrade and deterministic source packaging contracts |
| `node scripts/web_service_ui_test.mjs` | PASS | | `node scripts/web_service_ui_test.mjs` | PASS |
| `node --check` for changed administration JavaScript | PASS |
| language/OpenAPI JSON parse | PASS | | language/OpenAPI JSON parse | PASS |
| `go test ./...` in `docker-agent` | PASS | | `go test ./...` in `docker-agent` | PASS |
| `./dev.sh test` | Rust-independent gates through security contracts PASS; the wrapper hit the workspace command-time limit before its final non-Rust steps, which were then run individually and PASS. Cargo remains unavailable for the Rust stage. | | `python3 scripts/check_source.py` | PASS; explicitly reports Rust parsing/types/runtime as not verified |
| `./dev.sh test` | Rust-independent gates PASS; wrapper exits 127 at the Rust stage because Cargo is unavailable |
| `./dev.sh check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace | | `./dev.sh check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
| `cargo fmt --check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace | | `cargo fmt --check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
| `cargo clippy --all-targets --all-features -- -D warnings` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace | | `cargo clippy --all-targets --all-features -- -D warnings` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
| `cargo test --all-targets --all-features` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace | | `cargo test --all-targets --all-features` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
| complete release build | BLOCKED at Rust compilation: Rust/Cargo toolchain unavailable in this workspace | | complete release build | BLOCKED at Rust compilation: Rust/Cargo toolchain unavailable in this workspace |
The 0.26.6 source archive must not be described as compiler-verified until the Rust-enabled release-host gates pass. Rust-independent checks do not substitute for Rust parsing, type checking, Clippy or runtime tests. The 0.26.7 source archive must not be described as compiler-verified until the Rust-enabled release-host gates pass. Rust-independent checks do not substitute for Rust parsing, type checking, Clippy or runtime tests.
+50
View File
@@ -0,0 +1,50 @@
# RustProxy 0.26.7 implementation note
## Scope
0.26.7 is a compatibility-preserving cache observability, backend lifecycle and container documentation patch on top of 0.26.6. SQLite remains schema 8 and the saved proxy configuration document is unchanged.
## HOT/COLD cache occupancy
The authenticated `/cache` page now renders explicit occupancy meters for both cache tiers. HOT and COLD object cards compare the current entry count with `max_entries` / `cold_max_entries`; HOT memory compares charged HOT bytes with `max_bytes`; COLD disk compares persisted bytes with `cold_max_bytes`. The cards also show the configured limit and percentage so the progress bars remain interpretable without estimating from the visual fill alone.
The HOT byte meter intentionally uses allocated/charged HOT bytes, not only logical body length. This matches the budget enforced by cache admission. COLD uses persisted storage bytes.
## Backend rotation continuity
Age-based recycling continues to run from the backend maintenance loop and request-count recycling remains request-driven. The isolated client rotation logic already advanced generation and rotation counters correctly.
The release fixes a separate observability/lifecycle problem: rebuilding an otherwise compatible active routing snapshot created a new upstream client object, which restarted generation age, request count and rotation totals at zero. Pool rebuilds now retain the previous compiled backend when both ordinary and WebSocket upstream clients have exactly the same client factory: backend definition, address policy, keepalive settings, protocol mode and connect timeout. Health observations retain their existing compatibility rules independently.
A real backend-policy change still creates a fresh client. This avoids retaining stale DNS/listener safety policy, TLS identity, keepalive or timeout behavior while keeping long-running rotation diagnostics continuous through unrelated snapshot publications.
## Container certificate tooling and documentation
The OCI runtime image now creates a dedicated `/opt/rustproxy-certbot` Python environment and pins Certbot plus the supported Cloudflare, OVH and RFC2136 DNS plugins to the same release. The image therefore supports every certificate provider exposed by RustProxy without an extra image-extension step. Container documentation was refreshed to remove the obsolete statement that DNS plugins are absent and to use the current release tag in examples.
Current cache, vhost/backend and installation documentation was reviewed alongside the release notes so the dashboard limits, rotation semantics and container prerequisites describe the implemented behavior.
## Compatibility
- SQLite schema: 8, unchanged.
- Saved configuration format: unchanged.
- No migration is required from 0.26.6.
- Backend rotation counters intentionally restart when the backend client policy itself changes.
## Cache Age and downstream compression follow-up
The cache freshness clock remains RFC-oriented: ordinary stored HITs advance `Age` from
`initial_age + resident elapsed time`, while a successful validator revalidation starts a
new freshness calculation. `X-Cache-Resident-Age` remains the monotonic residency clock
across revalidation and tier movement. `Age` is now protected from generic response-header
rules, and regression coverage explicitly checks that both the wire HIT header and `/cache`
object age advance from the stored clock.
Compression still refuses Authorization, private/no-store/no-transform, Set-Cookie, ranges,
SSE, upgrades and already encoded responses. Cookie-bearing requests now have one bounded
exception: a response can be compressed when it is already a shared-cache HIT accepted by
cache policy or a local static Web Server response. This aligns the static-cookie cache
exception with downstream compression without enabling compression for arbitrary dynamic
cookie responses. The bundled compression tester no longer sends `Cache-Control: no-cache`
by default because that header forces validator revalidation and can make cache `Age` appear
to stay at zero; `--revalidate` retains the old diagnostic behavior explicitly.
+4
View File
@@ -206,6 +206,10 @@ it does not reload the vhost, purge cache or intentionally abort active exchange
request counter is per backend client generation, not per TCP connection. Setting either request counter is per backend client generation, not per TCP connection. Setting either
recycle value to `0` disables that trigger independently. `http1_max_buffer_bytes` bounds recycle value to `0` disables that trigger independently. `http1_max_buffer_bytes` bounds
Hyper's adaptive upstream HTTP/1 read buffer per connection; it does not apply to HTTP/2. Hyper's adaptive upstream HTTP/1 read buffer per connection; it does not apply to HTTP/2.
Compatible backend clients are retained across unrelated runtime snapshot rebuilds, so
generation age, request count and rotation totals remain continuous. A changed backend
origin, address policy, keepalive settings or connect timeout creates a fresh client
generation state instead of reusing an incompatible pool.
Disabling reuse sets the idle pool limit to zero and disables keepalive probes. The idle Disabling reuse sets the idle pool limit to zero and disables keepalive probes. The idle
limit is not a separate per-backend active-request cap; global connection/exchange bounds limit is not a separate per-backend active-request cap; global connection/exchange bounds
still apply. HTTP health probes use separate no-reuse HTTP/1.1 connections. still apply. HTTP health probes use separate no-reuse HTTP/1.1 connections.
+1 -1
View File
@@ -19,7 +19,7 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw
are intentionally absent from Prometheus labels. are intentionally absent from Prometheus labels.
Import the JSON in Grafana and select a Prometheus datasource when prompted. The Import the JSON in Grafana and select a Prometheus datasource when prompted. The
dashboard expects RustProxy 0.26.6 metric names and a scrape interval around 15s. dashboard expects RustProxy 0.26.7 metric names and a scrape interval around 15s.
Collectors can be disabled in RustProxy settings; panels for disabled collectors Collectors can be disabled in RustProxy settings; panels for disabled collectors
will simply show no series. will simply show no series.
+2 -2
View File
@@ -4529,9 +4529,9 @@
}, },
"timepicker": {}, "timepicker": {},
"timezone": "browser", "timezone": "browser",
"title": "RustProxy 0.26.6 - Operations", "title": "RustProxy 0.26.7 - Operations",
"uid": "rustproxy-0170", "uid": "rustproxy-0170",
"version": 5, "version": 5,
"weekStart": "", "weekStart": "",
"description": "RustProxy 0.26.6 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic." "description": "RustProxy 0.26.7 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
} }
+1 -1
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.26.6.zip') parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.7.zip')
output = parser.parse_args().output.resolve() output = parser.parse_args().output.resolve()
paths = files() paths = files()
checksum = ROOT / 'SHA256SUMS' checksum = ROOT / 'SHA256SUMS'
+48 -10
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.26.6" assert manifest["package"]["version"] == "0.26.7"
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.26.6"' in lock assert 'name = "rust-proxy"\nversion = "0.26.7"' 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.26.6", "README.md": "# RustProxy 0.26.7",
"CHANGELOG.md": "## 0.26.6", "CHANGELOG.md": "## 0.26.7",
"ROADMAP.md": "## 0.26.6", "ROADMAP.md": "## 0.26.7",
"AGENTS.md": "Current source version: 0.26.6.", "AGENTS.md": "Current source version: 0.26.7.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.6", "docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.7",
}.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"
@@ -136,6 +136,15 @@ assert "cargo generate-lockfile" in dockerfile and "cargo build --locked --relea
assert "USER rustproxy:rustproxy" in dockerfile assert "USER rustproxy:rustproxy" in dockerfile
assert 'VOLUME ["/var/lib/rustproxy"]' in dockerfile assert 'VOLUME ["/var/lib/rustproxy"]' in dockerfile
assert "COPY . ." in dockerfile assert "COPY . ." in dockerfile
for marker in [
"python3 -m venv /opt/rustproxy-certbot",
"certbot==5.8.0",
"certbot-dns-cloudflare==5.8.0",
"certbot-dns-ovh==5.8.0",
"certbot-dns-rfc2136==5.8.0",
"PATH=/opt/rustproxy-certbot/bin:",
]:
assert marker in dockerfile, marker
assert "data" in (root / ".dockerignore").read_text().splitlines() assert "data" in (root / ".dockerignore").read_text().splitlines()
css = (root / "src/admin/web/style.css").read_text() css = (root / "src/admin/web/style.css").read_text()
assert ".logging-grid fieldset" not in css, "logging sinks must reuse shared card styling" assert ".logging-grid fieldset" not in css, "logging sinks must reuse shared card styling"
@@ -170,6 +179,9 @@ assert 'member.backend.websocket_client.maintain_rotation();' in balance_source
upstream_client_source = rust_source(root, "src/upstream/client.rs") upstream_client_source = rust_source(root, "src/upstream/client.rs")
assert 'fn maintain_rotation(&self)' in upstream_client_source assert 'fn maintain_rotation(&self)' in upstream_client_source
assert 'self.rotate(self.generation.load(Ordering::Acquire), "age", false);' in upstream_client_source assert 'self.rotate(self.generation.load(Ordering::Acquire), "age", false);' in upstream_client_source
assert 'fn reusable_with(&self, other: &Self) -> bool' in upstream_client_source
assert 'member.backend = old.backend.clone();' in balance_source
assert 'compatible_pool_rebuild_reuses_upstream_clients' in balance_source
for marker in [ for marker in [
'pub struct PassiveHealth', 'enabled: false', 'pub passive_health: PassiveHealth', 'pub struct PassiveHealth', 'enabled: false', 'pub passive_health: PassiveHealth',
'passive_health_failures_total', 'http_5xx_total', 'timeouts_total', 'passive_health_failures_total', 'http_5xx_total', 'timeouts_total',
@@ -256,7 +268,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.26.6"', '"/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.26.7"', '"/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
@@ -390,6 +402,10 @@ assert 'transform_enabled' in docker_js
cache_panel_js = (root / 'src/admin/web/cache-panel.js').read_text() cache_panel_js = (root / 'src/admin/web/cache-panel.js').read_text()
for marker in ['/api/cache/cold/cleanup', 'removed_entries', 'freed_bytes', '/api/cache/invalidate', '/api/cache/bans', 'cache-invalidation-operation', 'cache-invalidation-selector', 'cache-ban-ttl', 'confirm_wide', 'cachePurgeUrl']: for marker in ['/api/cache/cold/cleanup', 'removed_entries', 'freed_bytes', '/api/cache/invalidate', '/api/cache/bans', 'cache-invalidation-operation', 'cache-invalidation-selector', 'cache-ban-ttl', 'confirm_wide', 'cachePurgeUrl']:
assert marker in cache_panel_js or marker in html, marker assert marker in cache_panel_js or marker in html, marker
for marker in ['cp-hot-entries-meter', 'cp-memory', 'cp-cold-entries-meter', 'cp-cold-disk']:
assert marker in html and marker in cache_panel_js, marker
for marker in ['data.settings?.max_entries', 'data.settings?.cold_max_entries', 'data.settings?.cold_max_bytes', 'setMeter(']:
assert marker in cache_panel_js, marker
transform_panel_js = (root / 'src/admin/web/transform-panel.js').read_text() transform_panel_js = (root / 'src/admin/web/transform-panel.js').read_text()
for marker in ['/api/transform', 'tp-cache-hits', 'transform-reason-list']: for marker in ['/api/transform', 'tp-cache-hits', 'transform-reason-list']:
assert marker in transform_panel_js, marker assert marker in transform_panel_js, marker
@@ -769,6 +785,28 @@ docker_ui = (root / "src/admin/web/docker.js").read_text()
for marker in ["pub enum AppliedEncoding", "AppliedEncoding::Gzip", "AppliedEncoding::Brotli", "AppliedEncoding::Zstd", "AppliedEncoding::Deflate"]: for marker in ["pub enum AppliedEncoding", "AppliedEncoding::Gzip", "AppliedEncoding::Brotli", "AppliedEncoding::Zstd", "AppliedEncoding::Deflate"]:
assert marker in compression_source, marker assert marker in compression_source, marker
assert "quality_weights_can_select_each_enabled_codec" in compression_source assert "quality_weights_can_select_each_enabled_codec" in compression_source
for marker in [
"pub struct AllowCookie",
'response.extensions().get::<AllowCookie>().is_none()',
"cache_safe_cookie_marker_allows_compression_without_weakening_default_guard",
"age_header_survives_downstream_compression",
]:
assert marker in compression_source, marker
compression_finalize_source = rust_source(root, "src/proxy/upstream_helpers.rs")
for marker in [
'exchange.cache_status.load(Ordering::Relaxed) == 1',
'response.extensions().get::<StaticSourceResponse>().is_some()',
'.insert(crate::compression::AllowCookie)',
]:
assert marker in compression_finalize_source, marker
headers_source_0267 = rust_source(root, "src/headers.rs")
vhost_core_0267 = (root / "src/admin/web/vhosts/core.js").read_text()
assert '| "age"' in headers_source_0267, "Age must be protected from generic response rules"
assert '["host", "age", "connection"' in vhost_core_0267, "UI must protect Age consistently"
compression_tester_0267 = (root / "scripts/test_compression.py").read_text()
for marker in ["--revalidate", "if revalidate:", 'headers["Cache-Control"] = "no-cache"', "headers.get('Age', '-')"]:
assert marker in compression_tester_0267, marker
assert "cache_hit_and_dashboard_age_advance_from_storage_clock" in read_cache_source()
diagnostics_source = rust_source(root, "src/diagnostics.rs") + rust_source(root, "src/system_stats/mod.rs") + rust_source(root, "src/system_stats/linux.rs") diagnostics_source = rust_source(root, "src/diagnostics.rs") + rust_source(root, "src/system_stats/mod.rs") + rust_source(root, "src/system_stats/linux.rs")
for marker in ["pub struct NetworkInterfaceView", "pub network_interfaces: Vec<NetworkInterfaceView>", "sysinfo::Networks::new_with_refreshed_list", "system.network_stats", "collect_network_systemstat", "normalize_network_interfaces", "merge_network_interfaces"]: for marker in ["pub struct NetworkInterfaceView", "pub network_interfaces: Vec<NetworkInterfaceView>", "sysinfo::Networks::new_with_refreshed_list", "system.network_stats", "collect_network_systemstat", "normalize_network_interfaces", "merge_network_interfaces"]:
assert marker in diagnostics_source, marker assert marker in diagnostics_source, marker
@@ -1238,7 +1276,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.26.6"' in openapi assert '"RuntimeInformation"' in openapi and '"version": "0.26.7"' 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")
@@ -1283,5 +1321,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.26.6 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("PASS: 0.26.7 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.26.6'}) request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.7'})
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.')
+7 -6
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Rust-independent documentation/Reader consistency checks for the 0.26.6 release.""" """Rust-independent documentation/Reader consistency checks for the 0.26.7 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.26.7_IMPLEMENTATION.md",
DOCS / "V0.25.5_AUDIT.md", DOCS / "V0.25.5_AUDIT.md",
DOCS / "V0.25.4_AUDIT.md", DOCS / "V0.25.4_AUDIT.md",
DOCS / "V0.25.3_AUDIT.md", DOCS / "V0.25.3_AUDIT.md",
@@ -48,23 +49,23 @@ assert "READER_DOCUMENTS" in documentation_rs and "reader_bundle" in documentati
assert 'include_str!(concat!(env!("CARGO_MANIFEST_DIR")' in documentation_rs assert 'include_str!(concat!(env!("CARGO_MANIFEST_DIR")' in documentation_rs
assert 'docs-content.json' not in documentation_rs and 'documentation_bundle' not in build assert 'docs-content.json' not in documentation_rs and 'documentation_bundle' not in build
for document in sorted(DOCS.glob("*.md")): for document in sorted(DOCS.glob("*.md")):
marker = f'/{document.relative_to(ROOT).as_posix()}"' marker = f'"{document.relative_to(ROOT).as_posix()}"'
assert marker in documentation_rs, f"Reader does not embed {document.relative_to(ROOT)}" assert marker in documentation_rs, f"Reader does not embed {document.relative_to(ROOT)}"
assert '"/docs/content.json"' in admin 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.26.6") assert readme.startswith("# RustProxy 0.26.7")
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.26.6" in roadmap and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap assert roadmap.startswith("# RustProxy roadmap") and "## 0.26.7" in roadmap and "## 0.26.6" in roadmap and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
assert changelog.startswith("# Changelog\n") assert changelog.startswith("# Changelog\n")
for release in ["0.26.6", "0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]: for release in ["0.26.7", "0.26.6", "0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
assert f"## {release}" in changelog, release assert f"## {release}" in changelog, release
assert changelog.index("## 0.26.6") < changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5") assert changelog.index("## 0.26.7") < changelog.index("## 0.26.6") < changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog 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}"
+2 -2
View File
@@ -75,10 +75,10 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}") raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "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/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "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.26.6 source tree: missing {path}") raise ValueError(f"Incomplete RustProxy 0.26.7 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.26.6 entry point; extract the complete package, not individual files.") raise ValueError("src/lib.rs is not the 0.26.7 entry point; extract the complete package, not individual files.")
return messages return messages
+17 -5
View File
@@ -123,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.26.6"' in (target / "Cargo.toml").read_text(encoding="utf-8") assert 'version = "0.26.7"' 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()
@@ -134,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.26.6" assert manifest["to_version"] == "0.26.7"
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()
@@ -295,6 +295,10 @@ def ui_contract_smoke() -> None:
'id="cache-cold-path"', 'id="cache-cold-path"',
'id="cache-cold-bytes"', 'id="cache-cold-bytes"',
'id="cache-cold-entries"', 'id="cache-cold-entries"',
'id="cp-hot-entries-meter"',
'id="cp-memory"',
'id="cp-cold-entries-meter"',
'id="cp-cold-disk"',
'id="cache-cold-after"', 'id="cache-cold-after"',
'id="log-format-preset"', 'id="log-format-preset"',
'id="log-format-open"', 'id="log-format-open"',
@@ -346,6 +350,8 @@ def ui_contract_smoke() -> None:
assert 'data-panel="configuration"' not in html and 'id="panel-configuration"' not in html assert 'data-panel="configuration"' not in html and 'id="panel-configuration"' not in html
assert html.index('data-panel="vhosts"') < html.index('data-panel="redirects"') < html.index('data-panel="notfound"') < html.index('data-panel="streams"') < html.index('data-panel="acl-profiles"') assert html.index('data-panel="vhosts"') < html.index('data-panel="redirects"') < html.index('data-panel="notfound"') < html.index('data-panel="streams"') < html.index('data-panel="acl-profiles"')
assert "matching_resident_bytes" in cache and "matching_body_bytes" in cache assert "matching_resident_bytes" in cache and "matching_body_bytes" in cache
for marker in ["data.settings?.max_entries", "data.settings?.cold_max_entries", "data.settings?.cold_max_bytes", "setMeter("]:
assert marker in cache, marker
for marker in ["cache-memory-breakdown", "cache-transient-breakdown", "cache-cold-storage", "cache-transient-sparkline", "cache-fill-sparkline", "cache-queue-sparkline", "cache-io-sparkline", "cache-cold-sparkline", "metric-info", "process-rss-total", "memory-cache-hot", "memory-cache-transient", "memory-transform", "memory-transform-inflight", "memory-non-cache-rss", "process-rss-anon", "process-rss-file", "process-data-swap", "memoryAccountingHint"]: for marker in ["cache-memory-breakdown", "cache-transient-breakdown", "cache-cold-storage", "cache-transient-sparkline", "cache-fill-sparkline", "cache-queue-sparkline", "cache-io-sparkline", "cache-cold-sparkline", "metric-info", "process-rss-total", "memory-cache-hot", "memory-cache-transient", "memory-transform", "memory-transform-inflight", "memory-non-cache-rss", "process-rss-anon", "process-rss-file", "process-data-swap", "memoryAccountingHint"]:
assert marker in html, marker assert marker in html, marker
for marker in ["transient_bytes", "fill_bytes_active", "cold_queue_bytes", "cold_write_bytes_active", "cold_read_bytes_active", "process_rss_anon_bytes", "process_rss_file_bytes"]: for marker in ["transient_bytes", "fill_bytes_active", "cold_queue_bytes", "cold_write_bytes_active", "cold_read_bytes_active", "process_rss_anon_bytes", "process_rss_file_bytes"]:
@@ -565,7 +571,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.26.6"' in openapi_0241 assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.26.7"' 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
@@ -1022,7 +1028,7 @@ def source_contract_smoke() -> None:
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8") openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
assert '"/api/config/history/restore"' in openapi assert '"/api/config/history/restore"' in openapi
for marker in ['"version": "0.26.6"', '"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.26.7"', '"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"']:
@@ -1095,6 +1101,12 @@ def container_contract_smoke() -> None:
"cargo generate-lockfile", "cargo generate-lockfile",
"cargo build --locked --release", "cargo build --locked --release",
"FROM debian:trixie-slim AS runtime", "FROM debian:trixie-slim AS runtime",
"python3 -m venv /opt/rustproxy-certbot",
"certbot==5.8.0",
"certbot-dns-cloudflare==5.8.0",
"certbot-dns-ovh==5.8.0",
"certbot-dns-rfc2136==5.8.0",
"PATH=/opt/rustproxy-certbot/bin:",
"USER rustproxy:rustproxy", "USER rustproxy:rustproxy",
'VOLUME ["/var/lib/rustproxy"]', 'VOLUME ["/var/lib/rustproxy"]',
]: ]:
@@ -1197,7 +1209,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.26.6 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.26.7 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
@@ -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.26.6"}) request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.7"})
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:
+13 -2
View File
@@ -63,12 +63,13 @@ def human_size(size):
return f"{size / 1024 / 1024:.2f} MB" return f"{size / 1024 / 1024:.2f} MB"
def request_test(url, accept_encoding, verify_ssl=True, timeout=15): def request_test(url, accept_encoding, verify_ssl=True, timeout=15, revalidate=False):
headers = { headers = {
"Accept-Encoding": accept_encoding, "Accept-Encoding": accept_encoding,
"User-Agent": "compression-tester/1.0", "User-Agent": "compression-tester/1.0",
"Cache-Control": "no-cache",
} }
if revalidate:
headers["Cache-Control"] = "no-cache"
start = time.perf_counter() start = time.perf_counter()
@@ -126,6 +127,11 @@ def main():
default=15, default=15,
help="Timeout HTTP w sekundach", help="Timeout HTTP w sekundach",
) )
parser.add_argument(
"--revalidate",
action="store_true",
help="Wyslij Cache-Control: no-cache (wymusza rewalidacje i moze resetowac Age do 0)",
)
args = parser.parse_args() args = parser.parse_args()
@@ -145,6 +151,7 @@ def main():
accept_encoding, accept_encoding,
verify_ssl=not args.insecure, verify_ssl=not args.insecure,
timeout=args.timeout, timeout=args.timeout,
revalidate=args.revalidate,
) )
results[name] = result results[name] = result
except Exception as exc: except Exception as exc:
@@ -227,6 +234,10 @@ def main():
print(f" Content-Length: {headers.get('Content-Length', '-')}") print(f" Content-Length: {headers.get('Content-Length', '-')}")
print(f" Vary: {headers.get('Vary', '-')}") print(f" Vary: {headers.get('Vary', '-')}")
print(f" Cache-Control: {headers.get('Cache-Control', '-')}") print(f" Cache-Control: {headers.get('Cache-Control', '-')}")
print(f" Age: {headers.get('Age', '-')}")
print(f" X-Cache: {headers.get('X-Cache', '-')}")
print(f" X-Cache-Hits: {headers.get('X-Cache-Hits', '-')}")
print(f" Cache-Status: {headers.get('Cache-Status', '-')}")
print(f" ETag: {headers.get('ETag', '-')}") print(f" ETag: {headers.get('ETag', '-')}")
print(f" Server: {headers.get('Server', '-')}") print(f" Server: {headers.get('Server', '-')}")
print() print()
+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.26.6-" + uuid.uuid4().hex[:12]) backup = target / ".upgrade-backup" / ("before-0.26.7-" + 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.26.6", "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.26.7", "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.26.6. Previous managed source files: {backup}") print(f"Installed RustProxy 0.26.7. 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
@@ -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.26.6</b>", "RustProxy <b>0.26.7</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",
+17 -42
View File
@@ -1,5 +1,15 @@
//! Embedded Markdown documents for the authenticated Reader. //! Embedded Markdown documents for the authenticated Reader.
macro_rules! reader_doc {
($path:literal, $category:literal) => {
(
$path,
$category,
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/", $path)),
)
};
}
pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
( (
"README.md", "README.md",
@@ -399,48 +409,13 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
"/docs/V0.25.1_AUDIT.md" "/docs/V0.25.1_AUDIT.md"
)), )),
), ),
( reader_doc!("docs/V0.26.7_IMPLEMENTATION.md", "technical"),
"docs/V0.26.6_IMPLEMENTATION.md", reader_doc!("docs/V0.26.6_IMPLEMENTATION.md", "technical"),
"technical", reader_doc!("docs/V0.26.5_IMPLEMENTATION.md", "technical"),
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.26.6_IMPLEMENTATION.md")), reader_doc!("docs/V0.26.4_IMPLEMENTATION.md", "technical"),
), reader_doc!("docs/V0.26.3_IMPLEMENTATION.md", "technical"),
( reader_doc!("docs/V0.26.2_IMPLEMENTATION.md", "technical"),
"docs/V0.26.5_IMPLEMENTATION.md", reader_doc!("docs/V0.26.1_IMPLEMENTATION.md", "technical"),
"technical",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/V0.26.5_IMPLEMENTATION.md")),
),
(
"docs/V0.26.4_IMPLEMENTATION.md",
"technical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.26.4_IMPLEMENTATION.md"
)),
),
(
"docs/V0.26.3_IMPLEMENTATION.md",
"technical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.26.3_IMPLEMENTATION.md"
)),
),
(
"docs/V0.26.2_IMPLEMENTATION.md",
"technical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.26.2_IMPLEMENTATION.md"
)),
),
(
"docs/V0.26.1_IMPLEMENTATION.md",
"technical",
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/docs/V0.26.1_IMPLEMENTATION.md"
)),
),
( (
"docs/V0.25.5_AUDIT.md", "docs/V0.25.5_AUDIT.md",
"audits", "audits",
+1 -1
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.26.6", "version": "0.26.7",
"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": [
+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.26.6"; $("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.7";
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();
} }
+27 -5
View File
@@ -10,6 +10,19 @@
const scope = () => $("cache-scope").value; const scope = () => $("cache-scope").value;
const domainFilter = () => $("cache-domain-filter").value; const domainFilter = () => $("cache-domain-filter").value;
const number = v => fmt(v ?? 0); const number = v => fmt(v ?? 0);
const occupancy = (used, limit) => {
const boundedLimit = Number(limit) || 0;
if (boundedLimit <= 0) return 0;
return Math.min(100, Math.max(0, (Number(used) || 0) / boundedLimit * 100));
};
const setMeter = (id, used, limit) => {
const value = occupancy(used, limit), meter = $(id);
if (meter) {
meter.value = value;
meter.setAttribute("aria-valuetext", `${value.toFixed(1)}%`);
}
return value;
};
const operationMessage = (id, value = "", error = false) => { const operationMessage = (id, value = "", error = false) => {
const box = $(id); const box = $(id);
if (!box) return; if (!box) return;
@@ -111,18 +124,27 @@
const total = field => groups.reduce((n, row) => n + (row[field] || 0), 0), all = scope() === "*"; const total = field => groups.reduce((n, row) => n + (row[field] || 0), 0), all = scope() === "*";
const hits = total("hits"), misses = total("misses"); const hits = total("hits"), misses = total("misses");
const hotEntries = all ? (m.entries || 0) : total("hot_entries"), coldEntries = all ? (m.cold_entries || 0) : total("cold_entries"); const hotEntries = all ? (m.entries || 0) : total("hot_entries"), coldEntries = all ? (m.cold_entries || 0) : total("cold_entries");
const hotEntryLimit = Number(data.settings?.max_entries) || 0;
const hotEntryPercent = setMeter("cp-hot-entries-meter", hotEntries, hotEntryLimit);
$("cp-entries").textContent = number(hotEntries); $("cp-entries").textContent = number(hotEntries);
$("cp-capacity").textContent = `${t("cacheGlobalLimit")}: ${number(data.settings?.max_entries)}`; $("cp-capacity").textContent = `${t("cacheGlobalLimit")}: ${number(hotEntryLimit)} · ${hotEntryPercent.toFixed(1)}%`;
const resident = all ? (m.hot_allocated_bytes ?? m.resident_bytes ?? 0) : total("resident_bytes"); const resident = all ? (m.hot_allocated_bytes ?? m.resident_bytes ?? 0) : total("resident_bytes");
const logical = all ? (m.hot_logical_bytes ?? 0) : total("logical_bytes"); const logical = all ? (m.hot_logical_bytes ?? 0) : total("logical_bytes");
const hotLimit = m.hot_limit_bytes ?? data.settings?.max_bytes ?? 0; const hotLimit = m.hot_limit_bytes ?? data.settings?.max_bytes ?? 0;
$("cp-bytes").textContent = fmtBytes(resident); $("cp-memory").value = resident / Math.max(1, hotLimit) * 100; const hotPercent = setMeter("cp-memory", resident, hotLimit);
$("cp-memory-detail").textContent = `${t("cacheLogicalShort")}: ${fmtBytes(logical)} · ${t("cacheLimitShort")}: ${fmtBytes(hotLimit)}`; $("cp-bytes").textContent = fmtBytes(resident);
$("cp-memory-detail").textContent = `${t("cacheLogicalShort")}: ${fmtBytes(logical)} · ${t("cacheLimitShort")}: ${fmtBytes(hotLimit)} · ${hotPercent.toFixed(1)}%`;
$("cp-transient").textContent = `${fmtBytes(m.transient_bytes)} / ${fmtBytes(m.transient_limit_bytes ?? data.settings?.transient_max_bytes)}`; $("cp-transient").textContent = `${fmtBytes(m.transient_bytes)} / ${fmtBytes(m.transient_limit_bytes ?? data.settings?.transient_max_bytes)}`;
$("cp-transient-detail").textContent = `${t("cacheFillShort")}: ${fmtBytes(m.fill_bytes_active)} · ${t("cacheQueueShort")}: ${fmtBytes(m.cold_queue_bytes)} / ${fmtBytes(m.cold_queue_limit_bytes ?? data.settings?.cold_queue_max_bytes)} · ${t("cacheIoShort")}: ${fmtBytes((Number(m.cold_write_bytes_active) || 0) + (Number(m.cold_read_bytes_active) || 0))}`; $("cp-transient-detail").textContent = `${t("cacheFillShort")}: ${fmtBytes(m.fill_bytes_active)} · ${t("cacheQueueShort")}: ${fmtBytes(m.cold_queue_bytes)} / ${fmtBytes(m.cold_queue_limit_bytes ?? data.settings?.cold_queue_max_bytes)} · ${t("cacheIoShort")}: ${fmtBytes((Number(m.cold_write_bytes_active) || 0) + (Number(m.cold_read_bytes_active) || 0))}`;
const coldBytes = all ? (m.cold_bytes || 0) : total("cold_bytes"); const coldBytes = all ? (m.cold_bytes || 0) : total("cold_bytes");
$("cp-cold-entries").textContent = number(coldEntries); $("cp-cold-capacity").textContent = `${t("cacheGlobalLimit")}: ${number(data.settings?.cold_max_entries || 0)}`; const coldEntryLimit = Number(data.settings?.cold_max_entries) || 0;
$("cp-cold-bytes").textContent = fmtBytes(coldBytes); $("cp-cold-disk").value = coldBytes / Math.max(1, data.settings?.cold_max_bytes || 1) * 100; const coldEntryPercent = setMeter("cp-cold-entries-meter", coldEntries, coldEntryLimit);
$("cp-cold-entries").textContent = number(coldEntries);
$("cp-cold-capacity").textContent = `${t("cacheGlobalLimit")}: ${number(coldEntryLimit)} · ${coldEntryPercent.toFixed(1)}%`;
const coldLimit = Number(data.settings?.cold_max_bytes) || 0;
const coldPercent = setMeter("cp-cold-disk", coldBytes, coldLimit);
$("cp-cold-bytes").textContent = fmtBytes(coldBytes);
$("cp-cold-disk-detail").textContent = `${t("cacheLimitShort")}: ${fmtBytes(coldLimit)} · ${coldPercent.toFixed(1)}%`;
$("cp-ratio").textContent = `${fmt((all ? (m.hit_ratio || 0) : (hits + misses ? hits / (hits + misses) : 0)) * 100, 1)}%`; $("cp-ratio").textContent = `${fmt((all ? (m.hit_ratio || 0) : (hits + misses ? hits / (hits + misses) : 0)) * 100, 1)}%`;
$("cp-requests").textContent = `${number(all ? m.hits : hits)} HIT / ${number(all ? m.misses : misses)} MISS`; $("cp-requests").textContent = `${number(all ? m.hits : hits)} HIT / ${number(all ? m.misses : misses)} MISS`;
const bypasses = all ? (Number(m.bypasses) || 0) : total("bypasses"); const bypasses = all ? (Number(m.bypasses) || 0) : total("bypasses");
+14 -9
View File
@@ -1320,8 +1320,10 @@
</div> </div>
</details> </details>
<div class="stats-grid cache-stats"> <div class="stats-grid cache-stats">
<article class="stat"><span data-i18n="cacheHotObjects">Hot objects</span><strong <article class="stat"><span data-i18n="cacheHotObjects" id="cache-hot-objects-label">Hot objects</span><strong
id="cp-entries">-</strong><small id="cp-capacity">-</small></article> id="cp-entries">-</strong><small id="cp-capacity">-</small><progress
aria-labelledby="cache-hot-objects-label" id="cp-hot-entries-meter" max="100"
value="0"></progress></article>
<article class="stat"><span data-i18n="cacheHotMemory" id="cache-memory-label">Hot <article class="stat"><span data-i18n="cacheHotMemory" id="cache-memory-label">Hot
memory</span><strong id="cp-bytes">-</strong><small memory</span><strong id="cp-bytes">-</strong><small
id="cp-memory-detail">-</small><progress aria-labelledby="cache-memory-label" id="cp-memory-detail">-</small><progress aria-labelledby="cache-memory-label"
@@ -1329,10 +1331,13 @@
</article> </article>
<article class="stat"><span data-i18n="cacheTransientMemory">Transient cache RAM</span><strong <article class="stat"><span data-i18n="cacheTransientMemory">Transient cache RAM</span><strong
id="cp-transient">-</strong><small id="cp-transient-detail">-</small></article> id="cp-transient">-</strong><small id="cp-transient-detail">-</small></article>
<article class="stat"><span data-i18n="cacheColdObjects">Cold objects</span><strong <article class="stat"><span data-i18n="cacheColdObjects" id="cache-cold-objects-label">Cold objects</span><strong
id="cp-cold-entries">-</strong><small id="cp-cold-capacity">-</small></article> id="cp-cold-entries">-</strong><small id="cp-cold-capacity">-</small><progress
<article class="stat"><span data-i18n="cacheColdDisk">Cold disk</span><strong aria-labelledby="cache-cold-objects-label" id="cp-cold-entries-meter" max="100"
id="cp-cold-bytes">-</strong><progress id="cp-cold-disk" max="100" value="0"></progress> value="0"></progress></article>
<article class="stat"><span data-i18n="cacheColdDisk" id="cache-cold-disk-label">Cold disk</span><strong
id="cp-cold-bytes">-</strong><small id="cp-cold-disk-detail">-</small><progress
aria-labelledby="cache-cold-disk-label" id="cp-cold-disk" max="100" value="0"></progress>
</article> </article>
<article class="stat"><span data-i18n="cacheHitRatio">Hit ratio</span><strong <article class="stat"><span data-i18n="cacheHitRatio">Hit ratio</span><strong
id="cp-ratio">-</strong><small id="cp-requests">-</small></article> id="cp-ratio">-</strong><small id="cp-requests">-</small></article>
@@ -1919,7 +1924,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.26.6" id="server-value" maxlength="256" placeholder="RustProxy/v0.26.7"
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">
@@ -2718,7 +2723,7 @@
</section> </section>
</div> </div>
</section> </section>
<footer class="page-footer"><span>RustProxy <b>0.26.6</b></span><span id="footer-status">API - / <footer class="page-footer"><span>RustProxy <b>0.26.7</b></span><span id="footer-status">API - /
dropped: 0</span></footer> dropped: 0</span></footer>
</div> </div>
</main> </main>
@@ -3429,7 +3434,7 @@ max_execution_time = 30</code></pre>
<article class="card about-card"> <article class="card about-card">
<div class="about-product"> <div class="about-product">
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span> <span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
<div><span class="small-label">RustProxy</span><strong>0.26.6</strong></div> <div><span class="small-label">RustProxy</span><strong>0.26.7</strong></div>
</div> </div>
<dl class="about-meta"> <dl class="about-meta">
<div> <div>
+1 -1
View File
@@ -72,7 +72,7 @@
for (const direction of ["request", "response"]) { for (const direction of ["request", "response"]) {
const rules = value[direction] || []; if (!Array.isArray(rules) || rules.length > 32) fail(); for (const rule of rules) { const rules = value[direction] || []; if (!Array.isArray(rules) || rules.length > 32) fail(); for (const rule of rules) {
if (!rule || typeof rule !== "object" || Object.keys(rule).some(k => !["op", "name", "value"].includes(k)) || !["set", "append", "remove"].includes(rule.op) || typeof rule.name !== "string" || rule.name.length > 128 || !/^[-!#$%&'*+.^_`|~0-9A-Za-z]+$/.test(rule.name)) fail(); if (!rule || typeof rule !== "object" || Object.keys(rule).some(k => !["op", "name", "value"].includes(k)) || !["set", "append", "remove"].includes(rule.op) || typeof rule.name !== "string" || rule.name.length > 128 || !/^[-!#$%&'*+.^_`|~0-9A-Za-z]+$/.test(rule.name)) fail();
const name = rule.name.toLowerCase(); if (["host", "connection", "content-length", "transfer-encoding", "te", "trailer", "content-encoding", "content-range", "upgrade", "keep-alive", "proxy-connection", "proxy-authorization", "proxy-authenticate", "forwarded", "x-real-ip", "x-request-id", "x-rust-proxy-backend", "x-rust-proxy-backend-time", "x-rustproxy-backend-time"].includes(name) || name.startsWith("x-forwarded-") || name.startsWith("sec-websocket-")) throw new Error(t("headersProtected")); const name = rule.name.toLowerCase(); if (["host", "age", "connection", "content-length", "transfer-encoding", "te", "trailer", "content-encoding", "content-range", "upgrade", "keep-alive", "proxy-connection", "proxy-authorization", "proxy-authenticate", "forwarded", "x-real-ip", "x-request-id", "x-rust-proxy-backend", "x-rust-proxy-backend-time", "x-rustproxy-backend-time"].includes(name) || name.startsWith("x-forwarded-") || name.startsWith("sec-websocket-")) throw new Error(t("headersProtected"));
if (rule.op === "remove") { if (Object.hasOwn(rule, "value") && rule.value !== null) fail(); } else if (typeof rule.value !== "string" || rule.value.length > 8192 || /[\r\n\0]/.test(rule.value)) fail(); if (rule.op === "remove") { if (Object.hasOwn(rule, "value") && rule.value !== null) fail(); } else if (typeof rule.value !== "string" || rule.value.length > 8192 || /[\r\n\0]/.test(rule.value)) fail();
} }
} return value; } return value;
+1 -1
View File
@@ -47,7 +47,7 @@ pub enum StickyMode {
Cookie, Cookie,
IpHash, IpHash,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
pub struct Keepalive { pub struct Keepalive {
pub enabled: bool, pub enabled: bool,
+18 -4
View File
@@ -61,10 +61,24 @@ impl Pool {
&previous.settings.passive_health, &previous.settings.passive_health,
)) ))
.ok(); .ok();
if same_origin && same_health && (old.definition.enabled || !member.definition.enabled) let same_clients = member.backend.client.reusable_with(&old.backend.client)
{ && member
// This pool has not been published or cloned into routes yet. .backend
if let Some(member) = Arc::get_mut(member) { .websocket_client
.reusable_with(&old.backend.websocket_client);
// This pool has not been published or cloned into routes yet.
if let Some(member) = Arc::get_mut(member) {
if same_clients {
// Preserve the live Hyper pools and, with them, generation
// age/request/rotation state across unrelated snapshot
// rebuilds. Changed origins, address policy, keepalive or
// connect timeout still receive newly compiled clients.
member.backend = old.backend.clone();
}
if same_origin
&& same_health
&& (old.definition.enabled || !member.definition.enabled)
{
member.state = old.state.clone(); member.state = old.state.clone();
member.token = old.token.clone(); member.token = old.token.clone();
} }
+17
View File
@@ -98,6 +98,23 @@ fn upstream_pool_recycle_limits_are_bounded_and_can_be_disabled() {
assert!(settings.validate().is_ok()); assert!(settings.validate().is_ok());
} }
#[test]
fn compatible_pool_rebuild_reuses_upstream_clients() {
let previous = pool(Algorithm::LeastConnections, StickyMode::Off);
let mut rebuilt = pool(Algorithm::LeastConnections, StickyMode::Off);
let rebuilt_mut = Arc::get_mut(&mut rebuilt).expect("fresh pool must be uniquely owned");
assert!(!Arc::ptr_eq(
&rebuilt_mut.members[0].backend,
&previous.members[0].backend
));
rebuilt_mut.reuse_observations(&previous);
assert!(Arc::ptr_eq(
&rebuilt_mut.members[0].backend,
&previous.members[0].backend
));
}
#[test] #[test]
fn retry_policy_rejects_unbounded_or_non_5xx_configuration() { fn retry_policy_rejects_unbounded_or_non_5xx_configuration() {
let mut settings = Settings::default(); let mut settings = Settings::default();
+24
View File
@@ -349,6 +349,30 @@
assert_eq!(cache.purge(None), 1); assert_eq!(cache.purge(None), 1);
assert!(cache.get(&t).is_none()); assert!(cache.get(&t).is_none());
} }
#[tokio::test]
async fn cache_hit_and_dashboard_age_advance_from_storage_clock() {
let cache = Cache::new(1);
let t = ticket(&cache, &HeaderMap::new());
fill(&cache, t.clone(), "payload", &settings()).await;
{
let mut store = cache.store.lock().unwrap_or_else(|e| e.into_inner());
let entry = store.entries.values_mut().next().expect("stored entry");
entry.received = Instant::now() - Duration::from_secs(5);
}
let hit = cache.get(&t).expect("fresh cached response");
assert!(
hit.response.headers()["age"]
.to_str()
.unwrap()
.parse::<u64>()
.unwrap()
>= 5
);
assert!(cache.dashboard(None, 0, 50).objects[0].age_seconds >= 5);
}
#[test] #[test]
fn defaults_enable_static_cookie_boost_for_common_assets() { fn defaults_enable_static_cookie_boost_for_common_assets() {
let settings = Settings::default(); let settings = Settings::default();
+8 -1
View File
@@ -61,6 +61,12 @@ impl Settings {
#[derive(Clone)] #[derive(Clone)]
pub struct Skip; pub struct Skip;
/// Internal proof that a request carrying Cookie may still be compressed.
/// Only shared-cache HITs accepted by cache policy and local static Web Server
/// responses set this marker; arbitrary authenticated/dynamic responses do not.
#[derive(Clone, Copy)]
pub struct AllowCookie;
/// Encoding applied by RustProxy itself. This marker is kept in response /// Encoding applied by RustProxy itself. This marker is kept in response
/// extensions so accounting never has to infer the selected codec from a /// extensions so accounting never has to infer the selected codec from a
/// header that later response rules may rewrite. /// header that later response rules may rewrite.
@@ -132,7 +138,8 @@ fn eligible(
|| response.headers().contains_key("upgrade") || response.headers().contains_key("upgrade")
|| request.contains_key("range") || request.contains_key("range")
|| request.contains_key("authorization") || request.contains_key("authorization")
|| request.contains_key("cookie") || (request.contains_key("cookie")
&& response.extensions().get::<AllowCookie>().is_none())
|| no_transform(request) || no_transform(request)
|| no_transform(response.headers()) || no_transform(response.headers())
{ {
+36
View File
@@ -122,6 +122,42 @@ mod tests {
} }
} }
#[tokio::test]
async fn cache_safe_cookie_marker_allows_compression_without_weakening_default_guard() {
let settings = Settings {
gzip: true,
..Default::default()
};
let mut headers = HeaderMap::new();
headers.insert("accept-encoding", "gzip".parse().unwrap());
headers.insert("cookie", "session=secret".parse().unwrap());
let blocked = encode(response(), &headers, &Method::GET, &settings).await;
assert!(!blocked.headers().contains_key("content-encoding"));
let mut safe = response();
safe.extensions_mut().insert(AllowCookie);
let encoded = encode(safe, &headers, &Method::GET, &settings).await;
assert_eq!(encoded.headers()["content-encoding"], "gzip");
}
#[tokio::test]
async fn age_header_survives_downstream_compression() {
let settings = Settings {
gzip: true,
..Default::default()
};
let mut headers = HeaderMap::new();
headers.insert("accept-encoding", "gzip".parse().unwrap());
let mut cached = response();
cached.headers_mut().insert("age", "37".parse().unwrap());
let encoded = encode(cached, &headers, &Method::GET, &settings).await;
assert_eq!(encoded.headers()["content-encoding"], "gzip");
assert_eq!(encoded.headers()["age"], "37");
}
#[tokio::test] #[tokio::test]
async fn no_transform_personal_and_sse_are_not_compressed() { async fn no_transform_personal_and_sse_are_not_compressed() {
let s = Settings { let s = Settings {
+1 -1
View File
@@ -128,7 +128,7 @@ pub fn compile_with_defaults(
) -> Result<Snapshot> { ) -> Result<Snapshot> {
compile_reusing(config, revision, policy, defaults, None) compile_reusing(config, revision, policy, defaults, None)
} }
/// Rebuild clients and route policies, but retain compatible health observations, /// Rebuild route policies and retain compatible backend clients, health observations,
/// full-exchange counters and affinity tokens through unrelated config edits. /// full-exchange counters and affinity tokens through unrelated config edits.
pub fn compile_reusing( pub fn compile_reusing(
config: Config, config: Config,
+1 -1
View File
@@ -93,7 +93,7 @@ impl Default for AclProfile {
} }
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
pub struct Backend { pub struct Backend {
#[serde(default)] #[serde(default)]
+1
View File
@@ -52,6 +52,7 @@ fn protected(name: &str) -> bool {
matches!( matches!(
name, name,
"host" "host"
| "age"
| "connection" | "connection"
| "content-length" | "content-length"
| "transfer-encoding" | "transfer-encoding"
+1 -1
View File
@@ -41,7 +41,7 @@ use zeroize::Zeroizing;
#[command( #[command(
name = "rust-proxy", name = "rust-proxy",
version, version,
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.26.6" about = "RustProxy - independent Rust reverse proxy and static Web Server 0.26.7"
)] )]
struct Cli { struct Cli {
#[arg( #[arg(
+1 -1
View File
@@ -239,7 +239,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
} }
}); });
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal(); let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
println!("RustProxy live logs 0.26.6 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits."); println!("RustProxy live logs 0.26.7 - 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();
+16
View File
@@ -278,6 +278,22 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
{ {
response.headers_mut().remove("alt-svc"); response.headers_mut().remove("alt-svc");
} }
// Cookie-bearing requests are normally excluded from compression to avoid
// compressing potentially private dynamic content. The cache can explicitly
// ignore cookies for configured static extensions, though; once that request
// is a shared-cache HIT, cache policy has already proven the stored response
// contains no Set-Cookie/private/no-store metadata. Local Web Server static
// responses are equally independent of request cookies. Allow compression in
// those two bounded cases so cached/static browser assets still negotiate
// Content-Encoding.
if original_headers.contains_key("cookie")
&& (exchange.cache_status.load(Ordering::Relaxed) == 1
|| response.extensions().get::<StaticSourceResponse>().is_some())
{
response
.extensions_mut()
.insert(crate::compression::AllowCookie);
}
response = crate::compression::encode( response = crate::compression::encode(
response, response,
&original_headers, &original_headers,
+3 -1
View File
@@ -35,7 +35,7 @@ pub use client::{
client, client_with_keepalive, client_with_timeout, ClientRotationStats, ProxyClient, client, client_with_keepalive, client_with_timeout, ClientRotationStats, ProxyClient,
}; };
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct AddressPolicy { pub struct AddressPolicy {
pub allow_private: bool, pub allow_private: bool,
pub accept_local_listener_risk: bool, pub accept_local_listener_risk: bool,
@@ -77,6 +77,8 @@ impl AddressPolicy {
let addresses = local_addresses(); let addresses = local_addresses();
self.interfaces_known = addresses.is_some(); self.interfaces_known = addresses.is_some();
self.local_ips = addresses.unwrap_or_default(); self.local_ips = addresses.unwrap_or_default();
self.local_ips.sort_unstable();
self.local_ips.dedup();
self self
} }
pub fn with_bindings(mut self, bindings: &[SocketAddr]) -> Self { pub fn with_bindings(mut self, bindings: &[SocketAddr]) -> Self {
+9 -1
View File
@@ -33,7 +33,7 @@ fn monotonic_millis() -> u64 {
.min(u64::MAX as u128) as u64 .min(u64::MAX as u128) as u64
} }
#[derive(Clone)] #[derive(Clone, PartialEq, Eq)]
struct ClientFactory { struct ClientFactory {
backend: Backend, backend: Backend,
policy: AddressPolicy, policy: AddressPolicy,
@@ -104,6 +104,14 @@ impl ProxyClient {
self.inner.stats() self.inner.stats()
} }
/// Whether a newly compiled backend client has exactly the same connection
/// policy and lifecycle settings as this one. Compatible clients can be
/// retained across unrelated snapshot rebuilds so generation age and
/// rotation counters are not restarted from zero.
pub(crate) fn reusable_with(&self, other: &Self) -> bool {
self.inner.factory == other.inner.factory
}
/// Recycle an aged connection pool even when no user request arrives. /// Recycle an aged connection pool even when no user request arrives.
pub(crate) fn maintain_rotation(&self) { pub(crate) fn maintain_rotation(&self) {
self.inner.maintain_rotation(); self.inner.maintain_rotation();
+1 -1
View File
@@ -159,7 +159,7 @@ pub(super) fn fastcgi_params(
if context.tls { "on" } else { "off" }.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.26.6".into()); params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.26.7".into());
params.insert("REDIRECT_STATUS".into(), "200".into()); params.insert("REDIRECT_STATUS".into(), "200".into());
if let Some(value) = headers if let Some(value) = headers
.get(CONTENT_TYPE) .get(CONTENT_TYPE)
+1 -1
View File
@@ -185,7 +185,7 @@ pub(super) fn directory_listing(path: &str, entries: &[DirectoryEntry]) -> Vec<u
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>" "<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.26.6</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.26.7</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.26.6</b>")); assert!(body.contains("RustProxy <b>0.26.7</b>"));
} }
#[tokio::test] #[tokio::test]