diff --git a/AGENTS.md b/AGENTS.md index d604467..4b97227 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes. The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for -compatibility. Current source version: 0.27.3. +compatibility. Current source version: 0.28.0. ## Delivery diff --git a/CHANGELOG.md b/CHANGELOG.md index d7b10e1..ed9799d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,26 @@ > test-only changes and packaging implementation notes were moved to > [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md). +## 0.28.0 + +- Add a manual GeoIP **Download now** action and replace the fragile release/checksum-base coupling with direct source URLs. CSV mode accepts separate IPv4/IPv6 files; MMDB mode accepts one native MaxMind DB for both families. Publisher SHA-256 URLs are optional per file; size/format validation, staging, the updater lock, atomic publication and hot reload remain mandatory. +- Improve active-listener visibility in Settings by rendering HTTP/HTTPS binds as distinct endpoint cards and showing the active `H1`, `H2`, `H2C` and `H3/UDP` protocol badges per listener. +- Remove the speculative Python/Certbot `checking/preparing` line from the startup banner; log the final detected ACME runtime state at INFO after the asynchronous check completes. +- Add a configurable cache `Via` response header, enabled by default with value `RustProxy`, while preserving any upstream `Via` chain. +- Avoid full cache invalidation when only cache bypass rules or response-only cache diagnostic/Via headers change; representation-affecting settings still invalidate incompatible cache entries. +- Fix the vhost Security header-rule editor layout so Delete no longer collides with the first Operation selector. +- Keep SQLite schema 8 and COLD format 3 unchanged; older configurations gain the default `cache.via_header` through serde defaults. + ## 0.27.3 +- Preserve the existing data-directory UID/GID across backup restore, including restores executed as root. +- Make `/var/lib/rustproxy` the production CLI data-directory default while retaining `./data` in `dev.sh`. +- Add enabled-by-default automatic GeoIP updates with configurable HTTPS release/checksum sources, 256 MiB default file limit, 24-hour default interval, immediate first-start attempt, publisher SHA-256 verification, streaming staging, live reload and serialization with manual upgrades. +- Fix explicit startup listener overrides so `--proxy-listen`/`--tls-listen` replace the canonical saved HTTP/HTTPS bind instead of retaining the old `:80`/`:443` listener alongside it. +- Accept strict WebSocket `101` responses with absent `Content-Length` or `Content-Length: 0`; reject positive `Content-Length` and any `Transfer-Encoding` without requiring legacy mode. +- Make release-version checking strictly best-effort and start it only after listener binding succeeds; DNS/update-server failures no longer interfere with startup diagnostics. +- Keep the sidebar runtime/footer block at the viewport bottom when interface scale is reduced. + - Fix managed Certbot validation and execution: Certbot 5.8.0 is a console-script application and does not provide `certbot.__main__`, so `python -m certbot` incorrectly failed validation. Managed ACME now executes the final `acme-python/bin/certbot` entry point directly; 0.27.3 creates/rebuilds the environment at its final path so the console-script shebang is valid. - Improve managed ACME bootstrap on systems with Python/pip but restricted `venv`/PEP 668 behavior: keep installation private, retry the application-local `virtualenv` bootstrap safely when required, and report whether failure happened during environment creation, pip preparation, Certbot installation or validation. diff --git a/Cargo.lock b/Cargo.lock index cc5705b..194cf76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "rust-proxy" -version = "0.27.3" +version = "0.28.0" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index eaccbb0..7876d64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-proxy" -version = "0.27.3" +version = "0.28.0" edition = "2021" rust-version = "1.88" description = "RustProxy - independent reverse proxy with local administration" diff --git a/README.md b/README.md index cb62e73..6a064f3 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# RustProxy 0.27.3 +# RustProxy 0.28.0 Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration, streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools. -**0.27.3 release source.** +**0.28.0 release source.** ## Layer-4 streams @@ -11,18 +11,18 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners. -## What changed in 0.27.3 +## What changed in 0.28.0 -- Treat body-less `HEAD` requests consistently on HTTP/2 and HTTP/3 cache paths. HTTP/3 `HEAD` now gets the same bounded empty-body probe as `GET`, preventing false `request_body` bypasses. -- Reduce COLD-cache RAM usage without changing the on-disk cache format: response headers and validators are loaded only on an actual COLD read, SHA-256 digests are stored as 32-byte values in the in-memory index, and the duplicate cache key is removed from each COLD entry. -- Reduce rate-limit/IP memory retention by storing each vhost name once, reclaiming expired client buckets during traffic and periodic maintenance, and shrinking sparse hash tables before the existing allocator trim runs. -- Exclude the rebuildable managed Certbot environments `acme-python` and `acme_python` from application backups. -- Keep response compression streaming with no full-body aggregation. Compression uses the fastest codec level and keeps only per-active-response encoder state, so it is not a long-lived cache-like RAM owner. -- Make the DEB/RPM and OPNsense package repositories the documented official installation paths; Debian source builds remain available as an optional path. -- Separate application-coupled scripts (`scripts/`) from developer/operator helpers (`tools/`) and external platform integrations (`integrations/`) without changing runtime behavior. -- Keep SQLite schema 8, the COLD on-disk format and the saved configuration format unchanged. +- Add an authenticated **Download now** action for GeoIP and move managed sources to direct per-file URLs. CSV mode uses separate IPv4/IPv6 files; MMDB mode accepts one native MaxMind DB for both families. Publisher SHA-256 URLs are optional per file, while size/format validation, staging, atomic publication and hot reload remain mandatory. Country ACLs, country rate limits, access-log country capture, Prometheus status and administration status use the new database without restarting proxy traffic. +- Make active listener state easier to scan in Settings: each active HTTP/HTTPS bind is rendered as a separate endpoint card with explicit active protocol badges (`H1`, `H2`, `H2C`, `H3/UDP`) derived from the running listener configuration. +- Add a configurable cache response `Via` value, enabled by default as `Via: RustProxy` and disableable from Cache settings. Existing upstream `Via` values are preserved and RustProxy appends its own value. +- Stop full cache purges for request-bypass edits and response-only cache diagnostic/Via header changes. Settings that can change stored representations or compatibility still rotate the cache revision and invalidate incompatible objects. +- Fix the vhost Security request/response header-rule layout so the Delete control is separated from the first Operation selector. +- Report the completed ACME Python/Certbot runtime check through the normal INFO log instead of printing a speculative `checking/preparing` line in the startup banner. +- Retain the 0.27.3 maintenance fixes for restore ownership, production/development data directories, automatic GeoIP updates, explicit listener override semantics, strict WebSocket 101 framing, fail-soft release checks and scaled sidebar layout. +- Keep SQLite schema 8 and COLD on-disk format 3 unchanged; the new `cache.via_header` field is defaulted for older saved configurations. -0.27.3 retains the 0.27.2 managed-ACME repair and earlier administration/proxy behavior. Historical details are in [CHANGELOG.md](CHANGELOG.md). +0.28.0 retains the 0.27.3 HTTP/3 cache and maintenance work plus earlier administration/proxy behavior. Historical details are in [CHANGELOG.md](CHANGELOG.md). ### Historical 0.26.7 release notes @@ -90,7 +90,7 @@ Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost - Replace backend sort text (`A-Z` / `Z-A`) with direction arrows (`↑` / `↓`). - Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.1 requires no data migration. -The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.27.3 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md). +The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.28.0 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md). ## Documentation @@ -131,8 +131,9 @@ The preferred installation/update path is the project-maintained package reposit - **DEB/RPM packages:** `https://rustproxy.linuxiarz.pl/` - **OPNsense plugin:** `https://rustproxy-opn.linuxiarz.pl/` -Use the repository instructions appropriate for the target operating system. Building on Debian is -kept as an optional source/development path rather than the primary installation method. +Use the repository instructions appropriate for the target operating system. Package/service installs do not need a packaging-time GeoIP download: the proxy starts an enabled-by-default background updater after its listeners bind, performs the first verified refresh immediately, and then checks again every 24 hours unless changed in **Settings -> GeoIP updates**. Building on Debian is kept as an optional source/development path rather than the primary installation method. + +The production CLI default data directory is `/var/lib/rustproxy`. `dev.sh` explicitly overrides it to the repository-local `./data` directory for development commands. ### Optional: Debian 13+ one-line source install/update @@ -180,7 +181,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \ Package-build metadata remains outside this source tree and is maintained with the official package repositories above. -The 0.27.3 source archive preserves the resolved `Cargo.lock` supplied with the source tree and +The 0.28.0 source archive preserves the resolved `Cargo.lock` supplied with the source tree and updates only this package's own version entry. This packaging environment has no Cargo, so the dependency graph was not re-resolved here. On the compiler-enabled deployment host, run the locked check/test/release gates below and retain the verified lockfile. `dev.sh` can generate a @@ -436,7 +437,7 @@ python3 tools/checks/release_smoke.py # Runs those checks first, then the Rust suite when Cargo is available: bash dev.sh test # Source-only package, with no Git checkout required: -python3 make_zip.py /tmp/RustProxy-0.27.3.zip +python3 make_zip.py /tmp/RustProxy-0.28.0.zip ``` ## New management and test-origin guide diff --git a/ROADMAP.md b/ROADMAP.md index fe0399d..d488bde 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,18 @@ # RustProxy roadmap +## 0.28.0 - GeoIP control, listener clarity and cache policy refinement + +Current direction: 0.27.3 -> 0.28.0 + +- [x] Add on-demand GeoIP refresh through the authenticated administration UI and reuse the automatic updater pipeline. +- [x] Hot-swap validated GeoIP databases so subsequent country ACL/rate-limit/logging lookups use the new data without a process restart. +- [x] Use direct GeoIP file URLs with optional per-file SHA-256 verification and support either separate IPv4/IPv6 CSV ranges or one native MaxMind MMDB database. +- [x] Present active listeners as readable HTTP/HTTPS endpoint cards with explicit H3/UDP state. +- [x] Add configurable `Via: RustProxy` cache response metadata while preserving upstream Via values. +- [x] Preserve cache objects across bypass and response-only diagnostic/Via setting changes. +- [x] Separate vhost Security header-rule Delete controls from the Operation selector. +- [x] Keep SQLite schema 8 and COLD format 3 unchanged. + ## 0.27.3 - HTTP/3 cache parity and bounded memory retention Current direction: 0.27.2 -> 0.27.3 diff --git a/SHA256SUMS b/SHA256SUMS index 53f0077..31d8c5f 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,18 +1,18 @@ 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example -48a06923863f33c6a350dd66da8677437d184f4797b0a813930f5daa8cb23141 .gitignore -b17968dc24b2841ee7bf107f54e488a858afd888efea4bc89ec8ad5941dc494a AGENTS.md -dca9fc8a1eb1a4171e92b695b2eb58896c8701dbbdc3acbfc44b380d8a9fb5ab CHANGELOG.md -948e0848bc426a15a40e7dab02817242417d8633ca9c3d89a7dc8e8da23c4d62 Cargo.lock -1a27b4fc0950feac78dd61695131211a16179cc47dab327c3d556a3b3109ccb8 Cargo.toml +9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore +b934978c1c6999e0a5c0ccb443ef2b7257327714c50d2065552ee78a9425a95f AGENTS.md +46621196e80c93b9f116d1f323b14906d6d2d05e2c51190b66b552f78e64718f CHANGELOG.md +09de70df5818af3f6ac155374300737ab3bd33709d0c4eec488a957bc6e910e2 Cargo.lock +c5b85b95201b85562014a0715384c68be56398f1d11b0eac28a431c812991cc8 Cargo.toml 75b008f237b2226c62a7721b7901018ce83bfc2726f047a83a17c66fcb799f3b Dockerfile -1725a09ad0dd2adff1aa32f125207fa4bfa6cbf051acbd3683c66e5961e6ad65 README.md -b6874ca7ddab94c40ddc0da3455e02e6e429292a092ff7152da88490c9cad9fc ROADMAP.md +2914d3a970d9cb952aab78e9113792716681eeba179be1646c793422219974c8 README.md +623fc7e391037cbb5def4850280a4924357df00c3ad96709d94e1e77b851af30 ROADMAP.md da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs 385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md 31d4e0cd454060b46f359fdb84a14b16a5fe0d3baabb387dd584d76e555558bb deploy/rustproxy.service -8b8adb663f9b72e3f8303f4b68d3f401b4fda82f38e0d9e5f6b2c21919ebca1b dev.sh +4f6abab66d07588855c3885ace57b6494ccbfd799fae959cf8d95230d71e25c0 dev.sh 5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md 7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go 5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example @@ -21,25 +21,25 @@ bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/d 9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go 3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go -c49dae9fd82dbad4db0f377e9d2b54debb5fde2a83a08fbae30cc942c79e04ac docker-agent/model.go +5a827e94b9295a98b7554252e080903fa7e2ea723eff9f0e3cbd79a355fb76e5 docker-agent/model.go e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go 634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service 5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go 875faa3969df9409d79f050a84b4183496f557edda3d34aaec19fe556cf8c471 docs/ACME_PROVIDERS.md ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md -c7b75ed137094cade2b2047c1c8d304e936e0fe1121ff91a858f77c2e969fa39 docs/API.md -557d82ec1cf2295d6a41ffed1d862448c1f9441554f1fc07f8a12324f168901d docs/API_ACCESS.md +e51a93b5d09d89878f64119cf7df8ae20773ae754c4fde023b5b79690f776cd9 docs/API.md +1d59f24bf5e9c36591e6a54dbb8909cfea045fe5fd7f11f1e54c50f9f01078ba docs/API_ACCESS.md 4c5288c9ed427cfb919bd023ed9a321dcf6d64bc4cad3e1768393356c5d835ba docs/ARCHITECTURE.md -4abdcc53efdb35cdd8feb024dbb81188da12c6abd37eefddecdb57a7eb93d6e2 docs/CACHE.md +3f18d48514ad003ac24ad07ea47271a5719dd8bc1f2329f0c3fbae774e086426 docs/CACHE.md 6266163e17d2921468abdae92c1620266af8351321530e2c9b7dca6f3dd89c88 docs/CACHE_DIAGNOSTICS.md c6003dcbc0b95e2cfa9523b7017186cc8b4826248b588af77111ac5eb15174f9 docs/CHANGELOG_TECHNICAL_ARCHIVE.md -17d9d2275d2838477e7542b164ebd41ad118ef06afdaccde36c95f8a522e5939 docs/CONTAINER.md +106eb9347b339546bcca703c64973cfc586a43dda806281c5bb894bc5cec5ca0 docs/CONTAINER.md 9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md 16d8f59908c66e595d6003495cbf5c068c68b10aa9495eb8612c38b2de0e4a7c docs/DOCKER_DISCOVERY.md 9ea8db4d034ecb573976dd323da29f044538525c230df9f43a47be1ac867b24c docs/ERROR_LOGGING.md -851e66e5dac47cfc70d963833d137fa0e35c04c374e9b943ed9b7345a51802fa docs/FILE_LOGS.md +1da868c5e965ab0f99f3b5c58851bfe3be96533bee5774f6e40318dcad7092e8 docs/FILE_LOGS.md 8dfc89a8b3512103b87864bd7c24a3dc3de4465edf8f9874e6bed1f4634dd930 docs/HTTP3.md -5b48bc23ffa2dbd1ebd2ed5cae9e6022dc9c0795a2d0df9565816c11c0096421 docs/INSTALL.md +80e0d15afe4c8f68b0d6ff9c4047626d42201e62874996947fdee38447b6ad0a docs/INSTALL.md 3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md 4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md 7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md @@ -50,7 +50,7 @@ e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND 89a1af6c9fe10c572b211f9632ce256166e67990b5dacacf2ae07265d99a5172 docs/NPM_MIGRATION.md 712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md 572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md -4cf425aabd16366df99e447d3eba2a362babeff3de7d014e189df51def657039 docs/README.md +71dba13d40df2199c2e6ae432681c73b6e72e5715ce074e3926ef9034a512932 docs/README.md 6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md 0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md be661dbf81af840d4d8aeeb5347c24c7faecdecc73a8cf0f6f09053f1abe04c1 docs/RENEWAL.md @@ -58,9 +58,9 @@ a2bb837cdd3559d58158880c1f832badf64035376a3623050bb4e40c2bca104c docs/ROADMAP_H 2b760c88e0e093d7705820560fbacf30bfc7bf6d67f2e68a500b3d4d970d80b6 docs/SECRET_STORE_BACKUP.md 5dbdf45fddba198a28f511dbbd2f12bf7d2b88c41c3cbedbc20dfea3858779d3 docs/SECURITY.md 620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md -632976125cbd9e96e91d1771331c73d1ac5adaa796476a20f8546971787c2529 docs/SETTINGS.md +63c776e2531c3e58d067caa921fbee79d0c9dd8c26a5e91a7ec10af088ed9be6 docs/SETTINGS.md 3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md -cbf3e7cfd32c50026df333489a8afb5056e873a9360bbe135b5ded5fadc4318b docs/TEST_REPORT.md +1a89213f3863b5bf140b9fa48f7ea33ea497da8a89caaf71e4314ebff32fed6f docs/TEST_REPORT.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md 8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md @@ -98,7 +98,8 @@ f9667fe81b31762e64d36126a7e94b609316dfa856de8f8e3b96661dd2a918c6 docs/V0.26.7_I efd5935852d4cfd09758bf67c70e437490cfce2baade46c8733201e506dbcc42 docs/V0.27.0_IMPLEMENTATION.md 9ba6417417a0eb293879b26b019449f143e773d1482434bee88f5eaf1fe34b90 docs/V0.27.1_IMPLEMENTATION.md 845cc7b7fcd553ef99613817e70fcecc4e50dd8fa77066a75577d63a7224cdec docs/V0.27.2_IMPLEMENTATION.md -86cf7498a031ed6aff8a79140767eaa07b696669623900b971f66ca5392e5174 docs/V0.27.3_IMPLEMENTATION.md +49fca8928f3ed6169df4480f9658b65b8d9dd5d1331ef5bb2b1f9adaca8212eb docs/V0.27.3_IMPLEMENTATION.md +713518c1a09969703d7b09b83380d5e659f5d1587ef23585abfa1fa87c033ae5 docs/V0.28.0_IMPLEMENTATION.md bc2ac0117528ed4616c68cf7fe05cb1d9bbd36ecaa8573828cb3b3136923f265 docs/VHOSTS.md 09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md 4026a4adeee70f98a96cc7aa8d8be16cda42ffb2e5890d38f1ba638134ab0675 docs/WEB_SERVER.md @@ -113,9 +114,9 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock- 5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json -89e53a8f7098bbbe64a5315dde365a4346ef102b0a8e457b941cacf7b843e04d grafana/README.md -e87ac9f20310c265ce580c0adcef6403728452f98f12ddba603859ff0dd34458 grafana/rustproxy-overview.json -fe1fc76bcb639cac0609a8969cd92725cfd77d87f1d8519756ef16825e04914a install-debian13.sh +62ca0f17538ae6f7b38aacf1cf9cffe765a7fc51579f17637f9268b378aaf7d1 grafana/README.md +3c565201675dba48ea7f2504fafa5e74017519154404b1ae944a9fbd51f7794c grafana/rustproxy-overview.json +a51d911526d2631282a358be4152ef1ad6ed4034e34f961a5290b1b90fffaf32 install-debian13.sh 909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh 42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md 2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 integrations/opnsense-acme/README.md @@ -127,9 +128,9 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r 96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh 3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh 71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md -d0aa8b66b39b1f0835e558aa6e4c9d860bc9a26187aff887463cff74397b3dc1 langs/en_US.json -69f8b2c88a04f8a5d32290df35040e1619513dfb873a9ef4bc5dfbd655ce3316 langs/pl_pl.json -113458f12c516f1e4516a341d6e558cee8a621ac16ca23e9bbd865ccde9e1f75 make_zip.py +c54cc75de6855c7932004de5d770badab43c99cf8ddcbc867a5bf6d3557bdd1d langs/en_US.json +008553b6897ecd614d4908063cb864284ee7090765fbc96bcf332891aaaf9de1 langs/pl_pl.json +f80544d7db2a69a4866c72e1523920587c4295af77ffcad49f8c0af404bd966b make_zip.py e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql 5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql @@ -145,11 +146,11 @@ ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_r 6e036f736a2b40feb78eec17bf34871771524190f283591d1c89e48e6d2271e6 nagios/check_rustproxy_queues 7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml 2145ad17ae7a823756371ceab6bdbeb332211df93193ffb4e486f7a80c4f29ae scripts/README.md -783afca6730accdeb74c29dd0a05123562e0fafc5f8479be6e7ba0feaf80a923 scripts/build/swagger_assets.py +9e71cb39113e034a9e8bce90d3a2cfee9502c3b1bb17772b2896c6b3e6d0b96a scripts/build/swagger_assets.py 187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy/deploy.py -475aecff3e44a12199a29099a8c5069c37d880ff0307cdfe7af8ff66e532bb16 scripts/deploy/deploy_core.py -148b5270c8f9bb78c9dfa783c6ca6ddcd572b5ab0389568f780058662f03032e scripts/deploy/deploy_runtime.py -83c730c499b5099f11d967a0c8cb12aaa595bda60f13e22d7f61bf63a756d8a5 scripts/deploy/upgrade.py +a9b3e34db18f623f5d7a6c98f721ff38d8b1273bdb7e3bbea091440088638493 scripts/deploy/deploy_core.py +2eec7ff7c689416135984e38a90c177a8b06bdf176da69294cce25e5571847d5 scripts/deploy/deploy_runtime.py +d5fca6ac46b6760f253f35210134d26f68e6076c28c7f1a93123158add83c40a scripts/deploy/upgrade.py 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py 31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py @@ -163,39 +164,39 @@ ea272433e7f92e983509a8ab1aa54242440abef21df526c2f99944ef279e09b9 src/acl/tests. c5334de6c732d76b2bea7bfc17b3ac305a250bbaad31a5420d6a7d7abb200449 src/acme/providers.rs b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/providers/tests.rs 29ff0647417b0f1d50a4cc075ab51afecff9ddf47db3eb7948d51db5a38f33f3 src/acme/runtime_bootstrap.rs -cbf221cd6695d8cf1f03da3aa2b42ed4b1afbfbc2e496329b2d29f7cc59ce0c8 src/acme/runtime_env.rs +c8941a929dfef9e8875adf534394ca01a1532e567d5099fefe3657826886ee25 src/acme/runtime_env.rs 7acbaf34e7b953b753ab074893e47b5024f0cd756dc4c7a1074067dd936c2e8e src/acme/tests.rs 55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs d18efa3f797f62b3916265e30ba546725a41e7ccfd9b123965ff9f522e3baa88 src/admin/certificates.rs 1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs -09c05dc6ca92b2e6b32d1bd8e92739aa31b850abe04d7aef711d80828750cfa5 src/admin/documentation/documents.rs +cba6baa861709a77983586da493d0762e2892ea5614f187fd6c429bc735b4cf4 src/admin/documentation/documents.rs 67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs 002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs d3210e037481e1f95e840550428e088ffd83ec9a42f1810e4c976298fb555a9a src/admin/file_logs.rs -beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs +de331ab75c31df65a4f0f1607dbfb26972799bff5d9f232d8e914c38225cffc7 src/admin/logging_api.rs 58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs -28611fe61da40ab45769055707235dca3ec5e0b1eaad81f130206ff7b9a7819f src/admin/openapi.json -a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs +490627376801eda6341bf8c28a646bcb77460bbd39de6d7934576b6a7391df43 src/admin/openapi.json +6b14f2a884e41745ad6f99d5ff4d36f502a3c0b5ba09ba15802eddabccc46177 src/admin/operations.rs 5287fff6b3ad436ac695e9b66feb1b0b5705b744a9f00cec7ea6a3e951e5eebf src/admin/preferences.rs b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs -684d5d28e0d2b10fb2baf2a56857b226d6334cc3346e1ca86de173ff914bee73 src/admin/router.rs +772063772b8688317aa4d7ba3a2ac509cdf0b10e0f6980ed058cf4db96054420 src/admin/router.rs 333d5f9f4c27941c535ac689e5826264ffa2bb86b00832b597a9bb9f811f01e8 src/admin/session.rs -1abc9e1aa8bcc7eec9139daa9c70f3ad0c4796622a7754a1623018d37d4aa13e src/admin/status.rs +bebf42cd1808a99a0e669d483951a441ffa22c8d5a0a289c64491adf13c8841a src/admin/status.rs cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs 59ea2bb773c86c3d8240ccc212a8bf7f670e17e360def9e773906469ef135ac5 src/admin/tests.rs 5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs 6f8c1e30babb2cc4cb7accf4bfa28338651a469b16341f6e7eaec881dd9abf2b src/admin/tests/part_02.rs 77608ad33da348dbea41d2a7ceadb75b0bcf05336cb5a20b7f2a47502f1e4842 src/admin/tests/part_03.rs -bf1757ec16c144c7791c8e08ac2c93118aa53a88209315a6928a63ef2aaeec31 src/admin/tests/part_04.rs +46c6d04fb6a125ebb00f7295d3ad3c16071477fc40f2b377455ed7a6eb1370d2 src/admin/tests/part_04.rs 5f0062abc42a3b81c79088dc0824ab4c81a22c6ea765e77d2527d07d13bffd58 src/admin/tests/part_05.rs f869e4da5de149fc04ce1aab658befeaf3208c862f149ca976235d15832c3cdb src/admin/tests/part_06.rs 5fa006413fd93529ac9aaa4ffb28bf6b0b88d0a55a0bf0c49576db4a06fe9fc3 src/admin/tests/part_07.rs 2ae5bc857dd62882f16d1cfcee9691c9ba8afc149f4c0435addd48b564487517 src/admin/web/acme.js 446011c2c7afddf7734ceb268c75a3b4146dbe8e915b11986495e956b6c62c69 src/admin/web/app/config-history.js -e701a8abb1893c5db18f8ef5195d323dd9aae8a820eb4c198409874d24280652 src/admin/web/app/core.js +ed063b8a414cf3b5e539c900e8722aa82d84c561a5cd54d875650793a25529e2 src/admin/web/app/core.js cb5c4c2dd4964795b61393b05537cc1528c72d7c1390953f4d119a5e55c65c7c src/admin/web/app/preferences.js -5891cbb5bfe7219ecb85d6f8d1794be1c3e4593fd3caa0668b53bd4121d083ce src/admin/web/app/runtime.js -adb56a17d6e1d517693d26c02353ed10ae277d9ce39ce15d9809ca91c9a736b1 src/admin/web/app/settings.js +b93b5117ff0977df2f5ae783ba7f08b6b35e3ce6103cd0a38a4a38047ce26485 src/admin/web/app/runtime.js +3b63cc15068d287fe873956b38306abaae8c88a2f288ecd98159c5c47acb98e1 src/admin/web/app/settings.js 74777adaba5a448f3a9abf3a46d91efc6eb81ff6f3b5441e5b99a9e1d6c09af0 src/admin/web/cache-panel.js 50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html 720e201a29d4b3d2e487aa9b857ddcfe785f6f26190827ae5c265ef8c308a86a src/admin/web/docker.js @@ -212,19 +213,19 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/ 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js -be1d4004d7188e92931dec34f49c1b8491f1d137573621176aae66217fdffb0b src/admin/web/index.html +ba9e0c1c874ace33d55287b1ba36935cc8fc49dae3eb5673e82b5c7f8fc05e12 src/admin/web/index.html 24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json af0fbd94d9b88ce1cce28e39dfd2c35de031adcba412dd4a9a17d9b416c256dd src/admin/web/login.html -45ca44ca82e43373f018629839287645218f225747f02b9b4a5d80e2bc8821cf src/admin/web/login.js +99fe9b8ba7a8a24e5bbe9ae40999484fe13b0faf5eb1d7a9193701f667e51ca5 src/admin/web/login.js e169d77359785ba604ce1a728420571fdc9b491fc1c0088642399466fbed8c1b src/admin/web/management.js 90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js -20670b3167a56e0c79b061ea1b9af74a0d1e4dedb4b57cb8c211463610df28eb src/admin/web/settings.js +0ff365087966846a7bda3e159f061443dcb32c35a4b3f6f46ec5530343542bf3 src/admin/web/settings.js bb67e77af5514478485cfb69f339d63d7eb0cbadd093fa054814a416260ccc1d src/admin/web/streams.js -625702e6e58ef6d78615909d02a0a236eee1a8b22cce451ca811ed30bb35967e src/admin/web/style.css +ff777b36069747dc30c10fdaf16f610a866879f6acf8af6ef67fb96c1d9b00c2 src/admin/web/style.css a8a73a4fa849ae49391fcd6143e900c515c8b577f71b55cbae9701fa9f5bbe47 src/admin/web/traffic-panel.js e686d68fd6a368110a85dc4c5dc64e8093950d20d63e9aa44790c03dea6fc193 src/admin/web/transform-panel.js -f746b0aa758c1f9fbf5e656ab269f3ffe32675b90c83f0ed5b1e398322095e34 src/admin/web/vhosts/core.js +afed64f55bc0f377ffac68b630b8e3a96dcf2f735a42ab658d74d9ce15cef265 src/admin/web/vhosts/core.js 4cf331c897574f7cba035e0af12f3fc5e2c8a6efae790f8ca3be77d29e4fa593 src/admin/web/vhosts/editor.js b17f6a06681102c507aa7bdfb48ca5a713a687de3a8ac2babf148e69943ce033 src/admin/web/vhosts/render.js 319b1f0b8b9591d54da7a9be86633b33520a259ba96e7eb32fa2a28817e768da src/admin/web/web-server.js @@ -233,11 +234,12 @@ b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files 97b5a060f24a102bbaa2f406426e985236c0d719b8038db210c25b34fcd8e923 src/auth/mod.rs c96a87e82ff0c1c9ce3d0cd5135903bb53af78d1c4fb6ebfe5fea503bf5644ea src/auth/store.rs 745226cabb007819370fc6aa680995a03a8a13fa057dacfab0fcc21d0c43bbe4 src/auth/tests.rs -b0efb68dec4b0e8611684457b21044d136882ddb5c529f934061a55d377d9b28 src/backup.rs +09c3a622a7d2169e49e3369ea5f0f816eab2f717eaf1675c625d518c8f34d367 src/backup.rs ebce56a154c57bf20d6618de161359c7c49392d7d24e8d23428b7d78a7a060c1 src/backup/archive.rs -4d7beea1c3d7c65ee6018d22af6461369754c67475b219d3474a85476cd0b57a src/backup/snapshot.rs -c0561c2ebcb67b20ab2aacd18f61337269b437dce32dbb11bd0bf01a4e204f78 src/backup/tests.rs -c8ee6afde0e463828757a9c2aca25506eb0a7b3b9369f4ec955857ab04b1f9d1 src/backup/validation.rs +732cd4f9ed38ae69f8bbae61b1db8baaa6f86b07ca21c41f24a206a30f0a96fc src/backup/ownership.rs +a4ed02528513a5c991463c368a2ce4c0e2dcad023d106900a37b560cefc8084a src/backup/snapshot.rs +a00571b409d305af86ab2a57a25434b5ab58706772df440d017fc1fec097f7b4 src/backup/tests.rs +b2ce83a48736c082e6b820e4e363cca7a5018f3ae2a1ce5adfd6af1a03d62598 src/backup/validation.rs 426a946b2c9e244f09f6cbc5f9664b8ad0737529a9bb5a3d549d27196a1f8043 src/balance.rs 3d4b3986f54bed99212063b6d135d40a202d556a3e4d253e24c74be4cb9e8ebc src/balance/health.rs dc9335e018d61cddfe3b4c9fa480913f4444c83e4fb1384e3b046cc40940a2c3 src/balance/lease_runtime.rs @@ -270,9 +272,9 @@ f3e026a3c0e953dca3e5c9def43d864a7706afcd7cbeeaf0f21c059a335b30cc src/cache/look 58ede9780b70ac46fe8b35b6a85f02f56f9a2c75b045e3435062cceb3921f3aa src/cache/model.rs ba94226addfd6cdab67d666dc15b8412c4d117111e44639e503e89a95175d0a2 src/cache/model/cache.rs 0ec4f6d0adea1ed80a3ca17c68c9c85502491da2904d9d690b4169ab9b025b21 src/cache/model/cold_metadata.rs -dac97eb28052795b7acbbbab1eaf94e8dc1186b07a7e0c4b7dc522af6de02574 src/cache/model/entry.rs +a99e79aba7f7614712386a4393ede0f3826be1f74af0cacfc1859a17636b8e6d src/cache/model/entry.rs 09a4417f82984eca8c040ea28eb82b3f129c347d951d4b4fab704bb194d9b985 src/cache/model/maintenance.rs -36d471e4b417f1eaffe1fd39bdaa64b6a6aa68faf58b26fe42d6b977f08769d9 src/cache/model/settings.rs +e477f9b9fc63347ad69acf6757c54c88db26ebbf486d5f3229247bec982e836a src/cache/model/settings.rs 55dcc0ad84d6b73689ba43e759d5685c406419c7733e23e78d5b072891ab17d7 src/cache/model/views.rs e26dce02df9c043329ca164fed215d271781bce8fb03240f528331bf3df6b9d7 src/cache/policy.rs ee06af7a6d14bc12e73be5c1bd494426c1735d0bba2a8870a2aea4764ba4270c src/cache/policy/request.rs @@ -282,7 +284,7 @@ efde2bd046ed073900cd3e67051f0542c802a06e8f5e38c2c20ee92604080c36 src/cache/test 13dab7b7a587cfc146826a92ab94ec9e04f7e500b7a84e678d77226829844923 src/cache/tests/cold_tier_tests.rs b4f609ee900c556b613136bbd3fce11c242a415d277ac3d25bdcb72275b66832 src/cache/tests/cold_tier_tests/part_01.rs f43aa6c231f2c85ee3390dddab74aab2eea2a3f874acb35f145c23866266cb52 src/cache/tests/cold_tier_tests/part_02.rs -8bcbd6e8158620c4e0c056b2872e0b5f786d5f2bc2e2203351b8416a94641f09 src/cache/tests/dashboard_tests.rs +e94cb7b767af1e7adb284d43604e1553cff1fae7cf72dccf40ae11f434a4a26c src/cache/tests/dashboard_tests.rs 6c69ef40567d34671d26231358e2db74cc26ec0113843303caa98d4dc0fbf1f9 src/cache/tests/scope_tests.rs 578b18cac918ba51fad775eca188f645b228500e59eb1790f783a0b8d0245b7b src/cache/tests/tests.rs c33013089bf9e75a07a5b43f1030acaba8aba4098796e393ed4bad784f4fff96 src/cache/tests/tests/part_01.rs @@ -297,8 +299,8 @@ a8a0ce446eb53ffdb8f8d97efa3e1344410dc3798b25f62b8817e8e160d28940 src/cache/tick e2f58279499b7c3b47c65d3a7814bb5c7cd6506544d939875fbb0f91fa9aa59d src/compression/tests.rs 7921ae451ccd65d647165b02919bd97399399da471454a6a1a1f63e682b76fc8 src/config.rs 7ba52d3de4d77d1f3cad1b10de418953adc33901727a0791980847ae223929aa src/config/backend.rs -8b39e92594133c32b6e113c67849fc10f51ceec9e3bbf69211779f6628d0e001 src/config/compile.rs -756967406faf9336fa07dceb2269f330f965d0522a2ce45543f566986a5c7007 src/config/model.rs +f6e9452a960f1d1c825ef6459125eda5ec9d16d5c461b5c6d21baae8e7d95d6c src/config/compile.rs +7572ef3ad2125806ea5fd9a7c31f3e7da860d8cfe167fcf7cd6fe4b345b70199 src/config/model.rs 2ee73f5f68b3fdaaef787319b8871ab3817a2052779fd0575965edee1d468fba src/config/routing.rs 9662674d17740df4999f28bec42a2fbd3453a93e591c88f0796edc396bb23a18 src/config/snapshot.rs 7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/config/tests.rs @@ -316,8 +318,13 @@ b45d6040810c93ae2acb04ef6b0a59c189751edf9008cd81a4521c9e04e7f07d src/docker.rs a12c47df4b90b6fc36c978a42477e6ddffbbbf69aed7252101c2fd829709bd6e src/docker/tests.rs 2a02ea2e010cde1b1fe99b2e6baa80ea344ea6b332dbb3c4be795b71541f64a7 src/forwarding.rs 057e652f216a1478638cbb53b1c64ac88c7f413a65825caacbf893c5af004a63 src/forwarding/tests.rs -72215ef6911893f55cc82278169268f03d869b94728f118bf007fc555d609fde src/geoip.rs -e40171e72327bc4350ba95cedacde2f3c9148c26a5a232d9f790f4449f20a685 src/geoip/tests.rs +6823040f72c8de387050a17cdca1acbba39b27858b150c1df71644ec11db3070 src/geoip.rs +80850f7acd1ed4c0048b065e9e81740869a8efbb0d9b5d2e83d8ec7fbc494703 src/geoip/mmdb.rs +06aa056d789742cc11c8826c338d985e6a6fb22da6c3530937eec38e35478034 src/geoip/mmdb_tests.rs +3e673d15ee8640b272026a0618323f666dc79e1ca7fb15552928e913c8b69ec5 src/geoip/tests.rs +70091bdf6f38f03b6df8c8c4407c300677e57f293130122503f936500963d37c src/geoip/update.rs +c1b49bef28d87712e4c46138e70b52c4bcfb1a00196579031fac043d0b4aaa7b src/geoip/update_http.rs +9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs 13269be4ad8d3e4c91893753f654c3022e6f86d171c5d1ff21eda2d9185ec61f src/headers.rs fbb6859780309f8cca1aeaba43fa67d13a1edf6f5a60c5c982d2067b71fb0846 src/headers/tests.rs d0cd8589321595309834690fa9ddb98488886bc98f34ea45aa0ccb3035412fb5 src/http3.rs @@ -331,12 +338,12 @@ a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/fo 7c0d8e8f46f96cbaf3a5cb5fd47953f9185d7558dd71e599676bc63578b7ca99 src/logging/runtime.rs 17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs 3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs -c66a9803e43bdd87101825e8c71ab33f890ac4c3428467ba5d43c17fdfa1439e src/main/admin_runtime.rs -e1ca82cf6f0360176323b32907a5c8dd0646d661ed75a935af003c0b1ed691cd src/main/bootstrap.rs +2f6b5a9517453541bf43580a9d963e64e36338575ee9ad295ca3718803a59ed6 src/main/admin_runtime.rs +c8972e8b2ba3d8ab91678ad990dfb7da39f901d81c74887a3910a4baec5194d6 src/main/bootstrap.rs 5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs 4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs 0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs -b26945f6405493df85dffddab3ca8804137e6cb99c6d165eaaf0ad024cb570ff src/main/tasks.rs +c97330d33287e3ce8901b58117b21c0a66f92446df243ec39eae5fe1affa0a4f src/main/tasks.rs 49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs 201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs 49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs @@ -346,7 +353,7 @@ a4702c95c7e5077c2ccff86e2e3216e1da6ff9ee4383ae199a0aff2f78db325f src/not_found. 6aab4f97d72ec6352c940bc89e911b8e50c580821da8202744e9832621458784 src/pages.rs 73a0770726eb90f89fe1a65e0e146c39209120e70d5817fe2e8e63f3c3521c40 src/pages/tests.rs 4198844bc70c59ca3f8da812928391e2ca3d4eebc84bde432a5724c0c63d414b src/prometheus.rs -ff1c502ed1ce8d5de9ba7685253a6b70793ca9824b75fba58bae8aa6c4c11b84 src/prometheus/core.rs +de67f5ef52289506ee3b4b4ceb72923eb16b8f53ae55373c6c9e7a01900dadcd src/prometheus/core.rs 5da9771bbd9843febe80cc9a722a9ada9d20cfb8d22addde7fef2c4edb66f327 src/prometheus/docker_misc.rs d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus/model.rs 064b2be7fed2f6acf526b1cce070c1bb1b943d36f08fa50bbfcbdf3cf1c9318c src/prometheus/tests.rs @@ -361,7 +368,7 @@ cfb0910af8cd868bde3f6c9992319e46d5328d6d8f69c50701df0ea12bef2108 src/proxy/forw 51c620b77004cca26edf387f9650f07d27801e093d32cc0162eaee1d0702c126 src/proxy/forward/success_finish.rs e5b1812c75b4cca91c32e99d28622cac1dea8e347bdf1c46e43546cb341f95a4 src/proxy/forward/success_prepare.rs 23b6bd26f6175b52c13d2571318fddd12550ba34ee77b87b5ac0e4811f8e2bee src/proxy/forward/upstream.rs -1a7bf906e74fa0286d84f8e88a1b2a977353dab91d036d192d5f28989e7ddebb src/proxy/forward_entry.rs +fcddbe41ec8493dabad338c282679d1679d0eb4dfdec51abdab1f80f582ba3ac src/proxy/forward_entry.rs c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs 5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs f6aef54204b5bb9305dd1367818c7ffd3cb273008404c9fab0317af1d13ba7a6 src/proxy/mod.rs @@ -369,8 +376,8 @@ d75ff3e38affa49f9a8b9d6801454c1449458e5fbf07e08d51aa3f42abebafd5 src/proxy/test 9a4c13efb218a5fe8689db5bd6b66029ca6ec270724d948651f3b18e78c80fba src/proxy/types.rs 5ad8b7821b53804f12b151b2bc39cc586401d9fa71d287d15d1e970b0160d14d src/proxy/upstream_helpers.rs 69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs -d05e655ba0b3b322c657aaa9c2cbe7a0181064a896026b5eeaca347c4a0b7831 src/proxy/websocket/handshake.rs -b21dc9cf3817dd4e3273ce25887d63d7a772c477b86ca57f8d5b72b8154456e7 src/proxy/websocket/tests.rs +016a60b2471edd3df9bbe9f92a9cebea11047178b83ff0e3b2351c12814a4939 src/proxy/websocket/handshake.rs +7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs a44421714aab37f7ceda19e3ec2512c645d2a3ce5dcb3da5bc7a0d87a442d6a8 src/proxy/websocket/tunnel.rs 15fabd38576b61f650c039acdbf7ac5a5bd24330062475b94315285b2dc258b6 src/proxy/websocket/validation.rs 463a871aef70a5531c33a9ea43b9dc4e628b272d654bb225e6161aa1497d96f3 src/rate_limit.rs @@ -379,11 +386,11 @@ d40ad63939097d2b91339127d5835a74b618d83b7ba31db0c512f05a82107d6e src/redirect.r 811c83ae9fd0f5a13d35b5443cd37843f82b9d600bee4bb136ba9b87732a5347 src/redirect/tests.rs d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs 4e0b25c2b86eefddd76c64b39dc1c1cd6d243addc32cdea99e6c3ac84ed879a6 src/runtime/certificates.rs -2ef0bf9a1a80a4e310b274789e026590f219c6579f4f259af3d83e66ae76697c src/runtime/constructors.rs +ad79a64f0fb31881aa93a2305fa3084d7e02e15d464c71dbba38123bd6a81fc5 src/runtime/constructors.rs 31dc5cdea00efd63594dc6becd5a9103594696145cbc8e7b7594fb407214ad9d src/runtime/docker.rs -c052b35673d905219a71130b44a5dc0027bf9b853bff0b6bdbed4be4064550ea src/runtime/tests.rs -03144b1b00fb45c5616ca9c9b0e57013e86c1f07f129830aaec98a73b274199f src/runtime/types.rs -019543a127009acdc2df05a1bbc83b2413c22d056d638a53dbe80636926a546d src/runtime/updates.rs +b3f2456ce8be5ae54399c3cefbab4f99a518631ae57eac0dcc21907c6eb97617 src/runtime/tests.rs +571fd71115e4c9c3b31baf85978eec46fb87459cc1b6ad1976203142ce821970 src/runtime/types.rs +88bc546fdda20d3f1f8f5e7d7872ede1fa334b8bb5102c729bd72f56e3fb94b9 src/runtime/updates.rs b4c17b304166592e63c81884076589fbaf725c3f57043f8924c46167b061ac5b src/secret_store.rs 6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs 6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs @@ -394,8 +401,8 @@ f2f6196632b2fdb2444189624f961edaf5bdfff57f700b0207b57e2491c1daa5 src/server/htt 71899a9b1897fa22ab9e8eb6a670054a6d382bad78714006b9b218d07ba99902 src/server/serve.rs e9e478cf45b88994eaffd115f51764b6b07852ee3c941cafdabb20584d3dabc0 src/server/serve/tests.rs 048f2c7a8ef698a172ba7ed844558f4118b367b2cf340748c5c28da7723d8e85 src/server/tls_runtime.rs -576af23edf5a85837925833f4a57c4fc686c4d20e7625f94560e5c6499f6bf0f src/settings.rs -0b63a0b73ddc93be259182f124f46ef59300ad688ad74b509f5f32a29f968566 src/settings/tests.rs +392e4321e5079bd94c795e9bcc08d0abe4b40e721d0e6a36e115f9fad7e86478 src/settings.rs +3b3893d899b8e82969588f30c1e0aeff250449e4c94627ff5ad94d29f227ceca src/settings/tests.rs 566daf9c0e1a30da2dc18e51090541a85ced3320a0fc00ed1d1de3e2b4da5072 src/stream.rs 46d9f63ce13a634e00f38d36ca3bdb217f2e37f04d75a4ef118e7dac9896a489 src/stream/model.rs 9c59bdb856106c0a74d1f8c156b0dd7289a170322ea556bad19bb8de3bf13d69 src/stream/runtime.rs @@ -428,7 +435,7 @@ fde2fa3aa94b30d8536afec1c11348cfbd922d0b5ff89e91d34266e8e3863e26 src/transform/ a4e1d2aa2913587c250f251356431097b76995aee6e8bf9e35c4978a95491de1 src/transform/svg.rs 0f53c64f442390ee20a59087a10f8f9ffb58053263e9aa848ddabfae1a6f3f51 src/transform/svg/tests.rs 76b3cb4ebdba9cbac8e388d3a9d74bd2055db564d8e31869a60e7a439b46f7b3 src/transform/tests.rs -639ed68b2c62a9c8ef23beac480ed4ecb0a7d31672177c9e0bde6c223137fac1 src/update_check.rs +6146c59f36cf419623747d497b3de685da6c84964df43317bebdcb21deb9ee09 src/update_check.rs c42d000d1d576721af3ffe930589388693001b8113554a7658f6dcbc665b5d1b src/update_check/tests.rs a67e58a597a5eeb7efe605ae6e438a8f84ff6a1cedf8cd228141cb95551872a8 src/upstream.rs 225e3078e6775ffaa6dc6f12bc87503a123983cbb347edf38cae4c37ff02a5db src/upstream/client.rs @@ -443,7 +450,7 @@ ff4980f0a091da9ff1722d6b9f6662529cbd7c5e2ac68dcd22a507ce6aaf3011 src/vhost/vali c1fef6dfb79275479c15097b211cd5f063ad7e0852a0767b0145cf4f32f751da src/web_server/config/model.rs f1d3e427c53f8b5be5768d85e69ead746d83647fcd3b355acc98e3f9d5e217e0 src/web_server/config/validation.rs 86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs -640d629c6fc2228d864cbe28b9982cf5b5e38feb941a5dbafaa3fd7fe1a924a5 src/web_server/fastcgi/protocol.rs +82c3f7ba6c5a610d0819002a9c3c68fc0e00c69b38832cdfac7536a747288863 src/web_server/fastcgi/protocol.rs 134c4998f6a0aa27e121af1ae3355b7e335488b2ae867ac0a934c58b4eb77191 src/web_server/fastcgi/response.rs 8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs 0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs @@ -453,14 +460,14 @@ f1d3e427c53f8b5be5768d85e69ead746d83647fcd3b355acc98e3f9d5e217e0 src/web_server 80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs 67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs -9f49fe34602f7c81543eef12a337c79d20b4dac904a9dfe5e709c123b2c9078e src/web_server/response/file.rs +35c03934f0e4e9e59d5639f69d488d8ba9283eeff8ac01d798a168aa40ec2ce2 src/web_server/response/file.rs 9aa68251a5f549f4ce3b1bda88d4abb9efc629caf444989381b4117c9488e37b src/web_server/response/headers.rs 2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs 400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs 3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs -648d7d9cfc8cd5191fa7dab30c1829fc245f0d4f3e238243d7e32644125a135e src/web_server/tests/part_02.rs +9885d5c91066668e4f59fe28498dc4412ee09920a274fbdaf4d3c9c7b93e41fe src/web_server/tests/part_02.rs e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json 1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs @@ -469,19 +476,19 @@ eecf680f001f2110dde4eceaccf2f922859a16c946a644f8854e8de4d3771f7c tests/policies 728999110a6d8cacb62bc0399bc333fa23a66526ee23f563a3c93bf00e5fad19 tests/transport_config.rs b04362ad082a32c9ccc4cf38c46f44d71347adecf7aba3224b3af3ea4872e3d0 tools/README.md 358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py -ba6ebb2e393a89b78ce138f2572156e51a6ae557c3aaefff3d8e52daaeb5dc61 tools/checks/check_source.py -873f21239d31659d1d8d66c5645032583ff4b7032f4712af34137afe510269a6 tools/checks/docs_check.py +2a90edbfe9f7d8764a73205a8726c8c8ac904590ebf0d11ed782c7a69b395797 tools/checks/check_source.py +bdeb2977fcff8950cc224264739c49d6febe4327a35b384ad14cabd9c9133726 tools/checks/docs_check.py 4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py cf762780c4085791c72c3c5f93054d7f2a1107403bee0616a1f4113bbcbb99f9 tools/checks/memory_contracts.py -4256001fe5ac0677b87b1384e7e1225e8a19686a5daedf06699cb8663e77d679 tools/checks/preflight.py -39422107ae42162a45a9dae3521125f648942c67a336f15d444f98e3c5b80007 tools/checks/release_smoke.py +27eab52ae463df0b06468e1b7fb2372f3135867869421078fb9d7beb4e717b92 tools/checks/preflight.py +0874a5faba6beb1c21c8afc0c1375322518a662d8709ddb0704840f5ed95f6a2 tools/checks/release_smoke.py f69845bc26ec33160fc756953991cf32aa8ad6a7fb4a131caa2093db91e783ef tools/checks/rust_module_contracts.py ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py 23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py 2309c41e769e797c838d4f40d50bb3c6b9ad9ad0f9ed07fb93e07f95d66563f9 tools/checks/source_module_contracts.py 0aaa80453e25aae7ea953fadb91b3ae3eaff627977c81c9ab5a2779f9a93582f tools/checks/web_assets.py -54cb50332c5805c0e11857161284bb001eac12363d555714c2ef3ab00b1a856e tools/checks/web_server_contracts.py +54d6d339071743d4098ed5a6aba7fccd142d57f224b15431c4373ea35949fceb tools/checks/web_server_contracts.py c245f8f832bd2c36a3fbd5e66fe98e7367c70a8db840c79e6b640f1d9d74f2ac tools/checks/web_service_ui_test.mjs d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py 2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py diff --git a/dev.sh b/dev.sh index d5cb63b..be20a60 100755 --- a/dev.sh +++ b/dev.sh @@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi case "$mode" in help|-h|--help) cat <<'HELP' -RustProxy 0.27.3 +RustProxy 0.28.0 ./dev.sh Build and start proxy + local panel ./dev.sh run [serve flags] Same, with extra server options ./dev.sh admin setup-token Generate/print a one-time setup capability before first login @@ -139,7 +139,7 @@ if [[ "$mode" == "test" ]]; then printf 'Checking all Rust targets before tests...\n' >&2 cargo check --locked --all-targets fi -printf 'Building RustProxy 0.27.3...\n' >&2 +printf 'Building RustProxy 0.28.0...\n' >&2 cargo build --locked BIN="$CARGO_TARGET_DIR/debug/rust-proxy" case "$mode" in diff --git a/docker-agent/model.go b/docker-agent/model.go index 6af35e4..d9380a9 100644 --- a/docker-agent/model.go +++ b/docker-agent/model.go @@ -1,6 +1,6 @@ package main -const version = "0.27.3" +const version = "0.28.0" type config struct { Listen string diff --git a/docs/API.md b/docs/API.md index 983d397..0459899 100644 --- a/docs/API.md +++ b/docs/API.md @@ -96,7 +96,7 @@ snapshot publication finish before a successful response. Disconnecting the call after the detached writer starts does not cancel an accepted write. The document limit is 256 KiB, with an additional 1024-byte request-envelope allowance. For ordinary Settings saves prefer `PUT /api/config/{section}`. Supported sections are -`listeners`, `response`, `cache`, `logging`, `forwarding`, `headers`, `prometheus`, +`listeners`, `response`, `cache`, `logging`, `forwarding`, `headers`, `prometheus`, `geoip`, `administration`, `tls`, `vhosts`, `web-services`, `redirects`, `not_found_hosts`, `streams`, `acl-profiles` and `host-policies`. Each section write still uses revision CAS, full validation and atomic persistence. Only a change to the derived cache-compatibility identity purges hot/cold cache; @@ -105,7 +105,7 @@ unrelated section saves do not. ## Configuration fields The canonical document uses `vhosts`, `web_services`, `redirects`, `listeners`, `administration`, `tls`, `logging`, -`prometheus`, `cache`, `compression`, `server_header`, `headers`, `host_policies` and other defaulted +`prometheus`, `geoip_updates`, `cache`, `compression`, `server_header`, `headers`, `host_policies` and other defaulted settings. Unknown fields are rejected. `backends`/`routes` at the top level are input-only legacy migration fields, omitted from saved canonical documents. diff --git a/docs/API_ACCESS.md b/docs/API_ACCESS.md index 07ef34f..f180214 100644 --- a/docs/API_ACCESS.md +++ b/docs/API_ACCESS.md @@ -1,4 +1,4 @@ -# Administration API access - 0.27.3 +# Administration API access - 0.28.0 ## Sign-in boundary diff --git a/docs/CACHE.md b/docs/CACHE.md index e5b68ce..5456c49 100644 --- a/docs/CACHE.md +++ b/docs/CACHE.md @@ -55,6 +55,7 @@ behavior exactly. | expose_hits | false | Emit per-object `X-Cache-Hits` | | expose_cache_status | false | Emit standardized `Cache-Status` (RFC 9211) | | expose_resident_age | false | Emit `X-Cache-Resident-Age` on reused stored responses | +| via_header | `RustProxy` | Append this `Via` response value; `null` disables it | | collapse_enabled | true | Collapse concurrent foreground fills for the same primary cache key | | collapse_wait_timeout_ms | 1500 | 10..60000 ms follower wait before fallback | | collapse_timeout_fallback | origin | `origin` or `stale_if_available` after follower timeout | @@ -148,7 +149,7 @@ Cache invalidation remains explicit: - BAN records a bounded TTL rule without synchronously scanning the entire store; objects created before a matching active BAN are rejected during lookup/insertion and are removed later by bounded GC, - new fills created after a BAN rule are allowed, so BAN invalidates the old representation set rather than permanently blocking a URL, - regex rules and prefix `/` are treated as broad operations and require explicit confirmation, -- only cache-compatibility changes (cache policy, configured header/origin semantics or vhosts) advance the normal cache generation/revision; unrelated logging/TLS/admin/metrics/response-presentation saves retain objects. +- only cache-compatibility changes advance the normal cache generation/revision; unrelated logging/TLS/admin/metrics/response-presentation saves retain objects, and cache request-bypass edits plus response-only `X-Cache`/`Cache-Status`/resident-age/`Via` diagnostics also apply without purging stored objects. There is no public unauthenticated PURGE/BAN endpoint. If an application changes a GET resource after POST/PUT/PATCH/DELETE, use origin freshness/validators or the authenticated @@ -272,7 +273,7 @@ bytes. Purge-all and purge-vhost remain the full-clear mechanisms; selective inv is deliberately separate. `X-Cache`, `X-Cache-Hits`, `Cache-Status`, `X-Cache-Reason` and -`X-Cache-Resident-Age` are optional diagnostics. `Cache-Status` is the standardized +`X-Cache-Resident-Age` are optional diagnostics. `Via` is a separate configurable response header, enabled by default with value `RustProxy`; RustProxy appends it instead of deleting an upstream `Via` chain. `Cache-Status` is the standardized RFC 9211 field name; RustProxy does not rename it to the legacy/custom `X-Cache-Status`. HTTP/2 and HTTP/3 serialize field names in lowercase, so `cache-status:` on the wire is normal. When the cached representation was produced by the transform pipeline, diff --git a/docs/CONTAINER.md b/docs/CONTAINER.md index 44df0e0..5f72ae1 100644 --- a/docs/CONTAINER.md +++ b/docs/CONTAINER.md @@ -9,7 +9,7 @@ each Docker host, so the proxy container never needs Docker-socket access. See Build locally with a normal OCI builder: ```bash -docker build -t rustproxy:0.27.3 . +docker build -t rustproxy:0.28.0 . ``` Persist `/var/lib/rustproxy`; it contains the SQLite database, certificates and the @@ -22,7 +22,7 @@ docker volume create rustproxy-data docker run --rm -d --name rustproxy \ -p 8080:8080 \ -v rustproxy-data:/var/lib/rustproxy \ - rustproxy:0.27.3 + rustproxy:0.28.0 docker exec rustproxy rust-proxy admin setup-token ``` diff --git a/docs/FILE_LOGS.md b/docs/FILE_LOGS.md index 24e5baf..4201a56 100644 --- a/docs/FILE_LOGS.md +++ b/docs/FILE_LOGS.md @@ -1,6 +1,6 @@ # Stored log explorer -RustProxy 0.27.3 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent. +RustProxy 0.28.0 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent. ## Separation from Live Logs diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 2ab354b..4d3915d 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -1,4 +1,4 @@ -# Installing and updating RustProxy 0.27.3 +# Installing and updating RustProxy 0.28.0 The scripts prepare a Linux/systemd installation. This source archive is **not a verified production build**: Rust/Cargo, real CA operations, live systemd startup @@ -67,12 +67,15 @@ precompiled binary in the archive. Set `--binary` explicitly when using a custom ## First installation -The normal first install also downloads `user-country-ipv4.csv` and -`user-country-ipv6.csv` from the `sapics/ip-location-db` latest release over HTTPS and -verifies each file against the publisher SHA-256 release before writing it privately to -`DATA_DIR/geoip`. This enables country access/rate rules. Use `--skip-geoip` only when -you intentionally do not want the database; allow-only country policies then fail -closed for unresolved clients. Updates preserve the installed database. +`install.sh` also downloads the direct `user-country-ipv4.csv` and `user-country-ipv6.csv` +assets from the `sapics/ip-location-db` latest release over HTTPS, validates their CSV +structure and writes them privately to `DATA_DIR/geoip`. Package/plugin installs may omit +that packaging-time download: after all listeners bind successfully, RustProxy starts an +enabled-by-default GeoIP worker, performs an immediate background refresh and then checks +every 24 hours by default. Country allow-only policies continue to fail closed until a valid +database is available. Settings can use separate IPv4/IPv6 CSV files or one native MaxMind +MMDB file; direct source URLs, optional per-file SHA-256 URLs, maximum file size and interval +are configurable under **Settings -> GeoIP updates**. ```bash @@ -225,11 +228,14 @@ bash update.sh --plan --binary target/release/rust-proxy sudo bash update.sh --binary target/release/rust-proxy ``` -Before stopping the service, the updater fetches the current publisher SHA-256 -files for the `user-country` GeoIP IPv4/IPv6 assets. Matching local files are left -untouched; stale or missing assets are staged, verified and published only after -the private-data backup exists. Use `--skip-geoip` only when intentionally accepting -a stale/missing country database (for example during an upstream outage). +Before stopping the service, the updater downloads the current direct `user-country` +IPv4/IPv6 CSV assets, validates their structure and compares their local SHA-256 with the +installed files. Unchanged files are left untouched; changed or missing assets are staged +and published only after the private-data backup exists. This intentionally avoids requiring +a separately moving publisher-checksum release. The manual updater and the in-process GeoIP +worker use the same `.geoip-update.lock`, so they cannot publish concurrently. Use +`--skip-geoip` only when intentionally accepting a stale/missing country database (for +example during an upstream outage). Use the original `--prefix` for a non-default installation. Other startup/account/ data settings are read from root-owned `install.json`; update does not silently diff --git a/docs/README.md b/docs/README.md index afa5d89..e5561fc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,7 @@ This is the canonical index for the current project documentation. The authentic - [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards. - [0.25.0 specification audit](V0.25.0_AUDIT.md) - Web Server foundation point-by-point implementation status. - [0.25.0 pre-production security audit](V0.25.0_SECURITY_PREPROD_AUDIT.md) - threat-oriented source/configuration review, fixes made in-session and mandatory runtime security gates. +- [0.28.0 implementation note](V0.28.0_IMPLEMENTATION.md) - manual/hot-reloaded GeoIP, listener status UX, cache Via/narrow invalidation and header-rule layout. - [0.27.3 implementation note](V0.27.3_IMPLEMENTATION.md) - HTTP/3 HEAD cache parity, COLD/rate-limit memory reduction, backup scope and package-path update. - [0.27.2 implementation note](V0.27.2_IMPLEMENTATION.md) - managed Certbot relocation/renewal repair. - [0.27.1 implementation note](V0.27.1_IMPLEMENTATION.md) - first-admin command, managed/fallback certificate runtime and account-scoped route-list sorting. @@ -71,12 +72,12 @@ This is the canonical index for the current project documentation. The authentic ## Historical or version-labelled context -Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md); [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability. +Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md); [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md) is retained as the prior HTTP/3/cache-maintenance milestone, [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability. 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. The pre-cleanup developer-level changelog is retained as [CHANGELOG_TECHNICAL_ARCHIVE](CHANGELOG_TECHNICAL_ARCHIVE.md) when low-level historical detail is needed. The root `CHANGELOG.md` remains the operator-facing release record. -Completed roadmap history is retained separately as [ROADMAP_HISTORY](ROADMAP_HISTORY.md); the root `ROADMAP.md` contains only the active 0.25 milestone, release gate and future direction. +Completed roadmap history is retained separately as [ROADMAP_HISTORY](ROADMAP_HISTORY.md); the root `ROADMAP.md` contains the current release scope and future direction. ## Maintenance rules diff --git a/docs/SETTINGS.md b/docs/SETTINGS.md index f7c00a7..6ed1043 100644 --- a/docs/SETTINGS.md +++ b/docs/SETTINGS.md @@ -52,6 +52,31 @@ The historical `/configuration` browser route is retained only as a compatibilit opens that Settings block; it is no longer a primary-navigation item. Prefer the structured section editors for routine changes so unrelated sections are not republished. +## GeoIP updates + +**Settings -> GeoIP updates** controls the background country-database refresh and is enabled by default. The worker starts only after all server sockets have bound successfully, attempts the first refresh immediately, and then refreshes every 24 hours by default. Sources are direct HTTPS file URLs rather than coupled release/checksum base URLs. Publisher SHA-256 verification is optional per file; every downloaded payload is still size-bounded, streamed to a private staging file and parsed as the selected database format before publication. + +Two formats are supported. **CSV ranges** uses separate IPv4 and IPv6 files and defaults to the direct `sapics/ip-location-db` `user-country-ipv4.csv` and `user-country-ipv6.csv` assets. **MaxMind DB (MMDB)** uses one `country.mmdb` file for both address families and defaults to the combined sapics `user-country.mmdb` asset. Standard MaxMind country databases are read from `country.iso_code`, then `registered_country.iso_code`; custom country MMDBs may expose top-level `country_code` or a root two-letter country string. To manage a private MMDB outside the updater, place it at `/geoip/country.mmdb`, select MMDB, disable automatic updates and leave the MMDB source URL empty; **Download now** then validates/reloads the local file instead of fetching it. + +When a checksum URL is empty, RustProxy deliberately skips publisher-checksum comparison. It still computes a local SHA-256 for the validated download and compares it with the installed file, so unchanged content is not republished or hot-reloaded. This avoids coupling a moving `latest` data URL to a separately moving checksum release. If a checksum URL is supplied, a mismatch rejects the staged file. The configured maximum is 1 MiB..256 MiB and the interval is 60 seconds..31 days. Failures are warnings only; proxy traffic continues and the worker retries. Manual UI refresh, the background worker and managed upgrades share `.geoip-update.lock`. After publication the `ArcSwap` database is replaced atomically: requests already holding a snapshot may finish on it, while subsequent country ACL/rate-limit/logging lookups and status/metrics reads use the new database without a restart. + +```json +{ + "geoip_updates": { + "enabled": true, + "format": "csv_pair", + "ipv4_url": "https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv4.csv", + "ipv6_url": "https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv6.csv", + "mmdb_url": "https://github.com/sapics/ip-location-db/releases/download/latest/user-country.mmdb", + "ipv4_checksum_url": null, + "ipv6_checksum_url": null, + "mmdb_checksum_url": null, + "max_bytes": 268435456, + "interval_seconds": 86400 + } +} +``` + ## Administration HTTPS `administration` is separate from public proxy listeners. It defaults to the startup diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md index 2065dc2..6edfb3b 100644 --- a/docs/TEST_REPORT.md +++ b/docs/TEST_REPORT.md @@ -1,38 +1,40 @@ -# Verification report - RustProxy 0.27.3 +# Verification report - RustProxy 0.28.0 ## Scope -RustProxy 0.27.3 fixes HTTP/3 `HEAD` cache eligibility, reduces persistent COLD-cache and rate-limit/IP memory retention, excludes rebuildable managed Python environments from backups, and updates the documented installation path. SQLite schema 8, the saved configuration format and COLD format 3 remain unchanged. +RustProxy 0.28.0 adds authenticated on-demand GeoIP refresh through the same staging/lock path as the automatic worker, replaces coupled release/checksum bases with direct per-database sources, makes publisher SHA-256 URLs optional, supports either separate IPv4/IPv6 CSV ranges or one native MaxMind DB (`.mmdb`), and verifies hot replacement of the shared `ArcSwap` database. It also improves active-listener presentation with running `H1`/`H2`/`H2C`/`H3` badges, reports the completed ACME Python/Certbot probe in the INFO log instead of printing speculative startup state, adds configurable cache `Via` metadata, narrows cache compatibility invalidation for bypass/response-only diagnostics, and fixes the vhost Security header-rule Delete/Operation layout. It retains the 0.27.3 restore/listener/WebSocket/update-check/automatic-GeoIP fixes. -The H3 repair is intentionally narrow: cache policy already accepts both `GET` and `HEAD`; the forwarding path now performs the existing bounded empty-body probe for both methods. This prevents an initially unpolled HTTP/3 receive body from being mistaken for request content. +SQLite schema 8 and COLD cache format 3 remain unchanged. Older saved configurations remain compatible: legacy GeoIP `release_url`/`checksum_url` values are accepted and migrated into the direct-source model, while the old moving default checksum source is deliberately treated as unpinned and therefore optional. `geoip_updates` and the new `cache.via_header` are serde-defaulted when absent. The cache `Via` value defaults to `RustProxy`; `null` disables it. -The COLD index no longer stores full response headers or origin validator values for every disk object. These are loaded and validated from the existing metadata JSON only on an actual COLD read. The in-memory SHA-256 is 32 bytes and the map-owned key is not duplicated inside `ColdEntry`. Rate limiting now groups IP buckets below one vhost string, sweeps expired fixed-window buckets during requests and periodic runtime maintenance, and shrinks sparse hash tables before allocator trimming. +The GeoIP request path reads `state.geoip.load()` when a frontend/location policy, country rate limit or country log capture needs a country. Manual and automatic updates both publish through the same `ArcSwap`. The regression test keeps an old `Arc`, publishes a replacement database and verifies that the old snapshot remains valid while subsequent slot loads return the new country mapping. CSV mode validates both IP families and ordering; MMDB mode validates MaxMind DB v2 structure and supports standard `country.iso_code`, `registered_country.iso_code`, sapics-style top-level `country_code`, and a two-letter root string used by simple custom databases. -Compression was reviewed but its wire behavior was not changed: it remains streaming through `tower_http`, uses `CompressionLevel::Fastest`, and does not aggregate a complete response body before encoding. Its primary RAM cost is therefore per concurrently active compressed response/codec state, not long-lived retained objects. +When publisher checksum URLs are empty, downloads remain size-bounded and structurally validated. RustProxy computes the staged file's local SHA-256 and compares it with the installed file, so unchanged content is not republished. If an optional checksum URL is configured, the publisher digest is additionally enforced. A disabled MMDB downloader may use a locally managed `/geoip/country.mmdb`; the manual refresh action validates and hot-reloads that local file. -Application-coupled Python is now separated under `scripts/runtime`, `scripts/build`, `scripts/deploy` and `scripts/migration`; QA/dev/audit/legacy helpers live under `tools/`, while OPNsense/RouterOS helpers live under `integrations/`. Release checks validate the new paths and archive contents. +Cache compatibility hashing intentionally ignores `bypass_query`, `bypass_prefixes` and response-only cache diagnostic/Via fields. Those settings affect admission/wire behavior on subsequent requests but do not mutate a stored representation. Representation/origin-affecting cache, transform, header, forwarding and vhost changes remain in the compatibility identity. ## Verification status in this workspace -Rust/Cargo is not installed in this workspace. Rust-independent checks are run here; compiler/type/runtime gates and real H2/H3 protocol execution remain open until this source is checked on a Rust-enabled host. - -The final status table is refreshed from the release checks performed for this archive. Static/source contracts do not substitute for Rust parsing, type checking, unit tests or real protocol runtime tests. +Rust/Cargo is not installed in this workspace. Rust-independent release checks can run here; compiler/type/runtime gates and real H2/H3 protocol execution require a Rust-enabled host. ## Release checks | Check | Result | | --- | --- | | Rust/source module contracts | PASS | -| Bounded-memory/cache/rate-limit/compression contracts | PASS | +| GeoIP direct-source/optional-checksum/MMDB/manual endpoint/UI/hot-swap source contracts | PASS | +| Cache Via/narrow invalidation source contracts | PASS | +| Listener protocol-badge/header-editor UI source contracts | PASS | +| ACME startup reporting contract (no speculative banner state; final INFO event) | PASS | | Documentation and language-pack checks | PASS | | Web Server, security and dependency source contracts | PASS | | `tools/checks/check_source.py` | PASS (compiler/runtime explicitly not verified) | | `tools/checks/release_smoke.py` | PASS | -| Python syntax (`py_compile`) for changed release scripts | PASS | +| Python syntax (`py_compile`) and direct-CSV installer validation checks | PASS | | Browser Web Service UI regression (`node tools/checks/web_service_ui_test.mjs`) | PASS | | Docker Agent (`go test ./...`) | PASS | -| `./dev.sh test` static/pre-compiler stages | PASS | -| Rust `cargo fmt/check/test`, linking and Rust unit tests | NOT VERIFIED - Cargo/Rust is not installed in this workspace | -| Live rebuilt HTTP/2 + HTTP/3 `HEAD` cache test | NOT VERIFIED - requires a compiled 0.27.3 binary and QUIC-capable runtime | +| `./dev.sh test` static/pre-compiler stages | PASS; command then exits 127 at the Rust/Cargo gate | +| Rust `cargo fmt/check/test`, linking and Rust unit tests (including synthetic standard/sapics/custom-MMDB lookup fixtures, GeoIP hot-swap, cache Via and cache-revision tests) | NOT VERIFIED - Cargo/Rust is not installed in this workspace | +| Live manual/automatic GeoIP direct-source download and real-world MMDB lookup | NOT VERIFIED - requires network access and a compiled 0.28.0 binary | +| Live HTTP/2 + HTTP/3 protocol tests | NOT VERIFIED - requires a compiled 0.28.0 binary and QUIC-capable runtime | -`./dev.sh test` exits with status 127 only when it reaches the Rust toolchain gate; all earlier release/static checks complete successfully. +`./dev.sh test` is expected to exit with status 127 only when it reaches the Rust toolchain gate; all earlier release/static checks must complete successfully for this report to remain valid. diff --git a/docs/V0.27.3_IMPLEMENTATION.md b/docs/V0.27.3_IMPLEMENTATION.md index 22aeede..0b305cc 100644 --- a/docs/V0.27.3_IMPLEMENTATION.md +++ b/docs/V0.27.3_IMPLEMENTATION.md @@ -2,7 +2,7 @@ ## Scope -0.27.3 aligns HTTP/3 `HEAD` cache handling with HTTP/2, reduces long-lived cache/rate-limit memory, excludes the rebuildable managed Python runtime from backups, and promotes package repositories as the normal installation path. SQLite schema 8, saved configuration compatibility and the COLD cache on-disk format remain unchanged. +0.27.3 aligns HTTP/3 `HEAD` cache handling with HTTP/2, reduces long-lived cache/rate-limit memory, excludes the rebuildable managed Python runtime from backups, promotes package repositories as the normal installation path, and includes maintenance fixes for restore ownership, startup listener overrides, strict WebSocket 101 framing, resilient update checks, scaled sidebar layout and automatic GeoIP maintenance. SQLite schema 8 and the COLD cache on-disk format remain unchanged; existing saved configurations remain compatible because the new `geoip_updates` object is defaulted when absent. ## HTTP/3 HEAD cache parity @@ -51,6 +51,22 @@ The managed ACME runtime now distinguishes private-environment creation, pip pre On distributions enforcing PEP 668, the application-local `virtualenv` bootstrap first uses normal `pip --target`; if the distribution rejects that operation, RustProxy retries with `--break-system-packages` while still writing exclusively into the private bootstrap directory under the RustProxy data tree. This does not install or modify packages in the distribution Python environment. +## Restore ownership and production data directory + +The production CLI default is `/var/lib/rustproxy`; `dev.sh` explicitly sets `RUST_PROXY_DATA_DIR` to repository-local `./data`. During restore, RustProxy records the UID/GID of an existing target data directory and recursively applies that ownership to the validated staging tree before the atomic rename. This prevents a root-run restore from converting service data to `root:root`; symlinks are ownership-adjusted with `lchown` and are not followed. + +## GeoIP background maintenance + +`Config.geoip_updates` defaults to enabled with the sapics latest/checksum release URLs, 256 MiB maximum file size and a 24-hour interval. The worker starts after successful listener binding, attempts immediately, compares publisher SHA-256 files with local files and downloads only stale assets. Database payloads are streamed to private staging files with a transfer limit and incremental SHA-256, the staged pair is parsed before publication, and the in-memory GeoIP database is atomically replaced without a proxy restart. Failures are fail-soft and retry later. The worker and managed manual updater share `.geoip-update.lock`; that runtime lock is excluded from backups. + +## Listener, WebSocket and control-plane startup fixes + +Explicit CLI/systemd HTTP/HTTPS binds now replace the corresponding canonical saved `http`/`https` bind rather than appending a second listener that can leave `:80`/`:443` active. Strict WebSocket response validation accepts `101` with no `Content-Length` or exactly `Content-Length: 0`, while rejecting positive lengths and any `Transfer-Encoding`. The release-version checker starts only after all sockets bind and logs polling failures as warnings, keeping network/DNS availability outside the proxy startup success path. + +## Scaled administration sidebar + +The desktop shell/sidebar height compensates for CSS `zoom` via `100vh / --ui-scale`. At scales below 100%, the flex sidebar therefore still spans the visual viewport and its bottom runtime information remains at the bottom edge. Mobile rules continue to use automatic height. + ## Source-tool separation 0.27.3 also separates application-coupled scripts from auxiliary tooling without changing proxy behavior. `scripts/runtime`, `scripts/build`, `scripts/deploy` and `scripts/migration` contain code directly tied to application/build/deployment workflows. Compiler-independent checks, mocks, audit probes and the retired Python backup helper moved to `tools/`. OPNsense and RouterOS helpers moved to `integrations/`. diff --git a/docs/V0.28.0_IMPLEMENTATION.md b/docs/V0.28.0_IMPLEMENTATION.md new file mode 100644 index 0000000..941dc21 --- /dev/null +++ b/docs/V0.28.0_IMPLEMENTATION.md @@ -0,0 +1,39 @@ +# RustProxy 0.28.0 implementation note + +## Scope + +0.28.0 extends the 0.27.3 maintenance release with operator-controlled GeoIP refresh, clearer listener status, cache `Via` metadata, narrower cache invalidation and a vhost Security header-rule layout repair. SQLite schema 8 and COLD cache format 3 are unchanged. + +## GeoIP manual refresh and hot reload + +`POST /api/geoip/update` is an authenticated/CSRF-protected control-plane action used by **Settings -> GeoIP updates -> Download now**. It invokes the same updater function as the background worker. The updater consumes direct source URLs, with optional publisher SHA-256 URLs per file, streams payloads into a private staging directory under the configured size limit, validates the selected database format, publishes only content whose local SHA-256 changed, and replaces the in-memory database only after validation succeeds. The UI action works when scheduled updates are disabled. + +CSV mode uses separate IPv4 and IPv6 range files. MMDB mode uses one native MaxMind DB for both families and recognizes standard `country.iso_code` / `registered_country.iso_code` records plus a top-level `country_code` compatibility record or a root two-letter country string. The combined sapics `user-country.mmdb` is the default MMDB source. When the publisher checksum URL is empty, no remote checksum is required; structural validation and local content hashing remain mandatory. An updater-disabled MMDB configuration may leave the URL empty and manage `/geoip/country.mmdb` locally; the manual action then validates/reloads that local file. + +The automatic worker, manual UI action and managed upgrade path serialize through `.geoip-update.lock`. Publishing uses the application's existing `ArcSwap`: a request that already owns the prior `Arc` remains memory-safe, while subsequent lookups load the new database. The request path uses this slot for country access rules, country rate limiting and optional country logging; administration and Prometheus status read the same slot. No process restart is needed. + +## Listener status UX + +Settings no longer compresses active endpoints into one `HTTP ... | HTTPS ...` sentence. Each active endpoint is rendered as a separate compact card with protocol and address plus badges for the protocols that are actually enabled on the running listener: `H1` is always present, TLS listeners add `H2` when ALPN HTTP/2 is enabled and `H3/UDP` when QUIC is enabled, while cleartext listeners add `H2C` when prior-knowledge HTTP/2 is enabled. Saved-listener restart semantics are unchanged. + +## Cache Via and invalidation + +`cache.via_header` is an optional string. Its default is `RustProxy`, producing `Via: RustProxy`; `null` disables RustProxy's Via value. Existing upstream Via fields are preserved and RustProxy appends its value. Validation bounds the configured field value to 256 valid HTTP-header bytes. + +Cache compatibility hashing now normalizes request-only bypass fields and response-only cache diagnostics (`X-Cache`, hit counters, `Cache-Status`, resident age and `Via`). These changes apply on subsequent requests without discarding HOT/COLD objects. Representation/origin-affecting configuration remains in the compatibility identity and continues to invalidate incompatible objects. + +## Vhost Security header rules + +The request/response header-rule editor now uses the same structured header editor layout as location rules. Delete is contained in the rule heading and aligned to the right, while Operation, Name and Value remain in the field grid below it. + +## Compatibility + +- SQLite schema: **8**, unchanged. +- COLD cache format: **3**, unchanged. +- Existing saved configurations remain valid because `Settings` uses serde defaults; an absent `cache.via_header` becomes `RustProxy`. +- Legacy 0.27.3 release/checksum base URLs are accepted during configuration migration. The old default checksum base migrates to optional/no publisher checksum; custom legacy bases are translated into per-file URLs. + +### Startup ACME runtime reporting + +The human-readable startup banner no longer prints the initial `checking/preparing` ACME Python state, because that value can race the asynchronous runtime probe. When the probe finishes, RustProxy emits an `INFO` event named `ACME runtime check complete` containing the final status, mode, Python/pip/Certbot availability and managed-environment path. + diff --git a/grafana/README.md b/grafana/README.md index a9b2b7f..61735b3 100644 --- a/grafana/README.md +++ b/grafana/README.md @@ -19,7 +19,7 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw are intentionally absent from Prometheus labels. Import the JSON in Grafana and select a Prometheus datasource when prompted. The -dashboard expects RustProxy 0.27.3 metric names and a scrape interval around 15s. +dashboard expects RustProxy 0.28.0 metric names and a scrape interval around 15s. Collectors can be disabled in RustProxy settings; panels for disabled collectors will simply show no series. diff --git a/grafana/rustproxy-overview.json b/grafana/rustproxy-overview.json index e4a1d38..cc3cb44 100644 --- a/grafana/rustproxy-overview.json +++ b/grafana/rustproxy-overview.json @@ -4529,9 +4529,9 @@ }, "timepicker": {}, "timezone": "browser", - "title": "RustProxy 0.27.3 - Operations", + "title": "RustProxy 0.28.0 - Operations", "uid": "rustproxy-0170", "version": 5, "weekStart": "", - "description": "RustProxy 0.27.3 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic." + "description": "RustProxy 0.28.0 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic." } diff --git a/langs/en_US.json b/langs/en_US.json index 01f9307..ccb3bb8 100644 --- a/langs/en_US.json +++ b/langs/en_US.json @@ -2152,6 +2152,36 @@ "acme.runtime_venv_pip_failed": "The private Python environment was created, but pip/ensurepip could not be started inside it. Check the Python venv/ensurepip installation. No system Certbot is available.", "acme.runtime_certbot_install_failed": "The private Python environment works, but pip could not install the Certbot packages. Check PyPI/proxy/DNS/TLS access. No system Certbot is available.", "acme.runtime_certbot_validation_failed": "ACME packages were installed, but Certbot or one of the required plugins failed validation. Remove the acme-python directory and restart RustProxy, or check compatibility with the local Python.", - "acme.runtime_data_dir_missing": "The RustProxy data directory is unavailable, so a private Python environment cannot be created. PEM import and existing certificate files still work." + "acme.runtime_data_dir_missing": "The RustProxy data directory is unavailable, so a private Python environment cannot be created. PEM import and existing certificate files still work.", + "geoipUpdatesTitle": "GeoIP updates", + "geoipUpdatesHelp": "Download direct GeoIP country sources in the background and hot-reload them without restarting proxy traffic. Publisher SHA-256 verification is optional per file.", + "geoipUpdatesEnable": "Enable automatic GeoIP updates", + "geoipReleaseUrl": "Release files base URL", + "geoipChecksumUrl": "Checksum files base URL", + "geoipMaxFileMiB": "Maximum file size (MiB)", + "geoipIntervalHours": "Update interval (hours)", + "geoipStatusLoaded": "GeoIP database loaded", + "geoipStatusMissing": "GeoIP database is not loaded yet. The background worker will retry automatically.", + "geoipStatusDisabled": "Automatic GeoIP updates are disabled.", + "cacheViaEnable": "Add Via response header", + "cacheViaValue": "Via header value", + "cacheViaHelp": "Defaults to Via: RustProxy. Bypass and response-only diagnostic header changes apply immediately without purging cached objects.", + "geoipUpdateNow": "Download now", + "geoipUpdateNowHelp": "Uses the saved direct source URLs and optional checksum URLs. Manual download works even when automatic updates are disabled and hot-reloads the validated database.", + "geoipUpdateSaveFirst": "Save GeoIP settings before running a manual download.", + "geoipUpdateWorking": "Downloading and validating GeoIP data…", + "geoipUpdateUpdated": "GeoIP database downloaded and hot-reloaded.", + "geoipUpdateCurrent": "GeoIP database is already current and loaded.", + "geoipUpdateBusy": "Another GeoIP update is already running; try again shortly.", + "geoipFormat": "Database format", + "geoipFormatCsvPair": "CSV ranges (separate IPv4 + IPv6)", + "geoipFormatMmdb": "MaxMind DB (.mmdb, IPv4 + IPv6)", + "geoipIpv4Url": "IPv4 CSV direct URL", + "geoipIpv4ChecksumUrl": "IPv4 SHA-256 URL (optional)", + "geoipIpv6Url": "IPv6 CSV direct URL", + "geoipIpv6ChecksumUrl": "IPv6 SHA-256 URL (optional)", + "geoipMmdbUrl": "MaxMind DB direct URL", + "geoipMmdbChecksumUrl": "MaxMind DB SHA-256 URL (optional)", + "geoipSourceHelp": "CSV mode uses separate IPv4 and IPv6 range files. MMDB mode uses one native MaxMind DB file and supports standard country.iso_code, registered_country.iso_code, or country_code records. Leave a checksum URL empty to skip publisher-checksum verification; every download is still size-bounded and format-validated before atomic publish. For a local custom MMDB, place it at /geoip/country.mmdb, select MMDB and disable automatic updates." } -} \ No newline at end of file +} diff --git a/langs/pl_pl.json b/langs/pl_pl.json index 1168a44..f0c742f 100644 --- a/langs/pl_pl.json +++ b/langs/pl_pl.json @@ -2152,6 +2152,36 @@ "acme.runtime_venv_pip_failed": "Prywatne środowisko Python zostało utworzone, ale nie udało się uruchomić w nim pip/ensurepip. Sprawdź instalację Python venv/ensurepip. Systemowy Certbot nie jest dostępny.", "acme.runtime_certbot_install_failed": "Prywatne środowisko Python działa, ale pip nie może zainstalować pakietów Certbota. Sprawdź dostęp do PyPI/proxy/DNS/TLS. Systemowy Certbot nie jest dostępny.", "acme.runtime_certbot_validation_failed": "Pakiety ACME zostały zainstalowane, ale Certbot lub jedna z wymaganych wtyczek nie przechodzi walidacji. Usuń katalog acme-python i uruchom RustProxy ponownie albo sprawdź zgodność lokalnego Pythona.", - "acme.runtime_data_dir_missing": "Katalog danych RustProxy jest niedostępny, więc nie można utworzyć prywatnego środowiska Python. Import PEM i istniejące pliki certyfikatów nadal działają." + "acme.runtime_data_dir_missing": "Katalog danych RustProxy jest niedostępny, więc nie można utworzyć prywatnego środowiska Python. Import PEM i istniejące pliki certyfikatów nadal działają.", + "geoipUpdatesTitle": "Aktualizacje GeoIP", + "geoipUpdatesHelp": "Pobiera w tle bezpośrednie źródła baz krajów GeoIP i przeładowuje je bez restartu ruchu proxy. Weryfikacja SHA-256 wydawcy jest opcjonalna osobno dla każdego pliku.", + "geoipUpdatesEnable": "Włącz automatyczne aktualizacje GeoIP", + "geoipReleaseUrl": "Bazowy URL plików wydań", + "geoipChecksumUrl": "Bazowy URL plików sum kontrolnych", + "geoipMaxFileMiB": "Maksymalny rozmiar pliku (MiB)", + "geoipIntervalHours": "Interwał aktualizacji (godziny)", + "geoipStatusLoaded": "Baza GeoIP załadowana", + "geoipStatusMissing": "Baza GeoIP nie jest jeszcze załadowana. Worker w tle automatycznie ponowi próbę.", + "geoipStatusDisabled": "Automatyczne aktualizacje GeoIP są wyłączone.", + "cacheViaEnable": "Dodawaj nagłówek odpowiedzi Via", + "cacheViaValue": "Wartość nagłówka Via", + "cacheViaHelp": "Domyślnie Via: RustProxy. Zmiany bypassów i nagłówków diagnostycznych odpowiedzi działają od razu bez czyszczenia zapisanych obiektów cache.", + "geoipUpdateNow": "Pobierz teraz", + "geoipUpdateNowHelp": "Używa zapisanych bezpośrednich URL-i źródeł i opcjonalnych URL-i sum kontrolnych. Ręczne pobranie działa także przy wyłączonych aktualizacjach automatycznych i przeładowuje zweryfikowaną bazę bez restartu.", + "geoipUpdateSaveFirst": "Najpierw zapisz ustawienia GeoIP, a potem uruchom ręczne pobranie.", + "geoipUpdateWorking": "Pobieranie i walidacja danych GeoIP…", + "geoipUpdateUpdated": "Baza GeoIP została pobrana i przeładowana bez restartu.", + "geoipUpdateCurrent": "Baza GeoIP jest już aktualna i załadowana.", + "geoipUpdateBusy": "Inna aktualizacja GeoIP już trwa; spróbuj ponownie za chwilę.", + "geoipFormat": "Format bazy", + "geoipFormatCsvPair": "Zakresy CSV (osobno IPv4 + IPv6)", + "geoipFormatMmdb": "MaxMind DB (.mmdb, IPv4 + IPv6)", + "geoipIpv4Url": "Bezpośredni URL CSV IPv4", + "geoipIpv4ChecksumUrl": "URL SHA-256 IPv4 (opcjonalny)", + "geoipIpv6Url": "Bezpośredni URL CSV IPv6", + "geoipIpv6ChecksumUrl": "URL SHA-256 IPv6 (opcjonalny)", + "geoipMmdbUrl": "Bezpośredni URL MaxMind DB", + "geoipMmdbChecksumUrl": "URL SHA-256 MaxMind DB (opcjonalny)", + "geoipSourceHelp": "Tryb CSV używa osobnych plików zakresów IPv4 i IPv6. Tryb MMDB używa jednego natywnego pliku MaxMind DB i obsługuje standardowe pola country.iso_code, registered_country.iso_code albo country_code. Puste pole URL sumy wyłącza weryfikację sumy wydawcy; każdy plik nadal ma limit rozmiaru i jest walidowany przed atomową publikacją. Dla lokalnej własnej bazy MMDB umieść plik w /geoip/country.mmdb, wybierz MMDB i wyłącz automatyczne aktualizacje." } -} \ No newline at end of file +} diff --git a/make_zip.py b/make_zip.py index 1b59fa6..7dd50e6 100755 --- a/make_zip.py +++ b/make_zip.py @@ -86,7 +86,7 @@ def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[ def main() -> None: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.27.3.zip') + parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.28.0.zip') output = parser.parse_args().output.resolve() paths = files() checksum = ROOT / 'SHA256SUMS' diff --git a/scripts/build/swagger_assets.py b/scripts/build/swagger_assets.py index d58bbf9..033f31c 100755 --- a/scripts/build/swagger_assets.py +++ b/scripts/build/swagger_assets.py @@ -85,7 +85,7 @@ def download(destination: Path) -> None: """TLS verification uses Python's default trust store; never executes a shell.""" started = time.monotonic() opener = build_opener(HttpsRedirects()) - request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.27.3"}) + request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.28.0"}) with opener.open(request, timeout=30) as response, destination.open("wb") as output: length = response.headers.get("Content-Length") if length and int(length) > MAX_ARCHIVE: diff --git a/scripts/deploy/deploy_core.py b/scripts/deploy/deploy_core.py index 77964cb..9af73a6 100755 --- a/scripts/deploy/deploy_core.py +++ b/scripts/deploy/deploy_core.py @@ -1,10 +1,10 @@ #!/usr/bin/env python3 """Install/update a local release binary on Linux/systemd. No builds. -First install downloads the PDDL user-country GeoIP CSV files from the -sapics/ip-location-db GitHub releases and verifies publisher SHA-256 files. -Updates compare the installed GeoIP files with current publisher checksums and -stage only stale assets before service downtime. Updates stop the service before +First install downloads the PDDL user-country GeoIP CSV files directly from the +sapics/ip-location-db GitHub release and validates their structure before publish. +Updates download/validate current files before downtime, compare content SHA-256 +locally and stage only changed assets. Updates stop the service before copying all private data, refresh the managed systemd unit, switch an immutable release symlink, check health/version and restore code, data and unit on failure. Use --plan to inspect operations without changing the host. @@ -35,12 +35,10 @@ ROOT = Path(__file__).resolve().parents[2] UNIT_PATH = Path('/etc/systemd/system/rustproxy.service') SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh') SERVICE = 'rustproxy.service' -GEOIP_ASSETS = ( - 'user-country-ipv4.csv', - 'user-country-ipv6.csv', -) -GEOIP_RELEASE = 'https://github.com/sapics/ip-location-db/releases/download/latest/' -GEOIP_CHECKSUM = 'https://github.com/sapics/ip-location-db/releases/download/checksum/' +GEOIP_SOURCES = { + 'user-country-ipv4.csv': 'https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv4.csv', + 'user-country-ipv6.csv': 'https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv6.csv', +} GEOIP_MAX_BYTES = 256 * 1024 * 1024 @@ -97,7 +95,7 @@ def digest(path: Path) -> str: def fetch_https(url: str, max_bytes: int) -> bytes: if urllib.parse.urlparse(url).scheme != 'https': raise ValueError('GeoIP downloads require HTTPS.') - request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.27.3'}) + request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.28.0'}) with urllib.request.urlopen(request, timeout=30) as response: if urllib.parse.urlparse(response.geturl()).scheme != 'https': raise ValueError('GeoIP download redirected away from HTTPS.') @@ -115,15 +113,65 @@ def fetch_https(url: str, max_bytes: int) -> bytes: return bytes(result) -def publisher_geoip_checksums() -> dict[str, str]: - expected: dict[str, str] = {} - for name in GEOIP_ASSETS: - checksum = fetch_https(GEOIP_CHECKSUM + name + '.sha256', 4096).decode('ascii', 'strict').strip() - matched = re.search(r'\b([0-9a-fA-F]{64})\b', checksum) - if not matched: - raise ValueError(f'Invalid publisher checksum for {name}.') - expected[name] = matched.group(1).lower() - return expected +def acquire_geoip_update_lock(data: Path, uid: int | None = None, gid: int | None = None) -> int: + """Serialize installer/manual GeoIP refresh with the in-process worker.""" + path = data / '.geoip-update.lock' + flags = os.O_RDWR | os.O_CREAT + if hasattr(os, 'O_NOFOLLOW'): + flags |= os.O_NOFOLLOW + fd = os.open(path, flags, 0o600) + try: + os.fchmod(fd, 0o600) + if uid is not None and gid is not None: + os.fchown(fd, uid, gid) + fcntl.flock(fd, fcntl.LOCK_EX) + return fd + except BaseException: + os.close(fd) + raise + + +def release_geoip_update_lock(fd: int | None) -> None: + if fd is None: + return + try: + fcntl.flock(fd, fcntl.LOCK_UN) + finally: + os.close(fd) + + +def validate_geoip_csv(name: str, payload: bytes) -> None: + """Reject truncated/error payloads before a GeoIP CSV is published.""" + family = 4 if name == 'user-country-ipv4.csv' else 6 + try: + text = payload.decode('utf-8', 'strict') + except UnicodeDecodeError as error: + raise ValueError(f'GeoIP {name} is not valid UTF-8.') from error + previous_end: int | None = None + rows = 0 + for number, raw in enumerate(text.splitlines(), 1): + line = raw.strip() + if not line or line.startswith('#'): + continue + fields = [value.strip().strip('"') for value in line.split(',')] + if len(fields) != 3: + raise ValueError(f'GeoIP {name} row {number} must contain exactly 3 columns.') + try: + start = ipaddress.ip_address(fields[0]) + end = ipaddress.ip_address(fields[1]) + except ValueError as error: + raise ValueError(f'GeoIP {name} row {number} contains an invalid IP address.') from error + if start.version != family or end.version != family or int(start) > int(end): + raise ValueError(f'GeoIP {name} row {number} contains an invalid address range.') + if previous_end is not None and int(start) <= previous_end: + raise ValueError(f'GeoIP {name} ranges overlap or are unsorted at row {number}.') + country = fields[2] + if country and not re.fullmatch(r'[A-Za-z]{2}', country): + raise ValueError(f'GeoIP {name} row {number} contains an invalid country code.') + previous_end = int(end) + rows += 1 + if rows == 0: + raise ValueError(f'GeoIP {name} does not contain any address ranges.') def geoip_file_digest(path: Path) -> str | None: @@ -137,29 +185,31 @@ def geoip_file_digest(path: Path) -> str | None: def prepare_geoip_update(data: Path, prefix: Path) -> tuple[Path | None, list[str]]: - """Verify local GeoIP files against current publisher checksums. + """Download and validate current GeoIP CSVs before service downtime. - Network/download work happens before the service is stopped. Changed files are - staged under the code prefix and are copied into the private data transaction - only after its backup exists. + Publisher checksums are intentionally not required here: the upstream checksum + release can move independently from the `latest` data release. Local SHA-256 is + used only to decide whether the validated payload changes an installed file. """ - expected = publisher_geoip_checksums() directory = data / 'geoip' if directory.exists() and directory.is_symlink(): raise ValueError('GeoIP directory cannot be a symlink.') - stale = [name for name in GEOIP_ASSETS if geoip_file_digest(directory / name) != expected[name]] + payloads: dict[str, bytes] = {} + stale: list[str] = [] + for name, url in GEOIP_SOURCES.items(): + payload = fetch_https(url, GEOIP_MAX_BYTES) + validate_geoip_csv(name, payload) + payloads[name] = payload + if geoip_file_digest(directory / name) != hashlib.sha256(payload).hexdigest(): + stale.append(name) if not stale: - print('GeoIP database is current (publisher SHA-256 matches).') + print('GeoIP database is current (validated content is unchanged).') return None, [] staging = prefix / ('.geoip-stage-' + uuid.uuid4().hex) staging.mkdir(mode=0o700) try: for name in stale: - payload = fetch_https(GEOIP_RELEASE + name, GEOIP_MAX_BYTES) - actual = hashlib.sha256(payload).hexdigest() - if actual != expected[name]: - raise ValueError(f'GeoIP SHA-256 mismatch for {name}.') - write_private(staging / name, payload, 0o600) + write_private(staging / name, payloads[name], 0o600) print('GeoIP update prepared: ' + ', '.join(stale)) return staging, stale except BaseException: @@ -204,20 +254,17 @@ def apply_geoip_update(data: Path, staging: Path | None, names: list[str], uid: def download_geoip(data: Path, uid: int, gid: int) -> None: - # First install has no code-prefix staging transaction, so download and verify - # each asset directly before publishing it atomically into the private data dir. - expected = publisher_geoip_checksums() + # First install has no code-prefix staging transaction. Validate each direct + # payload before publishing it atomically into the private data directory. directory = data / 'geoip' if directory.exists() and directory.is_symlink(): raise ValueError('GeoIP directory cannot be a symlink.') directory.mkdir(mode=0o700, exist_ok=True) os.chmod(directory, 0o700) os.chown(directory, uid, gid) - for name in GEOIP_ASSETS: - payload = fetch_https(GEOIP_RELEASE + name, GEOIP_MAX_BYTES) - actual = hashlib.sha256(payload).hexdigest() - if actual != expected[name]: - raise ValueError(f'GeoIP SHA-256 mismatch for {name}.') + for name, url in GEOIP_SOURCES.items(): + payload = fetch_https(url, GEOIP_MAX_BYTES) + validate_geoip_csv(name, payload) target = directory / name write_private(target, payload, 0o600) os.chown(target, uid, gid) diff --git a/scripts/deploy/deploy_runtime.py b/scripts/deploy/deploy_runtime.py index 512903a..b7e628f 100755 --- a/scripts/deploy/deploy_runtime.py +++ b/scripts/deploy/deploy_runtime.py @@ -4,6 +4,7 @@ from __future__ import annotations from deploy_core import * # re-exported internal deployment primitives ACME_RUNTIME_NAMES = frozenset({'acme-python', 'acme-python.installing', 'acme-python.virtualenv-bootstrap'}) +RUNTIME_STATE_NAMES = frozenset({'.geoip-update.lock'}) CERTBOT_PACKAGES = ('certbot==5.8.0', 'certbot-dns-cloudflare==5.8.0', 'certbot-dns-ovh==5.8.0', 'certbot-dns-rfc2136==5.8.0') def _under_acme_runtime(source: Path, path: Path) -> bool: @@ -30,7 +31,9 @@ def copy_data(source: Path, target: Path) -> None: checked_tree(source) root = source.resolve(strict=True) def ignore(directory: str, names: list[str]) -> set[str]: - return set(names) & ACME_RUNTIME_NAMES if Path(directory).resolve(strict=True) == root else set() + if Path(directory).resolve(strict=True) != root: + return set() + return set(names) & (ACME_RUNTIME_NAMES | RUNTIME_STATE_NAMES) shutil.copytree(source, target, symlinks=True, ignore=ignore) os.chmod(target, 0o700) @@ -353,64 +356,68 @@ def stop_for_managed_update(data: Path) -> None: def update(record: dict, binary: Path, sha: str, start: bool, refresh_geoip: bool = False, reset_config: bool = False) -> dict: prefix = Path(record['prefix']); data = Path(record['data_dir']); account = pwd.getpwnam(record['user']) - # Updates deliberately do not rewrite shell environment. Check the installer- - # managed PATH profile early and report the exact remediation target instead. - check_shell_path(prefix) - # Do this before staging/network work and, critically, before stopping the - # currently healthy service. It also repairs stale root:root/0700 metadata - # left by manual maintenance while preserving private 0700 semantics. - ensure_service_data_access(data, account) - release, version = stage(binary, prefix, record['user'], sha) + geoip_lock_fd: int | None = None geoip_stage: Path | None = None - geoip_names: list[str] = [] - if refresh_geoip: - geoip_stage, geoip_names = prepare_geoip_update(data, prefix) - active = subprocess.run(['systemctl', 'is-active', '--quiet', SERVICE], check=False, stdout=subprocess.DEVNULL).returncode == 0 - stop_for_managed_update(data) try: - backup = backup_stopped(record, prefix) - except BaseException: - if active: run(['systemctl', 'start', SERVICE]) - if geoip_stage is not None: - shutil.rmtree(geoip_stage, ignore_errors=True) - raise - replacement = dict(record, release=release, version=version, sha256=sha) - try: - if reset_config: - reset_saved_config(data) - apply_geoip_update(data, geoip_stage, geoip_names, account.pw_uid, account.pw_gid) - prepare_managed_acme_runtime(data, account) - switch(prefix, release) - write_private(UNIT_PATH, service_unit(replacement).encode(), 0o644) - run(['systemctl', 'daemon-reload']) - if start: - run(['systemctl', 'start', SERVICE]) - if not healthy(replacement, version): - raise RuntimeError('New release failed the local health/version check.') - write_private(prefix / 'install.json', json.dumps(replacement, indent=2).encode()) - except BaseException as error: - details = service_failure_details() - if details: - print('New release diagnostics:\n' + details, file=sys.stderr) + # Updates deliberately do not rewrite shell environment. Check the installer- + # managed PATH profile early and report the exact remediation target instead. + check_shell_path(prefix) + # Repair stale root:root/0700 metadata before staging/network work and before + # stopping the currently healthy service. + ensure_service_data_access(data, account) + release, version = stage(binary, prefix, record['user'], sha) + geoip_names: list[str] = [] + if refresh_geoip: + # Keep the same lock for checksum/download staging and publication. The + # runtime worker uses this file too, so manual upgrades cannot race it. + geoip_lock_fd = acquire_geoip_update_lock(data, account.pw_uid, account.pw_gid) + geoip_stage, geoip_names = prepare_geoip_update(data, prefix) + active = subprocess.run(['systemctl', 'is-active', '--quiet', SERVICE], check=False, stdout=subprocess.DEVNULL).returncode == 0 stop_for_managed_update(data) - failed = data.with_name(data.name + '.failed-' + uuid.uuid4().hex[:12]) - data.rename(failed) # keep the failed generation for diagnosis; do not delete it - copy_data(backup / 'data', data) - restore_managed_acme_runtime(failed, data) - chown_tree(data, account.pw_uid, account.pw_gid) - switch(prefix, record['release']) - restore_service_unit(backup) - write_private(prefix / 'install.json', json.dumps(record, indent=2).encode()) - if active: - run(['systemctl', 'start', SERVICE]) - if not healthy(record, record['version']): - raise RuntimeError(f'Update failed: {error}. Rollback restored code/data/unit but previous service health also failed. Backup: {backup}; failed state: {failed}') from error - raise RuntimeError(f'Update failed: {error}. Previous code, private data and service unit were restored. Installation is still present; do not run install.sh. Fix the cause and rerun update.sh. Backup: {backup}; failed state: {failed}') from error + try: + backup = backup_stopped(record, prefix) + except BaseException: + if active: + run(['systemctl', 'start', SERVICE]) + raise + replacement = dict(record, release=release, version=version, sha256=sha) + try: + if reset_config: + reset_saved_config(data) + apply_geoip_update(data, geoip_stage, geoip_names, account.pw_uid, account.pw_gid) + prepare_managed_acme_runtime(data, account) + switch(prefix, release) + write_private(UNIT_PATH, service_unit(replacement).encode(), 0o644) + run(['systemctl', 'daemon-reload']) + if start: + run(['systemctl', 'start', SERVICE]) + if not healthy(replacement, version): + raise RuntimeError('New release failed the local health/version check.') + write_private(prefix / 'install.json', json.dumps(replacement, indent=2).encode()) + except BaseException as error: + details = service_failure_details() + if details: + print('New release diagnostics:\n' + details, file=sys.stderr) + stop_for_managed_update(data) + failed = data.with_name(data.name + '.failed-' + uuid.uuid4().hex[:12]) + data.rename(failed) # keep the failed generation for diagnosis; do not delete it + copy_data(backup / 'data', data) + restore_managed_acme_runtime(failed, data) + chown_tree(data, account.pw_uid, account.pw_gid) + switch(prefix, record['release']) + restore_service_unit(backup) + write_private(prefix / 'install.json', json.dumps(record, indent=2).encode()) + if active: + run(['systemctl', 'start', SERVICE]) + if not healthy(record, record['version']): + raise RuntimeError(f'Update failed: {error}. Rollback restored code/data/unit but previous service health also failed. Backup: {backup}; failed state: {failed}') from error + raise RuntimeError(f'Update failed: {error}. Previous code, private data and service unit were restored. Installation is still present; do not run install.sh. Fix the cause and rerun update.sh. Backup: {backup}; failed state: {failed}') from error + print(f'Backup (contains private keys): {backup}') + return replacement finally: if geoip_stage is not None: shutil.rmtree(geoip_stage, ignore_errors=True) - print(f'Backup (contains private keys): {backup}') - return replacement + release_geoip_update_lock(geoip_lock_fd) def install(record: dict, binary: Path, sha: str, start: bool, install_geoip: bool = False) -> dict: diff --git a/scripts/deploy/upgrade.py b/scripts/deploy/upgrade.py index ddba023..b2c8eb0 100755 --- a/scripts/deploy/upgrade.py +++ b/scripts/deploy/upgrade.py @@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None for name in MANAGED: if (target / name).is_symlink(): raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.") - backup = target / ".upgrade-backup" / ("before-0.27.3-" + uuid.uuid4().hex[:12]) + backup = target / ".upgrade-backup" / ("before-0.28.0-" + uuid.uuid4().hex[:12]) backup.mkdir(parents=True, mode=0o700) installed: list[str] = [] moved: list[str] = [] @@ -84,7 +84,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None remove_legacy = legacy.exists() if remove_legacy and (legacy.is_symlink() or not legacy.is_file()): raise ValueError("Obsolete renewal observation sidecar must be a regular file.") - (backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.27.3", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8") + (backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.28.0", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8") if remove_legacy: legacy.unlink() except BaseException: @@ -115,7 +115,7 @@ def main() -> None: backup = upgrade(Path(__file__).resolve().parents[2], args.target, stopped=args.stopped, data_dir=data_dir) except (OSError, ValueError) as error: parser.exit(2, f"Upgrade refused or rolled back: {error}\n") - print(f"Installed RustProxy 0.27.3. Previous managed source files: {backup}") + print(f"Installed RustProxy 0.28.0. Previous managed source files: {backup}") print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.") print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh") diff --git a/src/acme/runtime_env.rs b/src/acme/runtime_env.rs index 696952b..4b42366 100644 --- a/src/acme/runtime_env.rs +++ b/src/acme/runtime_env.rs @@ -320,7 +320,18 @@ pub async fn runtime_setup_once(state: Weak) { let Some(state) = state.upgrade() else { return; }; - let _ = ensure_runtime(&state).await; + let setup_ok = ensure_runtime(&state).await.is_ok(); + let runtime = state.acme.runtime_view(); + tracing::info!( + status = runtime.status, + mode = runtime.mode, + python = runtime.python, + pip = runtime.pip, + certbot = runtime.certbot, + managed_env = runtime.managed_directory.as_deref().unwrap_or("-"), + setup_ok, + "ACME runtime check complete" + ); } struct ProcessGroup(Option); diff --git a/src/admin/documentation/documents.rs b/src/admin/documentation/documents.rs index aaa6fea..6f70584 100644 --- a/src/admin/documentation/documents.rs +++ b/src/admin/documentation/documents.rs @@ -409,6 +409,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[ "/docs/V0.25.1_AUDIT.md" )), ), + reader_doc!("docs/V0.28.0_IMPLEMENTATION.md", "technical"), reader_doc!("docs/V0.27.3_IMPLEMENTATION.md", "technical"), reader_doc!("docs/V0.27.2_IMPLEMENTATION.md", "technical"), reader_doc!("docs/V0.27.1_IMPLEMENTATION.md", "historical"), diff --git a/src/admin/logging_api.rs b/src/admin/logging_api.rs index 06d3b0f..19ba681 100644 --- a/src/admin/logging_api.rs +++ b/src/admin/logging_api.rs @@ -70,6 +70,10 @@ async fn update_section( config.prometheus = serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))? } + "geoip" => { + config.geoip_updates = + serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))? + } "administration" => { config.administration = serde_json::from_value(value).map_err(|e| invalid_section(§ion, e))? @@ -375,7 +379,7 @@ async fn traffic_status( "capture_user_agent": snapshot.envelope.config.logging.capture_user_agent, "capture_country": snapshot.envelope.config.logging.capture_country }, - "geoip": state.geoip.view(), + "geoip": state.geoip.load().view(), "analytics": state.traffic.view(Some(scope)), "vhosts": vhosts }))) diff --git a/src/admin/openapi.json b/src/admin/openapi.json index 79dd04a..d80c038 100644 --- a/src/admin/openapi.json +++ b/src/admin/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "RustProxy Administration API", - "version": "0.27.3", + "version": "0.28.0", "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": [ @@ -606,6 +606,9 @@ "prometheus": { "$ref": "#/components/schemas/PrometheusSettings" }, + "geoip_updates": { + "$ref": "#/components/schemas/GeoIpUpdateSettings" + }, "host_policies": { "type": "array", "items": { @@ -1593,6 +1596,13 @@ "expose_resident_age": { "type": "boolean" }, + "via_header": { + "type": "string", + "nullable": true, + "maxLength": 256, + "default": "RustProxy", + "description": "Optional response Via field value appended by RustProxy cache handling. Null disables the header. This response-only setting does not invalidate cached objects." + }, "cold_max_hot_seconds": { "type": "integer", "minimum": 5, @@ -2258,6 +2268,103 @@ "frontend" ] }, + "GeoIpUpdateSettings": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "default": true + }, + "format": { + "type": "string", + "enum": [ + "csv_pair", + "mmdb" + ], + "default": "csv_pair", + "description": "GeoIP database format. csv_pair uses separate IPv4/IPv6 range files; mmdb uses one native MaxMind DB file." + }, + "ipv4_url": { + "type": "string", + "format": "uri", + "default": "https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv4.csv", + "description": "Direct HTTPS URL for the IPv4 CSV database." + }, + "ipv6_url": { + "type": "string", + "format": "uri", + "default": "https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv6.csv", + "description": "Direct HTTPS URL for the IPv6 CSV database." + }, + "mmdb_url": { + "type": "string", + "default": "https://github.com/sapics/ip-location-db/releases/download/latest/user-country.mmdb", + "description": "Direct HTTPS URL for a native MaxMind DB. May be empty only when MMDB automatic updates are disabled and country.mmdb is managed locally." + }, + "ipv4_checksum_url": { + "type": "string", + "format": "uri", + "nullable": true, + "default": null, + "description": "Optional HTTPS URL containing the publisher SHA-256 for the IPv4 CSV." + }, + "ipv6_checksum_url": { + "type": "string", + "format": "uri", + "nullable": true, + "default": null, + "description": "Optional HTTPS URL containing the publisher SHA-256 for the IPv6 CSV." + }, + "mmdb_checksum_url": { + "type": "string", + "format": "uri", + "nullable": true, + "default": null, + "description": "Optional HTTPS URL containing the publisher SHA-256 for the MMDB file." + }, + "max_bytes": { + "type": "integer", + "format": "int64", + "minimum": 1048576, + "maximum": 268435456, + "default": 268435456 + }, + "interval_seconds": { + "type": "integer", + "format": "int64", + "minimum": 60, + "maximum": 2678400, + "default": 86400 + }, + "release_url": { + "type": "string", + "format": "uri", + "nullable": true, + "deprecated": true, + "description": "Legacy release base URL accepted for migration from pre-direct-source GeoIP settings." + }, + "checksum_url": { + "type": "string", + "format": "uri", + "nullable": true, + "deprecated": true, + "description": "Legacy checksum base URL accepted for migration. The former sapics checksum default is migrated to optional/no publisher checksum." + } + }, + "required": [ + "enabled", + "format", + "ipv4_url", + "ipv6_url", + "mmdb_url", + "ipv4_checksum_url", + "ipv6_checksum_url", + "mmdb_checksum_url", + "max_bytes", + "interval_seconds" + ] + }, "PrometheusSettings": { "type": "object", "additionalProperties": false, @@ -4472,15 +4579,49 @@ "type": "object", "description": "Python/pip/Certbot capability used by managed certificate automation.", "properties": { - "status": { "type": "string", "enum": ["checking", "initializing", "ready", "unavailable"] }, - "mode": { "type": "string", "enum": ["none", "managed", "system"] }, - "message": { "type": "string" }, - "managed_directory": { "type": "string", "nullable": true }, - "python": { "type": "boolean" }, - "pip": { "type": "boolean" }, - "certbot": { "type": "boolean" } + "status": { + "type": "string", + "enum": [ + "checking", + "initializing", + "ready", + "unavailable" + ] + }, + "mode": { + "type": "string", + "enum": [ + "none", + "managed", + "system" + ] + }, + "message": { + "type": "string" + }, + "managed_directory": { + "type": "string", + "nullable": true + }, + "python": { + "type": "boolean" + }, + "pip": { + "type": "boolean" + }, + "certbot": { + "type": "boolean" + } }, - "required": ["status", "mode", "message", "managed_directory", "python", "pip", "certbot"] + "required": [ + "status", + "mode", + "message", + "managed_directory", + "python", + "pip", + "certbot" + ] } }, "required": [ @@ -5276,6 +5417,68 @@ } }, "description": "Scoped dynamic-compression policy. The enabled switch and individual codecs inherit by default; global Response codec switches are the hard availability ceiling and min_bytes remains global." + }, + "GeoIpView": { + "type": "object", + "additionalProperties": false, + "properties": { + "loaded": { + "type": "boolean" + }, + "ipv4_ranges": { + "type": "integer", + "minimum": 0 + }, + "ipv6_ranges": { + "type": "integer", + "minimum": 0 + }, + "source": { + "type": "string" + }, + "format": { + "type": "string", + "enum": [ + "csv_pair", + "mmdb" + ] + } + }, + "required": [ + "loaded", + "ipv4_ranges", + "ipv6_ranges", + "source", + "format" + ] + }, + "GeoIpUpdateResult": { + "type": "object", + "additionalProperties": false, + "properties": { + "outcome": { + "type": "string", + "enum": [ + "updated", + "current", + "busy" + ] + }, + "updated_files": { + "type": "array", + "items": { + "type": "string" + } + }, + "database": { + "$ref": "#/components/schemas/GeoIpView" + } + }, + "required": [ + "outcome", + "updated_files", + "database" + ] } } }, @@ -7290,6 +7493,64 @@ } } }, + "/api/geoip/update": { + "post": { + "summary": "Check, download and hot-reload GeoIP country databases now", + "description": "Download the configured direct GeoIP source(s), optionally verify publisher SHA-256 values, validate the selected CSV/MMDB format, atomically publish changed files and hot-reload future GeoIP lookups.", + "tags": [ + "Settings" + ], + "operationId": "post_api_geoip_update", + "responses": { + "200": { + "description": "GeoIP check completed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GeoIpUpdateResult" + } + } + } + }, + "401": { + "description": "Authentication required or revoked", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "403": { + "description": "Host, origin or CSRF validation failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + }, + "502": { + "description": "Publisher check, download, validation or reload failed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Error" + } + } + } + } + }, + "security": [ + { + "AdminSession": [], + "CsrfToken": [] + } + ] + } + }, "/api/cache/purge": { "post": { "summary": "Purge all cache entries or one vhost", @@ -7956,7 +8217,7 @@ } } }, - "description": "Preferred UI/control-plane save path. Replaces only the selected section while preserving every other section from the current revision. This is not a merge patch. Cache identity is independent from the global configuration revision: logging, listeners, TLS inventory, metrics and administration saves do not purge cached objects. Changes which can alter origin requests or cached responses (cache/transform/response/forwarding/vhosts) rotate the cache compatibility identity and purge incompatible hot/cold objects. Supported section names include docker-agents, vhosts, redirects, not_found_hosts, streams, listeners, http2, tls, pages, cache, transform, logging, forwarding, headers, prometheus, administration, acl-profiles, host-policies and response.", + "description": "Preferred UI/control-plane save path. Replaces only the selected section while preserving every other section from the current revision. This is not a merge patch. Cache identity is independent from the global configuration revision: logging, listeners, TLS inventory, metrics and administration saves do not purge cached objects. Changes which can alter origin requests or cached responses (cache/transform/response/forwarding/vhosts) rotate the cache compatibility identity and purge incompatible hot/cold objects. Supported section names include docker-agents, vhosts, redirects, not_found_hosts, streams, listeners, http2, tls, pages, cache, transform, logging, forwarding, headers, prometheus, geoip, administration, acl-profiles, host-policies and response.", "parameters": [ { "name": "section", @@ -7975,6 +8236,7 @@ "forwarding", "headers", "prometheus", + "geoip", "administration", "http2", "tls", diff --git a/src/admin/operations.rs b/src/admin/operations.rs index 77c7280..7f90a45 100644 --- a/src/admin/operations.rs +++ b/src/admin/operations.rs @@ -1,3 +1,34 @@ +async fn geoip_update( + State(state): State>, + Extension(session): Extension, +) -> Result, ApiError> { + let result = crate::geoip::update_now(&state).await.map_err(|error| { + tracing::warn!(error = %format!("{error:#}"), "manual GeoIP update failed"); + ApiError( + StatusCode::BAD_GATEWAY, + format!("GeoIP update failed: {error:#}"), + ) + })?; + state + .store + .audit( + &session.username, + "geoip.update", + &format!( + "outcome={}; files={}", + result.outcome, + if result.updated_files.is_empty() { + "none".to_string() + } else { + result.updated_files.join(",") + } + ), + ) + .await + .map_err(auth::internal)?; + Ok(Json(result)) +} + async fn transform_status(State(state): State>) -> Json { let snapshot = state.config.load_full(); let global = &snapshot.envelope.config.transform; diff --git a/src/admin/router.rs b/src/admin/router.rs index 3cefc95..ed77062 100644 --- a/src/admin/router.rs +++ b/src/admin/router.rs @@ -342,6 +342,7 @@ pub fn router(state: Arc) -> Router { .route("/api/traffic", get(traffic_status)) .route("/api/cache", get(cache_status)) .route("/api/transform", get(transform_status)) + .route("/api/geoip/update", post(geoip_update)) .route("/api/cache/purge", post(cache_purge)) .route( "/api/cache/invalidate", diff --git a/src/admin/status.rs b/src/admin/status.rs index 54c156e..0057739 100644 --- a/src/admin/status.rs +++ b/src/admin/status.rs @@ -156,7 +156,7 @@ fn status_payload(state: &App) -> Value { let transform_resources = crate::transform::resource_usage(&snapshot.envelope.config.transform); payload["cache"] = json!(&cache_view); payload["transform_resources"] = json!(&transform_resources); - payload["geoip"] = json!(state.geoip.view()); + payload["geoip"] = json!(state.geoip.load().view()); let system = state.diagnostics.view(); let cache_ram_bytes = (cache_view.hot_allocated_bytes as u64).saturating_add(cache_view.transient_bytes); diff --git a/src/admin/tests/part_04.rs b/src/admin/tests/part_04.rs index fee06a6..c527074 100644 --- a/src/admin/tests/part_04.rs +++ b/src/admin/tests/part_04.rs @@ -396,6 +396,7 @@ ("/api/acme/issue", "POST"), ("/api/acme/renew", "POST"), ("/api/docker/refresh", "POST"), + ("/api/geoip/update", "POST"), ("/api/cache/purge", "POST"), ("/api/cache/invalidate", "POST"), ("/api/cache/bans", "GET"), diff --git a/src/admin/web/app/core.js b/src/admin/web/app/core.js index 84009a2..520dcea 100644 --- a/src/admin/web/app/core.js +++ b/src/admin/web/app/core.js @@ -116,7 +116,7 @@ function appConfirm(message, options = {}) { } function apiArea(path) { const section = path.match(/^\/api\/config\/([^/?]+)/)?.[1]; - if (section) return ({ "listeners": "settings", "http2": "settings", "docker-agents": "dockerVhosts", "response": "settings", "pages": "settings", "cache": "cache", "transform": "settings", "logging": "settings", "forwarding": "settings", "headers": "settings", "prometheus": "settings", "administration": "administration", "tls": "tls", "vhosts": "vhosts", "redirects": "redirects", "not_found_hosts": "notfound", "streams": "streams", "acl-profiles": "aclProfiles", "host-policies": "security" })[section] || "configuration"; + if (section) return ({ "listeners": "settings", "http2": "settings", "docker-agents": "dockerVhosts", "response": "settings", "pages": "settings", "cache": "cache", "transform": "settings", "logging": "settings", "geoip": "settings", "forwarding": "settings", "headers": "settings", "prometheus": "settings", "administration": "administration", "tls": "tls", "vhosts": "vhosts", "redirects": "redirects", "not_found_hosts": "notfound", "streams": "streams", "acl-profiles": "aclProfiles", "host-policies": "security" })[section] || "configuration"; if (path === "/api/config") return "configuration"; if (path.startsWith("/api/traffic")) return "traffic"; if (path.startsWith("/api/cache")) return "cache"; diff --git a/src/admin/web/app/runtime.js b/src/admin/web/app/runtime.js index d61bbd4..c3f44f3 100644 --- a/src/admin/web/app/runtime.js +++ b/src/admin/web/app/runtime.js @@ -359,7 +359,28 @@ function renderDiagnostics(status, appendSample = true) { const l = status.listeners || {}; $("listener-pending").hidden = !l.restart_required; $("listener-pending").textContent = t("restartPending"); - $("listener-active").textContent = `${t("activeListeners")}: ${l.active_endpoints?.map(e => `${e.tls ? "HTTPS" : "HTTP"} ${e.address}${e.http3 ? " + H3/UDP" : ""}`).join(" | ") || `HTTP ${status.proxy_listen} / HTTPS ${status.tls_listen || t("notEnabled")}`}`; + const activeListeners = $("listener-active"), listenerItems = el("div", undefined, "listener-active-items"); + let endpoints = Array.isArray(l.active_endpoints) ? l.active_endpoints : []; + if (!endpoints.length) { + endpoints = []; + if (status.proxy_listen) endpoints.push({ tls: false, address: status.proxy_listen, http3: false }); + if (status.tls_listen) endpoints.push({ tls: true, address: status.tls_listen, http3: false }); + } + for (const endpoint of endpoints) { + const item = el("div", undefined, "listener-active-item"); + item.append(el("strong", endpoint.tls ? "HTTPS" : "HTTP", "listener-active-protocol"), el("code", endpoint.address || "-", "listener-active-address")); + const protocols = ["H1"]; + if (endpoint.tls) { + if (endpoint.http2) protocols.push("H2"); + if (endpoint.http3) protocols.push("H3/UDP"); + } else if (endpoint.h2c) { + protocols.push("H2C"); + } + for (const protocol of protocols) item.append(el("span", protocol, "listener-active-feature")); + listenerItems.append(item); + } + if (!endpoints.length) listenerItems.append(el("span", t("notEnabled"), "fine")); + activeListeners.replaceChildren(el("strong", t("activeListeners"), "listener-active-title"), listenerItems); } function renderHeaderRules(direction, rules) { const target = $(`${direction}-header-rules`); target.replaceChildren(); diff --git a/src/admin/web/app/settings.js b/src/admin/web/app/settings.js index 0799a12..6d0a632 100644 --- a/src/admin/web/app/settings.js +++ b/src/admin/web/app/settings.js @@ -30,6 +30,8 @@ function renderSettings() { $("cache-query").checked = !!c.bypass_query; $("cache-prefixes").value = (c.bypass_prefixes || []).join("\n"); $("cache-static-cookie").checked = c.static_ignore_cookies !== false; $("cache-static-extensions").value = (c.static_extensions || []).join("\n"); $("cache-status-header").checked = !!c.expose_status; $("cache-hits-header").checked = !!c.expose_hits; $("cache-rfc-header").checked = !!c.expose_cache_status; $("cache-resident-age-header").checked = !!c.expose_resident_age; + const viaHeader = c.via_header === undefined ? "RustProxy" : c.via_header; + $("cache-via-enabled").checked = viaHeader !== null; $("cache-via-value").value = viaHeader || "RustProxy"; syncCacheVia(); setCacheMemoryPresetSelection(["conservative", "recommended", "performance"].includes(c.memory_preset) ? c.memory_preset : null); setCacheColdPresetSelection(c.cold_preset === "recommended" ? "recommended" : null); syncColdCachePolicy(); @@ -170,6 +172,10 @@ function applyCacheMemoryPreset(kind) { setCacheMemoryPresetSelection(kind); setSettingsDirty("cache-form"); updateCacheMemorySizing(); } +function syncCacheVia() { + const enabled = $("cache-via-enabled")?.checked; + if ($("cache-via-value")) $("cache-via-value").disabled = !enabled; +} function applyRecommendedColdCachePreset() { const transient = Number($("cache-transient-memory").value); const queue = Number.isFinite(transient) && transient > 0 ? Math.max(0.0625, Math.floor((transient * .5) / 8) * 8 || Math.min(transient, 8)) : 32; @@ -279,7 +285,7 @@ bindSettingsForm("response-form", next => { next.compression = { min_bytes: Number($("compress-min").value) }; for (const name of ["gzip", "br", "zstd", "deflate"]) next.compression[name] = $(`compress-${name}`).checked; }, "response", next => ({ server_header: next.server_header, diagnostic_headers: next.diagnostic_headers, compression: next.compression })); bindSettingsForm("cache-form", next => { - next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked }; + next.cache = { ...(next.cache || {}), enabled: $("cache-enabled").checked, memory_preset: selectedCacheMemoryPreset, max_bytes: Math.round(Number($("cache-memory").value) * 1048576), transient_max_bytes: Math.round(Number($("cache-transient-memory").value) * 1048576), max_object_bytes: Number($("cache-object").value) * 1024, max_entries: Number($("cache-entries").value), hot_eviction_max_objects: Number($("cache-eviction-limit").value), hot_auto_trim_enabled: $("cache-auto-trim-enabled").checked, hot_auto_trim_after_seconds: Number($("cache-auto-trim-after").value), hot_auto_trim_target_percent: Number($("cache-auto-trim-target").value), cold_enabled: $("cache-cold-enabled").checked, cold_preset: selectedCacheColdPreset, cold_path: $("cache-cold-path").value.trim(), cold_max_bytes: Math.round(Number($("cache-cold-bytes").value) * 1048576), cold_max_entries: Number($("cache-cold-entries").value), cold_queue_max_bytes: Math.round(Number($("cache-cold-queue-bytes").value) * 1048576), cold_gc_interval_seconds: Number($("cache-cold-gc-interval").value), cold_gc_max_delete: Number($("cache-cold-gc-max-delete").value), cold_after_seconds: Number($("cache-cold-after").value), cold_max_hot_seconds: Number($("cache-cold-max-hot").value), cold_on_idle: $("cache-cold-on-idle").checked, cold_on_age: $("cache-cold-on-age").checked, cold_on_lru: $("cache-cold-on-lru").checked, cold_on_shutdown: $("cache-cold-on-shutdown").checked, max_ttl_seconds: Number($("cache-ttl").value), default_ttl_seconds: Number($("cache-default-ttl").value), collapse_enabled: $("cache-collapse-enabled").checked, collapse_wait_timeout_ms: Number($("cache-collapse-timeout").value), collapse_timeout_fallback: $("cache-collapse-fallback").value, stale_if_error_timeout: $("cache-sie-timeout").checked, stale_if_error_connection: $("cache-sie-connection").checked, stale_if_error_dns: $("cache-sie-dns").checked, stale_if_error_tls: $("cache-sie-tls").checked, stale_if_error_statuses: $("cache-sie-statuses").value.split(/[\s,]+/).filter(Boolean).map(Number), proactive_refresh_enabled: $("cache-proactive-enabled").checked, proactive_refresh_before_expiry_seconds: Number($("cache-proactive-before").value), proactive_refresh_percent: Number($("cache-proactive-percent").value), proactive_refresh_max_inflight: Number($("cache-proactive-inflight").value), bypass_query: $("cache-query").checked, bypass_prefixes: $("cache-prefixes").value.split(/\r?\n/).map(v => v.trim()).filter(Boolean), static_ignore_cookies: $("cache-static-cookie").checked, static_extensions: $("cache-static-extensions").value.split(/[\n,]+/).map(v => v.trim().replace(/^\./, "").toLowerCase()).filter(Boolean), expose_status: $("cache-status-header").checked, expose_hits: $("cache-hits-header").checked, expose_cache_status: $("cache-rfc-header").checked, expose_resident_age: $("cache-resident-age-header").checked, via_header: $("cache-via-enabled").checked ? $("cache-via-value").value.trim() : null }; }, "cache"); bindSettingsForm("transform-form", next => { next.transform = { @@ -384,6 +390,7 @@ $("server-mode").addEventListener("change", syncServerMode); $("log-file-enabled").addEventListener("change", syncLoggingFileMode); $("log-file-per-vhost").addEventListener("change", syncLoggingFileMode); for (const id of ["cache-cold-enabled", "cache-cold-on-idle", "cache-cold-on-age"]) $(id).addEventListener("change", syncColdCachePolicy); +$("cache-via-enabled").addEventListener("change", syncCacheVia); document.querySelectorAll("[data-cache-memory-preset]").forEach(button => button.addEventListener("click", () => applyCacheMemoryPreset(button.dataset.cacheMemoryPreset))); document.querySelectorAll("[data-cache-cold-preset]").forEach(button => button.addEventListener("click", applyRecommendedColdCachePreset)); document.querySelectorAll("[data-transform-preset]").forEach(button => button.addEventListener("click", () => applyTransformPreset(button.dataset.transformPreset))); diff --git a/src/admin/web/index.html b/src/admin/web/index.html index 047543c..07fa8ed 100644 --- a/src/admin/web/index.html +++ b/src/admin/web/index.html @@ -1749,9 +1749,12 @@ data-settings-tab="pages" id="settings-tab-pages" role="tab" tabindex="-1" type="button"> @@ -2264,6 +2267,15 @@ type="checkbox" />Cache-Status (RFC 9211) +
+ + +
+

Defaults to Via: RustProxy. Changing this + response-only header does not purge cached objects.

Age is sent on cache hits unless an explicit response-header rule removes or replaces it. Such Age rules are wire-only and do not change cache freshness.

@@ -2548,6 +2560,49 @@ + -