This commit is contained in:
Mateusz Gruszczyński committed 2026-10-08 22:22:46 +02:00
1 parent 76cb4acef0
commit 68678e98be
84 files changed
+1494 -300

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes. Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
compatibility. Current source version: 0.29.0. compatibility. Current source version: 0.29.2.
## Delivery ## Delivery
+19
View File
@@ -4,6 +4,25 @@
> test-only changes and packaging implementation notes were moved to > test-only changes and packaging implementation notes were moved to
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md). > [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
## 0.29.2
- Debian install/update now deploys managed logrotate policy (14 daily rotations, 100 MB max, compressed, copytruncate) and repairs the shell PATH profile on upgrade.
- Fix Web Server vhosts returning 503 because backend `transient_unavailable` was applied to the main filesystem/PHP route. The main Web Server source does not consult or compile unused backends; enabled reverse-proxy Custom Locations still use the configured backend pool.
- Remove the three error-fallback Prometheus metrics without changing internal fallback routing.
- Add validated, operator-managed `PHP_ADMIN_VALUE` support for shared trusted PHP-FPM endpoints.
- Expose service-scoped PHP response counts, response-header latency and concurrency gauges in Prometheus, alongside Web Server status and latency counters.
- Keep FastCGI reserved parameter protections and PHP `PATH_INFO` support.
## 0.29.1
- Add Nginx-style internal error fallback routing for vhost sources and Custom locations, including internal-only named locations, configurable 403/404/500/502/503/504 interception, original request target/context preservation, validation-time cycle rejection and a runtime depth guard.
- Intercept configured upstream/local errors before finalization or cache storage, then process fallback targets through normal ACL, header, compression, cache and response handling.
- Add streaming multipart `Range` support with at most 16 byte ranges per request while retaining existing single-range behavior.
- Add optional PHP-FPM `PATH_INFO` support that securely separates an existing script path from trailing path information.
- Keep SQLite schema 9, saved-configuration compatibility and Cold Cache format 3 unchanged.
## 0.29.0 ## 0.29.0
- Group vhost hostnames and WWW redirect controls in Frontends. Each exact domain shows both redirect directions as selectable source → destination cards, even while redirects are off. Implicit `www.` targets are supported without adding an extra hostname. Other aliases continue serving the same application without cross-domain redirects; wildcard targets are excluded. - Group vhost hostnames and WWW redirect controls in Frontends. Each exact domain shows both redirect directions as selectable source → destination cards, even while redirects are off. Implicit `www.` targets are supported without adding an extra hostname. Other aliases continue serving the same application without cross-domain redirects; wildcard targets are excluded.
Generated
+1 -1
View File
@@ -1688,7 +1688,7 @@ dependencies = [
[[package]] [[package]]
name = "rust-proxy" name = "rust-proxy"
version = "0.29.0" version = "0.29.2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"arc-swap", "arc-swap",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rust-proxy" name = "rust-proxy"
version = "0.29.0" version = "0.29.2"
edition = "2021" edition = "2021"
rust-version = "1.88" rust-version = "1.88"
description = "RustProxy - independent reverse proxy with local administration" description = "RustProxy - independent reverse proxy with local administration"
+9 -11
View File
@@ -1,9 +1,9 @@
# RustProxy 0.29.0 # RustProxy 0.29.2
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration, Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools. streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
**0.29.0 release source.** **0.29.2 release source.**
## Layer-4 streams ## Layer-4 streams
@@ -11,17 +11,15 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners. Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
## What changed in 0.29.0 ## What changed in 0.29.2
- Add a per-vhost **www/non-www redirect** switch. The Frontends tab groups hostname chips, www controls and direction cards. For each exact hostname, the operator can select either www to bare or bare to www, even if the target variant was not explicitly added to the vhost; the runtime owns both names. Options stay visible while disabled for preview. Redirect `308` keeps path/query, Force SSL collapses into one hop, and other hostnames such as `suba.local` and `subb.local` keep serving the same application without cross-domain redirects. - Add Nginx-style **internal error fallbacks** for the vhost source and Custom locations. Configured upstream/Web Server statuses can internally hand the original method, URI, query string and request context to an `internal` Custom location without an HTTP redirect. Supported bounded statuses are `403/404/500/502/503/504`; configuration cycles are rejected and runtime dispatch has a depth limit.
- Add a first-class **Redirect** action to Custom locations with `301/302/303/307/308`, an absolute HTTP(S) target, optional capture rewrite regex (`$1`, `${name}`) and optional query preservation. This covers Nginx-style `location` + `rewrite ... permanent` workflows without opening a backend connection. - Execute error fallback before an intercepted response is finalized or cached. The target location re-enters the normal vhost/location pipeline, including ACL, headers, compression, cache policy and response processing. Request bodies are replayed only when safely bounded and replayable; otherwise the original error is retained.
- Make HTTP backend health checks automatically send the first exact vhost DNS hostname as `Host` when the health-check Host field is empty. An explicitly configured health-check Host remains the override. - The main Web Server source ignores backend health/unavailability; Custom Locations configured for Reverse Proxy continue to use their backend pool. Error-fallback-specific Prometheus counters have been removed.
- Add an explicitly dangerous, **per-vhost-only forced-cache URL list**. Matching GET/HEAD targets can bypass request-side query/Cookie/Authorization/client-no-cache exclusions, while response-side privacy guards such as `Set-Cookie`, `private` and `no-store` remain mandatory; Cookie and Authorization are included in variant identity when this mode is used. - Add streaming **multipart byte-range** responses for requests such as `Range: bytes=0-99,200-299`, with a maximum of 16 ranges and identity representation for multipart responses.
- Let Stored Logs discover and read logrotate-style `.gz` rotations with strict compressed/decompressed byte limits and the same bounded tail/search behavior as active logs. - Add opt-in PHP-FPM **PATH_INFO** splitting. With `path_info: true`, `/index.php/foo/bar` securely resolves the existing `index.php` script, keeps `SCRIPT_FILENAME` on that file and sends `/foo/bar` as `PATH_INFO`.
- Retain the existing local/static page status override and cover the common maintenance-page case where HTML is served with HTTP `503 Service Unavailable`.
- Add regression coverage for nested URL query parameters such as `/convert?url=https%3A//...&ip=...`. Literal/encoded backslashes remain rejected as ambiguous routing input; quoted curl URLs must not contain shell backslashes before `?`, `=` or `&`.
0.29.0 keeps SQLite schema 9 and Cold Cache format 3. Existing 0.28.2 configuration remains compatible; new fields use safe/default-off serde defaults. Historical details are in [CHANGELOG.md](CHANGELOG.md). 0.29.2 keeps SQLite schema 9 and Cold Cache format 3. Existing 0.29.0 configuration remains compatible; new fields use safe/default-off serde defaults. Historical details are in [CHANGELOG.md](CHANGELOG.md).
### Historical 0.26.7 release notes ### Historical 0.26.7 release notes
+22
View File
@@ -1,5 +1,27 @@
# RustProxy roadmap # RustProxy roadmap
## 0.29.2 - PHP-FPM operator controls and observability
- [x] Add bounded, operator-only PHP_ADMIN_VALUE INI overrides per Web Service.
- [x] Show admin overrides in the Web Service UI/API, reusing existing location-to-service routing.
- [x] Track PHP responses by status class, response-header latency and free concurrency slots in Prometheus.
- [ ] Verify FastCGI integration and full Rust test suite on a host with a compiler and PHP-FPM.
- [ ] Consider per-location PHP override profiles and PHP-FPM connect/write/read phase metrics.
## 0.29.1 - Internal error routing, multipart ranges and PHP PATH_INFO
Current direction: 0.29.0 -> 0.29.1
- [x] Add configurable internal error fallbacks for the main vhost source and Custom locations.
- [x] Preserve original method, URI, query string and request context during internal fallback dispatch.
- [x] Reject fallback cycles at validation time and cap runtime fallback depth.
- [x] Intercept configured errors before final response/cache handling and run the target through normal route processing.
- [x] Add bounded Prometheus counters for triggered, successful and failed error fallbacks.
- [x] Support fixed fallback status codes 403/404/500/502/503/504.
- [x] Add streaming multipart byte-range responses with a bounded range count.
- [x] Add opt-in secure PHP PATH_INFO splitting for PHP-FPM.
- [x] Keep SQLite schema 9 and Cold Cache format 3 unchanged.
## 0.29.0 - Redirect, health-host, forced-cache and rotated-log controls ## 0.29.0 - Redirect, health-host, forced-cache and rotated-log controls
Current direction: 0.28.2 -> 0.29.0 Current direction: 0.28.2 -> 0.29.0
+83 -75
View File
@@ -1,18 +1,18 @@
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
ee028ed38129f0e76a2b818066428ad74a3d3f1ee61b0b25ea36e77b65f4f0a8 AGENTS.md 6168eae7ed217707898df7041be628b3d24b7dfa51f4aa5b3c5d7f7238e33c37 AGENTS.md
36736e8d99e58e9bbc43f84d81a15df1a2794d483e11f218c21d9429f135e7c3 CHANGELOG.md db6548b334df4476717cc4fbae4d61b4e20ca05733cb66f56f06e68d2306cf61 CHANGELOG.md
1116b5deea3ec4a2a46692b3a4eb2a5bfc31081dd1bdab96b6244c6a6e22c933 Cargo.lock d81f6b20d330b8b67202f9736b5dc8f6314522c3f013895065452bfbb49809fa Cargo.lock
2d9dc00622ab42c7fb29f324528a5b190e906a37c5fe8d0fdd142fc35589972c Cargo.toml 005ba4b3bd3bf5555b74448065a7a6012d7b944534a8bca04c1658031d978bbe Cargo.toml
75b008f237b2226c62a7721b7901018ce83bfc2726f047a83a17c66fcb799f3b Dockerfile 75b008f237b2226c62a7721b7901018ce83bfc2726f047a83a17c66fcb799f3b Dockerfile
02954764624debfb8f630bd14060194469ffe84a571c4b2107ef09144c8c6829 README.md 46005f2fb18f4b737a9db696ad9e68175a53e006a7bac590a47bee9658d010f2 README.md
ac958a06396e1b8af0c46e903e1eb23f38706fa694aac380654abeed9299dbad ROADMAP.md e63a65964f5c8bb244afafcaaed4dcce2d8d9e161cfb0730f399c4e786988ab5 ROADMAP.md
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service 4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
5f950935ca5098a1b6ba500cee86c8f06b8827f7cb30b9294548efd53a92a031 dev.sh 1d63a1e957413e7c47bd51fcf9a75b89c4f7c92aff96c606f5111d4c1a766870 dev.sh
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md 5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go 7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example 5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
@@ -21,25 +21,25 @@ bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/d
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh 9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go 3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go
dc716943644f98db80213efdb3409d0c8c0a180298eefc7de924777606a09ef8 docker-agent/model.go ee4f99b6256713b346453789c78a40dd8eea267fc3179d72e2a3c834476b9749 docker-agent/model.go
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service 634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go 5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
875faa3969df9409d79f050a84b4183496f557edda3d34aaec19fe556cf8c471 docs/ACME_PROVIDERS.md 875faa3969df9409d79f050a84b4183496f557edda3d34aaec19fe556cf8c471 docs/ACME_PROVIDERS.md
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
67a1e0cbe5d1139251852401d876273ec7ea8cd1c595ecd63af097921b0d21b3 docs/API.md 67a1e0cbe5d1139251852401d876273ec7ea8cd1c595ecd63af097921b0d21b3 docs/API.md
464d514c54f9dd1efd7c1b7a0de9cf1dc91ff26bebb79432f177ca686e0a0113 docs/API_ACCESS.md f054206a09bed81018708608cdeecbfd09018ee7cec9d3abc038f8a37d47b815 docs/API_ACCESS.md
4c5288c9ed427cfb919bd023ed9a321dcf6d64bc4cad3e1768393356c5d835ba docs/ARCHITECTURE.md 4c5288c9ed427cfb919bd023ed9a321dcf6d64bc4cad3e1768393356c5d835ba docs/ARCHITECTURE.md
57471965bbbb8bebfe675a36ac9e7b8bf8fb56d7d60537c8724fd5d42a0ffe94 docs/CACHE.md 57471965bbbb8bebfe675a36ac9e7b8bf8fb56d7d60537c8724fd5d42a0ffe94 docs/CACHE.md
6266163e17d2921468abdae92c1620266af8351321530e2c9b7dca6f3dd89c88 docs/CACHE_DIAGNOSTICS.md 6266163e17d2921468abdae92c1620266af8351321530e2c9b7dca6f3dd89c88 docs/CACHE_DIAGNOSTICS.md
c6003dcbc0b95e2cfa9523b7017186cc8b4826248b588af77111ac5eb15174f9 docs/CHANGELOG_TECHNICAL_ARCHIVE.md c6003dcbc0b95e2cfa9523b7017186cc8b4826248b588af77111ac5eb15174f9 docs/CHANGELOG_TECHNICAL_ARCHIVE.md
3bf0f1d06fa0a13f6733aa6c3f1b7d269d7a95e81138c5db6df6fb49f5beb13e docs/CONTAINER.md c8e69e492179ce39958cc63615cf46c4ac08cfdb7f3483aaab66868a3abcd687 docs/CONTAINER.md
6794d1ca3e06a5c04c3e514f4d23f7de917f9f5429ff659fc90dbbae68abccc6 docs/CUSTOM_LOCATIONS.md 7a88824e1d84383ebf69fbef992d6d6c9975902a9170502eac800bf38fd97712 docs/CUSTOM_LOCATIONS.md
16d8f59908c66e595d6003495cbf5c068c68b10aa9495eb8612c38b2de0e4a7c docs/DOCKER_DISCOVERY.md 16d8f59908c66e595d6003495cbf5c068c68b10aa9495eb8612c38b2de0e4a7c docs/DOCKER_DISCOVERY.md
9ea8db4d034ecb573976dd323da29f044538525c230df9f43a47be1ac867b24c docs/ERROR_LOGGING.md 9ea8db4d034ecb573976dd323da29f044538525c230df9f43a47be1ac867b24c docs/ERROR_LOGGING.md
c23b9e7d6e4df4134aa39a697d67ec22948133ecce1d4a36774d2a2a344aae75 docs/FILE_LOGS.md 7dacf2129f2575823d26104ce12fd48a951906c8b2cfd40a7981b8e14813b154 docs/FILE_LOGS.md
8dfc89a8b3512103b87864bd7c24a3dc3de4465edf8f9874e6bed1f4634dd930 docs/HTTP3.md 8dfc89a8b3512103b87864bd7c24a3dc3de4465edf8f9874e6bed1f4634dd930 docs/HTTP3.md
ed17ee90719ff6211ce8974a00c39d1c299f7d58b538dbd4f2024b3ab47d05f2 docs/INSTALL.md a00ec3c1d3b28ba10677435128e031e5f18fe8c5cef2ac90eb0f6779cefeb434 docs/INSTALL.md
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md 3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md 4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md 7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
@@ -48,9 +48,9 @@ fea1ff4055d0930edeffb0c8501a6ba1ac55ec7a7064dcd0e300a43915e8432e docs/MIGRATION
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md 1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md
89a1af6c9fe10c572b211f9632ce256166e67990b5dacacf2ae07265d99a5172 docs/NPM_MIGRATION.md 89a1af6c9fe10c572b211f9632ce256166e67990b5dacacf2ae07265d99a5172 docs/NPM_MIGRATION.md
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md c6be860cad76833f7bda35f371e335a3304893a50b839953329fed3fb96a8af8 docs/PHP_FPM.md
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md 572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
c2a1f118c07c59fb25f0f0e13015fcb9b65bd04ae1570df06ec06ba591a73f4c docs/README.md 7046079140db7c152dc9caa536259d3c175c8893bda3a23d6ca4e4acb8d0b4c0 docs/README.md
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md 6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md 0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.md d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.md
@@ -60,7 +60,7 @@ a2bb837cdd3559d58158880c1f832badf64035376a3623050bb4e40c2bca104c docs/ROADMAP_H
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md 620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
d5c5c59f6e63b8d83dad5d159657c4d4039dfeb4cc1d8af210169b576adbf7e2 docs/SETTINGS.md d5c5c59f6e63b8d83dad5d159657c4d4039dfeb4cc1d8af210169b576adbf7e2 docs/SETTINGS.md
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md 3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
d03f3ec73a8a58d830712f3adaf9de61f11b93f3aa735f6584b07e724c0e0440 docs/TEST_REPORT.md bbf21108ebf9e9b73f346383cae2cf33807ad250a1e1086d273e8b997e173d78 docs/TEST_REPORT.md
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md 8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
660687fc961f71aae2bdf71595aa47a336fa782f4e78ceee8c1d1e63e46393ee docs/TRANSPORT.md 660687fc961f71aae2bdf71595aa47a336fa782f4e78ceee8c1d1e63e46393ee docs/TRANSPORT.md
@@ -103,9 +103,10 @@ efd5935852d4cfd09758bf67c70e437490cfce2baade46c8733201e506dbcc42 docs/V0.27.0_I
8e1b45415a2bffcf26fe113bbc25627daf5d8758eb9e44c38abb060a4a402a46 docs/V0.28.1_IMPLEMENTATION.md 8e1b45415a2bffcf26fe113bbc25627daf5d8758eb9e44c38abb060a4a402a46 docs/V0.28.1_IMPLEMENTATION.md
097a4ff47634e5b09180012135b49a8ed16193fadc36113cad0c8dffb9e47cca docs/V0.28.2_IMPLEMENTATION.md 097a4ff47634e5b09180012135b49a8ed16193fadc36113cad0c8dffb9e47cca docs/V0.28.2_IMPLEMENTATION.md
bfe9b83d2292bae1e579c1841a396173258496063dbd0f3953780dedeebc2c40 docs/V0.29.0_IMPLEMENTATION.md bfe9b83d2292bae1e579c1841a396173258496063dbd0f3953780dedeebc2c40 docs/V0.29.0_IMPLEMENTATION.md
7fe75e824160570c7ebd3b04e92d3493d74bfda64732c1dacfff59d90ed74398 docs/VHOSTS.md 1bf34805a56508bb7e60745be8e872ebdc458f03bd0f56941af9c18d94ad87ce docs/V0.29.1_IMPLEMENTATION.md
c7c6c237f77b8fbc9601d2fbe30216d51394a392ef1705af74d5ca79930e9d61 docs/VHOSTS.md
09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md 09b60fb7b40db4d0139fa2a9e69a2aa9f28d4e61b8643521cfc7e936d7df8774 docs/VHOST_ACCESS.md
4026a4adeee70f98a96cc7aa8d8be16cda42ffb2e5890d38f1ba638134ab0675 docs/WEB_SERVER.md 10bfd138ae4f9892f51c3ddcb72762ec57f6f8d12a1c371feff18abed4d23cb5 docs/WEB_SERVER.md
c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md c336f998027330c58d5afbbbb3a3a682fd44ae3997643882fe342633822f8bb3 docs/screenshots/README.md
5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json 5502e8ce44276213803b66c58e3a4d804a1354f5ed5d83f47ee805e8c44ab088 examples/acl-headers.json
2431dd88a2d9ad2cf3cd4b8fcdfa602563ab1013c612d8917fddbe7febfb2dc3 examples/cache-dashboard.json 2431dd88a2d9ad2cf3cd4b8fcdfa602563ab1013c612d8917fddbe7febfb2dc3 examples/cache-dashboard.json
@@ -117,9 +118,9 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json 5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
84a24c85d2986f4de509e3e816880b051e4eece00a0efa47945102a0c010fb52 grafana/README.md 4068b79ddd898af43b85617d8de9e9f48cfee09a4fb9ed26bf5b692571a30196 grafana/README.md
f15db15a269a1c4ec9c7ff758f418a402475e4712f821599cd20e85ade2f42a8 grafana/rustproxy-overview.json 0d97fbf31f60563bb41f2ea8d798bccebc3d4c57f10bad8b7faa17296722e36b grafana/rustproxy-overview.json
a51d911526d2631282a358be4152ef1ad6ed4034e34f961a5290b1b90fffaf32 install-debian13.sh fde541cd76bc3fa2b6765822bf04baf9f9ea3e4b6e185e76ff467270ddbd945e install-debian13.sh
909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh 909e9ab94617ace74a425f15084bf85f3086cc52ba5b28f2b441ff800abc6ef9 install.sh
42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md 42f9f1d18c021fc7734b5bd020844bf678725a0e539e2fcbe05ec676f6239eae integrations/README.md
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 integrations/opnsense-acme/README.md 2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 integrations/opnsense-acme/README.md
@@ -131,9 +132,9 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh 96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh 3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md 71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md
636ba7cf04e94629f04c65f30abb3e3eef4af4557d80f4edf4efad9377308589 langs/en_US.json 0fc9ecf808a296768c41eb4b44ca90446e24b5728492b2fe4efc9adc3382a7ca langs/en_US.json
000f6706dcf22d74b26f4f0aa7f04cab072fed86909a4f7754ee3b767c60fd7b langs/pl_pl.json 3e8c87e778d696b9062f70a941160a9b60e348ee75ced5bf353e91daa16051aa langs/pl_pl.json
1eaeed54289f6c2fc659714c258fbe2848682962ea4b1f2802a491fea0ef23ce make_zip.py a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql 5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
@@ -150,16 +151,17 @@ ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_r
6e036f736a2b40feb78eec17bf34871771524190f283591d1c89e48e6d2271e6 nagios/check_rustproxy_queues 6e036f736a2b40feb78eec17bf34871771524190f283591d1c89e48e6d2271e6 nagios/check_rustproxy_queues
7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml 7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml
2145ad17ae7a823756371ceab6bdbeb332211df93193ffb4e486f7a80c4f29ae scripts/README.md 2145ad17ae7a823756371ceab6bdbeb332211df93193ffb4e486f7a80c4f29ae scripts/README.md
466d313c77b76e4756cab175f019e4b91c4685766da26c8fe698534bed2c93db scripts/build/swagger_assets.py 2044e6d979712457d6ebc3483efa029cc6de22416ee85898d950300015bd815d scripts/build/swagger_assets.py
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy/deploy.py e4d15aa6aa864d04b9b30aca33826b833632af02ca643f8b23e219a5ea1298dd scripts/deploy/deploy.py
dc2cbeecb183308cb7ade81e2f88583fdd12df8d5f85f373424150edffb428d0 scripts/deploy/deploy_core.py cc268d9d3dd125c9cbff2522fe3137a561fb4fcbd5ee461d70ef2e3a8241cdb1 scripts/deploy/deploy_core.py
2eec7ff7c689416135984e38a90c177a8b06bdf176da69294cce25e5571847d5 scripts/deploy/deploy_runtime.py 8c7da13e95bcddd2bd1b4f1b22ade1c5ac1ecf54448601daa8416905d4239751 scripts/deploy/deploy_runtime.py
83d11d9b21358c698089f0a8ca3404c6b9b5bf1988040675ac5e90edfc18ed2f scripts/deploy/upgrade.py a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy/upgrade.py
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py 31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py 20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py
ad62f73bb42906d617cc1b479f02d4648c7324cb89fc11d2c6aa6bf13774eec9 src/acl.rs ad62f73bb42906d617cc1b479f02d4648c7324cb89fc11d2c6aa6bf13774eec9 src/acl.rs
ea272433e7f92e983509a8ab1aa54242440abef21df526c2f99944ef279e09b9 src/acl/tests.rs ea272433e7f92e983509a8ab1aa54242440abef21df526c2f99944ef279e09b9 src/acl/tests.rs
6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs 6f5d3137bf24451da05c1db9f0f9e2a451305db17b6bc2590815ad54a6511829 src/acme.rs
@@ -173,17 +175,17 @@ c8941a929dfef9e8875adf534394ca01a1532e567d5099fefe3657826886ee25 src/acme/runti
bc9eb32f0d2785bac122d5eff44742cd24306c503daa065564383d46416b2211 src/admin/access.rs bc9eb32f0d2785bac122d5eff44742cd24306c503daa065564383d46416b2211 src/admin/access.rs
019f4aa33e3596bc9c11ef8cf5703bcfffe46c15288c0eba83c8dd9e38055fb2 src/admin/certificates.rs 019f4aa33e3596bc9c11ef8cf5703bcfffe46c15288c0eba83c8dd9e38055fb2 src/admin/certificates.rs
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs 1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
2518cf03b99c2ffb784cb3a68afbeb40565326743c8df91560046ac5a831e13c src/admin/documentation/documents.rs 2270b77cc006de95b3d616aae07d4cb0b45abf3f9301abea5b5dad8ab05e4c56 src/admin/documentation/documents.rs
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs 67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs 002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs
e9f72e65c9ceb255248f460cd7a003fff33e4b258160e248353c3e1fb4e952fb src/admin/file_logs.rs e9f72e65c9ceb255248f460cd7a003fff33e4b258160e248353c3e1fb4e952fb src/admin/file_logs.rs
1596561bb57976268b02e36e8ace426091045ee9d3f8832ce775eccdbc30bc81 src/admin/file_logs_io.rs 1596561bb57976268b02e36e8ace426091045ee9d3f8832ce775eccdbc30bc81 src/admin/file_logs_io.rs
de331ab75c31df65a4f0f1607dbfb26972799bff5d9f232d8e914c38225cffc7 src/admin/logging_api.rs de331ab75c31df65a4f0f1607dbfb26972799bff5d9f232d8e914c38225cffc7 src/admin/logging_api.rs
58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs 58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs
4f11cd49f01f00a30918fc759d2a7bfe96ed0f69c9c5017652964a559a90d24a src/admin/openapi.json 1bdda508bfcc5cb1fa88ea0ce342808f71c1929baf783cc1fac8a738147c9ff9 src/admin/openapi.json
6c2b9a0ecb2f4172f110a363c22a6be32895dbcff9605fe7aa94efe91e0a536a src/admin/operations.rs 6c2b9a0ecb2f4172f110a363c22a6be32895dbcff9605fe7aa94efe91e0a536a src/admin/operations.rs
5287fff6b3ad436ac695e9b66feb1b0b5705b744a9f00cec7ea6a3e951e5eebf src/admin/preferences.rs 5287fff6b3ad436ac695e9b66feb1b0b5705b744a9f00cec7ea6a3e951e5eebf src/admin/preferences.rs
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs 31bd90ee5f5ec2984920225ff6b57e07db3e0758092fb55d7c230e3590c1b015 src/admin/public.rs
d73ea424a2795b6f44248abce459da770be179369a59345cb8f7514040b21071 src/admin/router.rs d73ea424a2795b6f44248abce459da770be179369a59345cb8f7514040b21071 src/admin/router.rs
8708427263cd4caf8454b9115e89acb09172483a090c6b0bd43811dd9e52c7f6 src/admin/session.rs 8708427263cd4caf8454b9115e89acb09172483a090c6b0bd43811dd9e52c7f6 src/admin/session.rs
bebf42cd1808a99a0e669d483951a441ffa22c8d5a0a289c64491adf13c8841a src/admin/status.rs bebf42cd1808a99a0e669d483951a441ffa22c8d5a0a289c64491adf13c8841a src/admin/status.rs
@@ -218,7 +220,7 @@ e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js af6e7a2e890df2fb7385a374bcb3fba52fb5a53dbe18adc1fb7865660fcba842 src/admin/web/file-logs.js
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
4ab416af339213f4774e358855aeaf890fa6a9ae33190bc44cc668fe53d25e87 src/admin/web/index.html 4a30f473b3e764d7703be41636396aef47522ada31caa054d364cd387eec7d8c src/admin/web/index.html
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js 24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json 7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
af0fbd94d9b88ce1cce28e39dfd2c35de031adcba412dd4a9a17d9b416c256dd src/admin/web/login.html af0fbd94d9b88ce1cce28e39dfd2c35de031adcba412dd4a9a17d9b416c256dd src/admin/web/login.html
@@ -230,10 +232,10 @@ bb67e77af5514478485cfb69f339d63d7eb0cbadd093fa054814a416260ccc1d src/admin/web/
82b3d9b270c0adfe7a44925c8dd1a93b39f2936f40f4b260d36dec0689701595 src/admin/web/style.css 82b3d9b270c0adfe7a44925c8dd1a93b39f2936f40f4b260d36dec0689701595 src/admin/web/style.css
a8a73a4fa849ae49391fcd6143e900c515c8b577f71b55cbae9701fa9f5bbe47 src/admin/web/traffic-panel.js a8a73a4fa849ae49391fcd6143e900c515c8b577f71b55cbae9701fa9f5bbe47 src/admin/web/traffic-panel.js
e686d68fd6a368110a85dc4c5dc64e8093950d20d63e9aa44790c03dea6fc193 src/admin/web/transform-panel.js e686d68fd6a368110a85dc4c5dc64e8093950d20d63e9aa44790c03dea6fc193 src/admin/web/transform-panel.js
c6f78facd9e34266b8b651c45bca46db2cf2fc7a1a76a16f31fcce787f60aa9b src/admin/web/vhosts/core.js 7ee39a069155dc2650d688f14f8ea51e1a7bdad95ab4cc62769b4565c52fd37b src/admin/web/vhosts/core.js
47f4f8faa64df6d515f553365aab86df4278eeeb2206fdb383f9a38fc46e31c3 src/admin/web/vhosts/editor.js 38d1f4bee4eae192fc977747241b76ed164bfae174d0377744d0a2ef06f85306 src/admin/web/vhosts/editor.js
a155dd33eb802d4f2906f581b8f0d4b40260cb917766c31d4e887cdf51f2e940 src/admin/web/vhosts/render.js a155dd33eb802d4f2906f581b8f0d4b40260cb917766c31d4e887cdf51f2e940 src/admin/web/vhosts/render.js
319b1f0b8b9591d54da7a9be86633b33520a259ba96e7eb32fa2a28817e768da src/admin/web/web-server.js 7fa7d243714236f145b9df486fbb96712a667e7296837cc9c6c6b9ade2f5d0de src/admin/web/web-server.js
98c00d99822f9be54fc58afb6d4b577901712effd2c5eddce014312681864625 src/auth/files.rs 98c00d99822f9be54fc58afb6d4b577901712effd2c5eddce014312681864625 src/auth/files.rs
b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs
97b5a060f24a102bbaa2f406426e985236c0d719b8038db210c25b34fcd8e923 src/auth/mod.rs 97b5a060f24a102bbaa2f406426e985236c0d719b8038db210c25b34fcd8e923 src/auth/mod.rs
@@ -302,12 +304,13 @@ cdc41a7241739b89297494323f29e0230fa06abbd74e219fedfda11df1db4334 src/cache/test
a8a0ce446eb53ffdb8f8d97efa3e1344410dc3798b25f62b8817e8e160d28940 src/cache/tickets.rs a8a0ce446eb53ffdb8f8d97efa3e1344410dc3798b25f62b8817e8e160d28940 src/cache/tickets.rs
3fc79b09474fe23b5b6fc98e4a376469afb02a41d6cc2dbbba59dc6091c790c8 src/compression.rs 3fc79b09474fe23b5b6fc98e4a376469afb02a41d6cc2dbbba59dc6091c790c8 src/compression.rs
e2f58279499b7c3b47c65d3a7814bb5c7cd6506544d939875fbb0f91fa9aa59d src/compression/tests.rs e2f58279499b7c3b47c65d3a7814bb5c7cd6506544d939875fbb0f91fa9aa59d src/compression/tests.rs
7921ae451ccd65d647165b02919bd97399399da471454a6a1a1f63e682b76fc8 src/config.rs 7bc8285cb2012506fd1a38a71dffa561f885f4ab65a9b2a7c8344319e3cd1cf6 src/config.rs
7ba52d3de4d77d1f3cad1b10de418953adc33901727a0791980847ae223929aa src/config/backend.rs 7ba52d3de4d77d1f3cad1b10de418953adc33901727a0791980847ae223929aa src/config/backend.rs
ae30522f84db31cd77e7324cca1f88212a16e567eefcba45ffb655918a921d30 src/config/compile.rs dd1632face27e53c6aa19f9be338df907700aae1d506667556cf79e4b86fdfbd src/config/compile.rs
63e636b2693950a48f7b559f9e7afdcdbe43644753cc259cc2ae219f766383c6 src/config/model.rs 490d05d84b2fe349ddde54eae44880993fab70811652484dd9ba38b5069d603b src/config/internal_routing.rs
c8bbb5d6356911accceca128d2cd14cf5ed8cc6cb1a789e17d8187074997cb69 src/config/routing.rs 875c107eb8c3b08e5ac4216e8600580397753fe599c4b665b40fa33022b8e169 src/config/model.rs
8ffac4b67fffa8a6d0b96b2f0aa171dc71d5b8b525a9da0f4b51c5b26af5d1f2 src/config/snapshot.rs 1bf510a70805e6269ff39c4029016cbce7985535e52da1b915f6f971a12c2e63 src/config/routing.rs
818545ad36af3ae8d56976f318349dd1f5264ef5665fbf94681a81b79003f8cb src/config/snapshot.rs
7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/config/tests.rs 7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/config/tests.rs
7592bdeaad9950a720e2d841fcb1a4edb2a68af5333a6163e672d0392616103c src/config/tests/part_01.rs 7592bdeaad9950a720e2d841fcb1a4edb2a68af5333a6163e672d0392616103c src/config/tests/part_01.rs
fb6e42d969b207e3e204d5892303740cf6f780be5a195cbee1f894b703994b60 src/config/tests/part_02.rs fb6e42d969b207e3e204d5892303740cf6f780be5a195cbee1f894b703994b60 src/config/tests/part_02.rs
@@ -345,11 +348,11 @@ a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/fo
17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs 17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs 3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
2f6b5a9517453541bf43580a9d963e64e36338575ee9ad295ca3718803a59ed6 src/main/admin_runtime.rs 2f6b5a9517453541bf43580a9d963e64e36338575ee9ad295ca3718803a59ed6 src/main/admin_runtime.rs
3b6a523d5b4e46576d97ae5816ed7b669ed97463afb90d326aa5a9ffb375353c src/main/bootstrap.rs b4e9a3fae5df972df3747352383beb16524056a6aeb95d9d609414f790a586ce src/main/bootstrap.rs
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs 5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs 4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs 0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
1b127f28d53c082614dec1163b6b6ce24bbff0db6cbef9c9873fc44c45443566 src/main/tasks.rs e5c2e9189777725f4afa1c3c052e4f75047ec3f2f486630d990f5d3c194bcc96 src/main/tasks.rs
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs 49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs 201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs 49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs
@@ -358,30 +361,32 @@ a4702c95c7e5077c2ccff86e2e3216e1da6ff9ee4383ae199a0aff2f78db325f src/not_found.
668840e7bd70590e2e13f59b6d5b9e43ca7c877f3d07c71907baea91b5bba6cb src/not_found/tests.rs 668840e7bd70590e2e13f59b6d5b9e43ca7c877f3d07c71907baea91b5bba6cb src/not_found/tests.rs
6aab4f97d72ec6352c940bc89e911b8e50c580821da8202744e9832621458784 src/pages.rs 6aab4f97d72ec6352c940bc89e911b8e50c580821da8202744e9832621458784 src/pages.rs
49c17c5084b3d2f52a7d3473e55311dc4daafb1a47805f8821a5a4090dd477af src/pages/tests.rs 49c17c5084b3d2f52a7d3473e55311dc4daafb1a47805f8821a5a4090dd477af src/pages/tests.rs
4198844bc70c59ca3f8da812928391e2ca3d4eebc84bde432a5724c0c63d414b src/prometheus.rs 0dba7b6a33866cc7bbe05509b01961e545892645d1dad4eb88ac8a4bec65e3bf src/prometheus.rs
de67f5ef52289506ee3b4b4ceb72923eb16b8f53ae55373c6c9e7a01900dadcd src/prometheus/core.rs cc6fb14351b009a16d07ed0f412c2bcc68cd7e551421aea03d8d97ceec9bee40 src/prometheus/core.rs
5da9771bbd9843febe80cc9a722a9ada9d20cfb8d22addde7fef2c4edb66f327 src/prometheus/docker_misc.rs 5ed3d03f9b805413544b932d0c266b0e32b830c783d01900af7988bca6bc48a3 src/prometheus/docker_misc.rs
d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus/model.rs d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus/model.rs
064b2be7fed2f6acf526b1cce070c1bb1b943d36f08fa50bbfcbdf3cf1c9318c src/prometheus/tests.rs 064b2be7fed2f6acf526b1cce070c1bb1b943d36f08fa50bbfcbdf3cf1c9318c src/prometheus/tests.rs
028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs 028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs
e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs
59095a04707c2e4714358f4462dcf21c41952474b9b815c42bbd14e96c573846 src/prometheus/web_php.rs
b5856fd38a957b2ca9c7e3606d84d13d5cfcf3a45b487b15ae1d2dc248cbda57 src/proxy/body.rs b5856fd38a957b2ca9c7e3606d84d13d5cfcf3a45b487b15ae1d2dc248cbda57 src/proxy/body.rs
16b9d4db3b20b2ab14c47ac14fcd18d3d8ca77c3ad9a1f37866ebfa60f379862 src/proxy/body/tests.rs 16b9d4db3b20b2ab14c47ac14fcd18d3d8ca77c3ad9a1f37866ebfa60f379862 src/proxy/body/tests.rs
f0cb26ed34c35bacc74b914457acac8a611390de14cf46e2e6d3bcc848e914e8 src/proxy/cache_helpers.rs f0cb26ed34c35bacc74b914457acac8a611390de14cf46e2e6d3bcc848e914e8 src/proxy/cache_helpers.rs
7bb29e31861757415c9c4d34d3b0b933c67d86a5758e9bdbcde6b54b307b689a src/proxy/error_fallback.rs
cfb0910af8cd868bde3f6c9992319e46d5328d6d8f69c50701df0ea12bef2108 src/proxy/forward/cache_lookup.rs cfb0910af8cd868bde3f6c9992319e46d5328d6d8f69c50701df0ea12bef2108 src/proxy/forward/cache_lookup.rs
212de22f407dd96800afcf65fd1fc7c40fede1b88f80d04850eb8d1227b08a5b src/proxy/forward/response.rs e5eec11349cda99d08b27f730202a57457fb2f48f856537b07dbda669168b5bd src/proxy/forward/response.rs
59665df93430930f146d331f360189bad6ef03979a9cfe991453c5ba259b351b src/proxy/forward/route_cache.rs 8ad9a8dee80a7c16a60f1dd0126790daf3cde1a13e28a2595a0ef64f3fd63792 src/proxy/forward/route_cache.rs
f239c1d5ea7a6efa83f59eb12cbeec310bef00f1360f41b5c64783af30e8469a src/proxy/forward/success_finish.rs f239c1d5ea7a6efa83f59eb12cbeec310bef00f1360f41b5c64783af30e8469a src/proxy/forward/success_finish.rs
e58c03f95b4ee8150665b20cbd76c3c6e21a1096b92b42dedc52c764123176cb src/proxy/forward/success_prepare.rs e58c03f95b4ee8150665b20cbd76c3c6e21a1096b92b42dedc52c764123176cb src/proxy/forward/success_prepare.rs
23b6bd26f6175b52c13d2571318fddd12550ba34ee77b87b5ac0e4811f8e2bee src/proxy/forward/upstream.rs 23b6bd26f6175b52c13d2571318fddd12550ba34ee77b87b5ac0e4811f8e2bee src/proxy/forward/upstream.rs
b604e221d7e0d26b6eedfb5f10e2584c8bda99899fcf28ea740f4046bba3da39 src/proxy/forward_entry.rs c39090511e8057d43f2620175958c3dbd9b686036306a0060bb64eaab4bf6c93 src/proxy/forward_entry.rs
c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs
5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs 5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs
9fbe0578c56f5d428ef07086c3c26cd40780d82d5aa21361ff58d946a395fed7 src/proxy/mod.rs f202c0fe0bc4e786a906d7337d368cd81bcac15ffdbbfd35d5b0d49126d9895a src/proxy/mod.rs
55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs 55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs
3c6ffd4b8578af0836976707f918d73b9516ab37d97e68744d3d608fbaa30438 src/proxy/tests/authority_tests.rs 3c6ffd4b8578af0836976707f918d73b9516ab37d97e68744d3d608fbaa30438 src/proxy/tests/authority_tests.rs
9a4c13efb218a5fe8689db5bd6b66029ca6ec270724d948651f3b18e78c80fba src/proxy/types.rs e80c7fe44abb7607c64ea02d56542753dc25290b3bcf4c4d45b75f2db6ca90d0 src/proxy/types.rs
1b980d68f8b06b5d745b4556343b7990fa20660825953b9d0037c056e6991fc3 src/proxy/upstream_helpers.rs ed1a4dfb68054252c3fb0b64de4dcd9ce00d394e008b79cb169012ef4075297f src/proxy/upstream_helpers.rs
69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs 69f775f6746b4c6d729dc10961dbef54168846f4ce9cf7fe453ab5a2b879c9b8 src/proxy/websocket.rs
016a60b2471edd3df9bbe9f92a9cebea11047178b83ff0e3b2351c12814a4939 src/proxy/websocket/handshake.rs 016a60b2471edd3df9bbe9f92a9cebea11047178b83ff0e3b2351c12814a4939 src/proxy/websocket/handshake.rs
7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs 7e9c4d4c207badabf90630223964bb2ad641f06d4149843cab751f192b8b4adf src/proxy/websocket/tests.rs
@@ -450,35 +455,37 @@ a67e58a597a5eeb7efe605ae6e438a8f84ff6a1cedf8cd228141cb95551872a8 src/upstream.r
225e3078e6775ffaa6dc6f12bc87503a123983cbb347edf38cae4c37ff02a5db src/upstream/client.rs 225e3078e6775ffaa6dc6f12bc87503a123983cbb347edf38cae4c37ff02a5db src/upstream/client.rs
4aa2420a92d33c50f7027f57ac4eccdc050bdb7e9ec8fa6e0eaa809228b71cd8 src/upstream/client_tests.rs 4aa2420a92d33c50f7027f57ac4eccdc050bdb7e9ec8fa6e0eaa809228b71cd8 src/upstream/client_tests.rs
6f932b8e59de8a5c89d3b29b838eb2a30f6e3ddd342fec29e39af1ce1b1c7d11 src/upstream/tests.rs 6f932b8e59de8a5c89d3b29b838eb2a30f6e3ddd342fec29e39af1ce1b1c7d11 src/upstream/tests.rs
251b5d1cf0c24d644e13f85ae473f630a76ca361dbb4b4ea971194c66dcab9f0 src/vhost.rs 3aa1b07cd26d4a5b9e5e94d1d6a712ef1397d827e7f1e8c01f7195b98d2c5f67 src/vhost.rs
2152423877010c45328e8c78305669c3b9cb86aff5abf769eb074736c0b80f9b src/vhost/model.rs 6678192de56bc10c5cf82bdff8da14eece7685f409592450c07478333aa3b99c src/vhost/error_fallback.rs
2d3a9d0321edc910d38afe6349d17d81bc090d36964486c8b87b64037311a83e src/vhost/model.rs
35a0aca3eee8d47d1bab27c83d4532cd572fc3e17b5f0d4a8d4515a8f85b3d96 src/vhost/redirect.rs 35a0aca3eee8d47d1bab27c83d4532cd572fc3e17b5f0d4a8d4515a8f85b3d96 src/vhost/redirect.rs
7127e081d884863b21c06f700b54884f90ec04efdfce835591b930f2d210faf6 src/vhost/tests.rs 9b16b5307e140db97363f56c9fac8319e56df846bb1c919d3bc06091ab6ce69c src/vhost/tests.rs
be63aaf2ca2a3efe10afc863216c38b0e12a114cefba96781e50c997e98f0434 src/vhost/validation.rs b61a40bb0ead18b3e348f03a258bab6a68d5c8f46968430f692458aa0cfd5bce src/vhost/validation.rs
8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs 8d1eff4b24f01d137414ef5419f75dd281879228adb70409645b11b3c78c20e9 src/web_server/config.rs
35471c7ba376365d4044c4cc042f675491453d6e59211255802ce4b386172209 src/web_server/config/compile.rs f1c9028fe40ee54e797dd2747c95ecffca87a77eb9de2e1cfde930b8bc8ad9c4 src/web_server/config/compile.rs
c1fef6dfb79275479c15097b211cd5f063ad7e0852a0767b0145cf4f32f751da src/web_server/config/model.rs 53def5e7ddea307d6a8e8969af532a225636fa56a981ca4de3ec6fc41e59e5e4 src/web_server/config/model.rs
f1d3e427c53f8b5be5768d85e69ead746d83647fcd3b355acc98e3f9d5e217e0 src/web_server/config/validation.rs 42a613234af7015a0b14f125bb88be1dcc30a47e9064b12e3fbb5b71ce6f7caf src/web_server/config/validation.rs
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs 86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
62f0b6dfad23dbe99a06f1dc2e998946af64f2754323c306ccb9fbdf2138ee5a src/web_server/fastcgi/protocol.rs 4b2d1b59283d8ac841b3ea013c1e9d94af6f1b939735081750c019c2ecb1dbb8 src/web_server/fastcgi/protocol.rs
134c4998f6a0aa27e121af1ae3355b7e335488b2ae867ac0a934c58b4eb77191 src/web_server/fastcgi/response.rs 6a2fff148bf5a6485c2017a8b0cb3d4214cfc247197dd3c77619bf0d19331a19 src/web_server/fastcgi/response.rs
8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs 8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs
0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs 0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs
013830a79a78ce803ce776a9b333de406367751e0cea7e33c3f6a98e6600c243 src/web_server/htaccess/model.rs 013830a79a78ce803ce776a9b333de406367751e0cea7e33c3f6a98e6600c243 src/web_server/htaccess/model.rs
76de1d11df75f4a64556b2821fd0a1774d26c9f0ca0e5f2b5fcfad0722d43d47 src/web_server/htaccess/parse.rs 76de1d11df75f4a64556b2821fd0a1774d26c9f0ca0e5f2b5fcfad0722d43d47 src/web_server/htaccess/parse.rs
66260d159986d41eab0dfde2cb9f0713fc1cc209c2fb47e9023bd24fd37919e8 src/web_server/htaccess/rewrite.rs 66260d159986d41eab0dfde2cb9f0713fc1cc209c2fb47e9023bd24fd37919e8 src/web_server/htaccess/rewrite.rs
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs ddc329309a3c75c095aede58495b7f698929237e1d08467921a971577ca8405d src/web_server/metrics.rs
d855dbc3a379a43a41e59998d9e93d069c09d562502630436447d692cce3100a src/web_server/mod.rs
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs 67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
5f5d0a1bcf8fe200d9883146786841c6b40b48776a8ffb8ecf397a0c3a58c0b7 src/web_server/response/file.rs 87a0dd815d92688bbc173ca1b2a28487c4dc580cf04318273ac1d460791f6788 src/web_server/response/file.rs
9aa68251a5f549f4ce3b1bda88d4abb9efc629caf444989381b4117c9488e37b src/web_server/response/headers.rs 20509f9302b79d3a7be76dda6edaf5711826d3ddf47d19ebe07cb289427ca328 src/web_server/response/headers.rs
2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs 9b58d6bb3ff1520060fc5aa86a3594f99192b1a8f8183255be1fb3ff35b4bb35 src/web_server/routing.rs
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs 400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs 84868853bfe2d895ec4051c7c8ce9ccdc7c429fdd9718be460a81e78fb7391f1 src/web_server/serve.rs
cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs
c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs f5254272e3588e89e8417932244c136cedd769a5286342332be57800579d8390 src/web_server/tests/part_01.rs
6b41487f46b9b249d141980a350e8501ce61e571ff0605bf3561e1a88201f4cf src/web_server/tests/part_02.rs 874c228e03d11b33b5db7e58279569b35820d813ad6a9f45ee73b630f7bdad03 src/web_server/tests/part_02.rs
e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs 8e52125bdde56484a9bf3880392de8e10bbab05e2ed0faae6056410296761d64 src/web_server/tests/part_03.rs
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs 1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
347a4ff9e028a1e999ad129590c355b5734cf928dd81e5099aea0c8aca8d5908 tests/http3.rs 347a4ff9e028a1e999ad129590c355b5734cf928dd81e5099aea0c8aca8d5908 tests/http3.rs
@@ -486,21 +493,22 @@ eecf680f001f2110dde4eceaccf2f922859a16c946a644f8854e8de4d3771f7c tests/policies
728999110a6d8cacb62bc0399bc333fa23a66526ee23f563a3c93bf00e5fad19 tests/transport_config.rs 728999110a6d8cacb62bc0399bc333fa23a66526ee23f563a3c93bf00e5fad19 tests/transport_config.rs
b04362ad082a32c9ccc4cf38c46f44d71347adecf7aba3224b3af3ea4872e3d0 tools/README.md b04362ad082a32c9ccc4cf38c46f44d71347adecf7aba3224b3af3ea4872e3d0 tools/README.md
358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py 358c1e94dd2b4a756430e5335747f0b437345b1713670a9972c5bcf78b837ccb tools/audit/admin_audit.py
a699c0366c1b9329083602b2731fa5687839dfe265e5a60ff60a3b5796a493fb tools/checks/check_source.py fa6565e6edca33225b86a4589f9b3f0a823369069a9634825b1e0a4b2d37af31 tools/checks/check_source.py
5e2382a4f4094cf0a7cbc5870c78a33dfea65682dd81625b4fc12eee000dd6c8 tools/checks/docs_check.py 0c01cc6980d2ed09f9dab9f2985980375981bbc930e53c7179fb9016d31aea67 tools/checks/docs_check.py
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py 4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
cf762780c4085791c72c3c5f93054d7f2a1107403bee0616a1f4113bbcbb99f9 tools/checks/memory_contracts.py cf762780c4085791c72c3c5f93054d7f2a1107403bee0616a1f4113bbcbb99f9 tools/checks/memory_contracts.py
f0cef60dfb1180fdb495cc6a617c033db92b92d279b5fd9cd3b1343b130d1e40 tools/checks/preflight.py c8b6981af08a78b06f7eaaa251514cec7f807a3e8508c1991049eb9d781c007a tools/checks/preflight.py
7187ba9799fda295698f56b1f89b2ac66c0abf3fa155f73c71726cbc14b91ad9 tools/checks/release_smoke.py cfa24e9792d9e151b84937ec274b23f1e266ebaee63e62056e1bdfa2fee02af1 tools/checks/release_smoke.py
f69845bc26ec33160fc756953991cf32aa8ad6a7fb4a131caa2093db91e783ef tools/checks/rust_module_contracts.py f69845bc26ec33160fc756953991cf32aa8ad6a7fb4a131caa2093db91e783ef tools/checks/rust_module_contracts.py
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py
adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py
23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py 23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py
2309c41e769e797c838d4f40d50bb3c6b9ad9ad0f9ed07fb93e07f95d66563f9 tools/checks/source_module_contracts.py 2309c41e769e797c838d4f40d50bb3c6b9ad9ad0f9ed07fb93e07f95d66563f9 tools/checks/source_module_contracts.py
987b4c593b51a7c340d8f01f9a2eea1387043cdd997eeb932c8341bbb8c0bd64 tools/checks/version_contracts.py
0c071c26fc316934bdfd2bfd19f5fed9521790d8c887760eaabda81a9be96703 tools/checks/vhost_www_ui_test.mjs 0c071c26fc316934bdfd2bfd19f5fed9521790d8c887760eaabda81a9be96703 tools/checks/vhost_www_ui_test.mjs
0aaa80453e25aae7ea953fadb91b3ae3eaff627977c81c9ab5a2779f9a93582f tools/checks/web_assets.py 0aaa80453e25aae7ea953fadb91b3ae3eaff627977c81c9ab5a2779f9a93582f tools/checks/web_assets.py
ba8e92ec5dde6e210fdc034a4871115acccf03288cf39b32a82ab2d4dcf390ed tools/checks/web_server_contracts.py 3c6fbb7fb96007d759e4bfef1424104f7ee14e22439c69ad955ed17fcfc53484 tools/checks/web_server_contracts.py
c245f8f832bd2c36a3fbd5e66fe98e7367c70a8db840c79e6b640f1d9d74f2ac tools/checks/web_service_ui_test.mjs e99cade378ef648d3f20cc9abed4da83d05c3b7322a69018467e8af37129c541 tools/checks/web_service_ui_test.mjs
adaa1ed9e5958739df40190055c780c80d0a8e270ec8dc105ed3fa2dbb8fb74f tools/checks/www_redirect_ui_test.mjs adaa1ed9e5958739df40190055c780c80d0a8e270ec8dc105ed3fa2dbb8fb74f tools/checks/www_redirect_ui_test.mjs
d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a tools/dev/mock_auth.py
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py 2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 tools/dev/mock_server.py
+5 -3
View File
@@ -13,12 +13,13 @@ fi
if ! command -v cargo >/dev/null 2>&1 && [[ -x "$HOME/.cargo/bin/cargo" ]]; then if ! command -v cargo >/dev/null 2>&1 && [[ -x "$HOME/.cargo/bin/cargo" ]]; then
export PATH="$HOME/.cargo/bin:$PATH" export PATH="$HOME/.cargo/bin:$PATH"
fi fi
RUSTPROXY_VERSION="$(python3 scripts/version.py)"
mode="${1:-run}" mode="${1:-run}"
if [[ $# -gt 0 ]]; then shift; fi if [[ $# -gt 0 ]]; then shift; fi
case "$mode" in case "$mode" in
help|-h|--help) help|-h|--help)
cat <<'HELP' cat <<HELP
RustProxy 0.29.0 RustProxy ${RUSTPROXY_VERSION}
./dev.sh Build and start proxy + local panel ./dev.sh Build and start proxy + local panel
./dev.sh run [serve flags] Same, with extra server options ./dev.sh run [serve flags] Same, with extra server options
./dev.sh admin setup-token Generate/print a one-time setup capability before first login ./dev.sh admin setup-token Generate/print a one-time setup capability before first login
@@ -90,6 +91,7 @@ if [[ "$mode" == "repair" ]]; then printf 'Source preflight: OK\n'; exit 0; fi
if [[ "$mode" == "test" ]]; then if [[ "$mode" == "test" ]]; then
python3 tools/checks/rust_module_contracts.py python3 tools/checks/rust_module_contracts.py
python3 tools/checks/source_module_contracts.py python3 tools/checks/source_module_contracts.py
python3 tools/checks/version_contracts.py
python3 tools/checks/check_source.py python3 tools/checks/check_source.py
python3 tools/checks/release_smoke.py python3 tools/checks/release_smoke.py
python3 tools/checks/memory_contracts.py python3 tools/checks/memory_contracts.py
@@ -141,7 +143,7 @@ if [[ "$mode" == "test" ]]; then
printf 'Checking all Rust targets before tests...\n' >&2 printf 'Checking all Rust targets before tests...\n' >&2
cargo check --locked --all-targets cargo check --locked --all-targets
fi fi
printf 'Building RustProxy 0.29.0...\n' >&2 printf "Building RustProxy %s...\n" "$RUSTPROXY_VERSION" >&2
cargo build --locked cargo build --locked
BIN="$CARGO_TARGET_DIR/debug/rust-proxy" BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
case "$mode" in case "$mode" in
+1 -1
View File
@@ -1,6 +1,6 @@
package main package main
const version = "0.29.0" const version = "0.29.2"
type config struct { type config struct {
Listen string Listen string
+1 -1
View File
@@ -1,4 +1,4 @@
# Administration API access - 0.29.0 # Administration API access - 0.29.1
## Sign-in boundary ## Sign-in boundary
+2 -2
View File
@@ -9,7 +9,7 @@ each Docker host, so the proxy container never needs Docker-socket access. See
Build locally with a normal OCI builder: Build locally with a normal OCI builder:
```bash ```bash
docker build -t rustproxy:0.29.0 . docker build -t rustproxy:0.29.1 .
``` ```
Persist `/var/lib/rustproxy`; it contains the SQLite database, certificates and the Persist `/var/lib/rustproxy`; it contains the SQLite database, certificates and the
@@ -22,7 +22,7 @@ docker volume create rustproxy-data
docker run --rm -d --name rustproxy \ docker run --rm -d --name rustproxy \
-p 8080:8080 \ -p 8080:8080 \
-v rustproxy-data:/var/lib/rustproxy \ -v rustproxy-data:/var/lib/rustproxy \
rustproxy:0.29.0 rustproxy:0.29.1
docker exec rustproxy rust-proxy admin setup-token docker exec rustproxy rust-proxy admin setup-token
``` ```
+6
View File
@@ -110,6 +110,12 @@ All access layers are additive. The request must pass the vhost method policy, l
Authentication, vhost rate limits, security headers, compression, custom error pages and WebSocket policy still apply through the normal vhost pipeline. Authentication, vhost rate limits, security headers, compression, custom error pages and WebSocket policy still apply through the normal vhost pipeline.
## Internal locations and error fallbacks
RustProxy 0.29.1 can mark a Custom location as `internal`. An internal location is addressed by its stable location `id` and is never selected by a public URI match. The vhost main source or another Custom location can use `error_fallback` to internally dispatch configured `403/404/500/502/503/504` responses to that target. The default intercepted status is `404`.
This is an internal request handoff, not an HTTP redirect. The original method, URI/query and request context are retained. The fallback target re-enters normal access/header/cache/compression/response processing. Fallback targets must be enabled internal locations in the same vhost; validation rejects cycles and runtime dispatch is depth-limited. See [0.29.1 implementation note](V0.29.1_IMPLEMENTATION.md).
## What is intentionally not an Nginx parser ## What is intentionally not an Nginx parser
Custom locations do **not** execute arbitrary Nginx directives or configuration snippets. RustProxy does not currently implement location-level `try_files`, per-location filesystem aliases/roots, embedded scripting, arbitrary `if` expressions or arbitrary module directives. Static roots are defined only by reusable Web Services. Use the explicit UI controls and the documented JSON schema instead; unknown fields are rejected on save. Custom locations do **not** execute arbitrary Nginx directives or configuration snippets. RustProxy does not currently implement location-level `try_files`, per-location filesystem aliases/roots, embedded scripting, arbitrary `if` expressions or arbitrary module directives. Static roots are defined only by reusable Web Services. Use the explicit UI controls and the documented JSON schema instead; unknown fields are rejected on save.
+1 -1
View File
@@ -1,6 +1,6 @@
# Stored log explorer # Stored log explorer
RustProxy 0.29.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. RustProxy 0.29.1 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent.
## Separation from Live Logs ## Separation from Live Logs
+1 -1
View File
@@ -1,4 +1,4 @@
# Installing and updating RustProxy 0.29.0 # Installing and updating RustProxy 0.29.1
The scripts prepare a Linux/systemd installation. This source archive is **not a The scripts prepare a Linux/systemd installation. This source archive is **not a
verified production build**: Rust/Cargo, real CA operations, live systemd startup verified production build**: Rust/Cargo, real CA operations, live systemd startup
+39 -1
View File
@@ -63,6 +63,7 @@ The normal ordered `index` list handles `index.php`; there is no separate PHP-in
### Additional PHP/FastCGI options ### Additional PHP/FastCGI options
- `path_info`: default-off secure script/path splitting. When enabled, `/index.php/foo/bar` may resolve the existing `index.php` script and pass `/foo/bar` as `PATH_INFO`.
- `pass_authorization`: forwards the inbound `Authorization` header as `HTTP_AUTHORIZATION`. Disable it when the application does not need that header. - `pass_authorization`: forwards the inbound `Authorization` header as `HTTP_AUTHORIZATION`. Disable it when the application does not need that header.
- `max_request_body_bytes`: streaming request-body limit enforced before/during FastCGI forwarding. `0` means no additional RustProxy-side limit. A request exceeding the configured value receives `413 Payload Too Large`. - `max_request_body_bytes`: streaming request-body limit enforced before/during FastCGI forwarding. `0` means no additional RustProxy-side limit. A request exceeding the configured value receives `413 Payload Too Large`.
- `max_response_header_bytes`: maximum bytes accepted before the CGI response-header terminator. The allowed range is 4096..1048576 bytes. - `max_response_header_bytes`: maximum bytes accepted before the CGI response-header terminator. The allowed range is 4096..1048576 bytes.
@@ -137,6 +138,7 @@ RustProxy supplies a controlled CGI/FastCGI parameter set including:
- `REQUEST_METHOD` - `REQUEST_METHOD`
- `SCRIPT_FILENAME` - `SCRIPT_FILENAME`
- `SCRIPT_NAME` - `SCRIPT_NAME`
- `PATH_INFO` when `path_info` is enabled and a trailing path was securely split from an existing PHP script
- `QUERY_STRING` - `QUERY_STRING`
- `REQUEST_URI` - `REQUEST_URI`
- `DOCUMENT_ROOT` - `DOCUMENT_ROOT`
@@ -157,7 +159,7 @@ Request headers cannot become FastCGI `PHP_VALUE` or `PHP_ADMIN_VALUE` parameter
An inbound `Proxy` header is **never** translated into FastCGI `HTTP_PROXY` (httpoxy mitigation), regardless of the optional Web Service hardening switch. The **Block common attack probes** option additionally blocks high-confidence sensitive/probe paths before PHP-FPM dispatch. FastCGI output cannot control downstream `Content-Length`, transfer framing, connection headers or `Proxy-*` transport headers; RustProxy owns downstream response framing. An inbound `Proxy` header is **never** translated into FastCGI `HTTP_PROXY` (httpoxy mitigation), regardless of the optional Web Service hardening switch. The **Block common attack probes** option additionally blocks high-confidence sensitive/probe paths before PHP-FPM dispatch. FastCGI output cannot control downstream `Content-Length`, transfer framing, connection headers or `Proxy-*` transport headers; RustProxy owns downstream response framing.
`PATH_INFO` script/path splitting is not implemented in the current 0.25 development stage; applications should route explicitly to a real script, normally through `try_files`. `PATH_INFO` splitting is optional and disabled by default. When enabled, RustProxy searches for an existing configured PHP script prefix inside the document root, keeps `SCRIPT_FILENAME`/`SCRIPT_NAME` on that script, and supplies only the remaining suffix as `PATH_INFO`. `REQUEST_URI` remains the original request target.
## Response parsing ## Response parsing
@@ -223,3 +225,39 @@ The implementation includes source/unit coverage for:
- bounded endpoint/time/concurrency configuration validation. - bounded endpoint/time/concurrency configuration validation.
Release-host acceptance should additionally exercise a real PHP-FPM runtime over Unix socket and loopback TCP and verify ACL/auth/custom errors/headers plus H1/H2/H3 client transports. Release-host acceptance should additionally exercise a real PHP-FPM runtime over Unix socket and loopback TCP and verify ACL/auth/custom errors/headers plus H1/H2/H3 client transports.
## Per-Web-Service PHP administrative INI overrides (0.29.2)
`php.php_admin_values` is a validated operator-managed string map sent in a single
`PHP_ADMIN_VALUE` FastCGI parameter. Example:
```json
"php": {
"enabled": true,
"path_info": true,
"endpoint": "unix:/run/php/php-fpm-mail.sock",
"php_admin_values": {
"open_basedir": "/var/www/webmail/:/tmp/",
"display_errors": "Off"
}
}
```
Use separate Web Services for distinct per-location PHP settings, then assign those
services to the corresponding Custom Locations. Each Web Service can refer to the
same trusted PHP-FPM socket. The setting does **not** isolate worker process
identities or file permissions. Never expose PHP-FPM FastCGI to untrusted clients.
HTTP headers cannot supply `PHP_ADMIN_VALUE`, and `php_values` must not repeat
an admin directive. Control characters and excessive values are rejected.
Prometheus metrics use only configured Web Service IDs, never script paths,
URLs, PHP values, or dynamic user labels. `rust_proxy_php_requests_total` and
`rust_proxy_php_responses_total{class=...}` count responses after headers have
been received (including proxy-generated failures); the cumulative header
latency metric intentionally excludes streamed body time.
The same snapshot also exports `rust_proxy_web_server_requests_total`,
`rust_proxy_web_server_responses_total{class=...}` and
`rust_proxy_web_server_response_header_duration_microseconds_total` for all
Web Server requests (PHP and non-PHP).
+2 -1
View File
@@ -15,6 +15,7 @@ This is the canonical index for the current project documentation. The authentic
- [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards. - [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards.
- [0.25.0 specification audit](V0.25.0_AUDIT.md) - Web Server foundation point-by-point implementation status. - [0.25.0 specification audit](V0.25.0_AUDIT.md) - Web Server foundation point-by-point implementation status.
- [0.25.0 pre-production security audit](V0.25.0_SECURITY_PREPROD_AUDIT.md) - threat-oriented source/configuration review, fixes made in-session and mandatory runtime security gates. - [0.25.0 pre-production security audit](V0.25.0_SECURITY_PREPROD_AUDIT.md) - threat-oriented source/configuration review, fixes made in-session and mandatory runtime security gates.
- [0.29.1 implementation note](V0.29.1_IMPLEMENTATION.md) - internal error fallbacks, multipart byte ranges and PHP PATH_INFO.
- [0.29.0 implementation note](V0.29.0_IMPLEMENTATION.md) - canonical host/custom-location redirects, health Host defaults, forced cache and gzip Stored Logs. - [0.29.0 implementation note](V0.29.0_IMPLEMENTATION.md) - canonical host/custom-location redirects, health Host defaults, forced cache and gzip Stored Logs.
- [0.28.2 implementation note](V0.28.2_IMPLEMENTATION.md) - external certificate ACL/path hardening and least-privilege Certificate API grants. - [0.28.2 implementation note](V0.28.2_IMPLEMENTATION.md) - external certificate ACL/path hardening and least-privilege Certificate API grants.
- [0.28.1 implementation note](V0.28.1_IMPLEMENTATION.md) - external certificate paths, Certbot symlink reloads, backup semantics and certificate-source UX. - [0.28.1 implementation note](V0.28.1_IMPLEMENTATION.md) - external certificate paths, Certbot symlink reloads, backup semantics and certificate-source UX.
@@ -75,7 +76,7 @@ This is the canonical index for the current project documentation. The authentic
## Historical or version-labelled context ## Historical or version-labelled context
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [V0.29.0_IMPLEMENTATION.md](V0.29.0_IMPLEMENTATION.md); [V0.28.2_IMPLEMENTATION.md](V0.28.2_IMPLEMENTATION.md) is retained as the prior certificate-hardening milestone; [V0.28.1_IMPLEMENTATION.md](V0.28.1_IMPLEMENTATION.md) is retained as the prior external-path milestone; [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md) is retained as the prior GeoIP/listener/cache milestone, and [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. 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.29.1_IMPLEMENTATION.md](V0.29.1_IMPLEMENTATION.md); [V0.29.0_IMPLEMENTATION.md](V0.29.0_IMPLEMENTATION.md) is retained as the prior redirect/health/cache/log milestone; [V0.28.2_IMPLEMENTATION.md](V0.28.2_IMPLEMENTATION.md) is retained as the prior certificate-hardening milestone; [V0.28.1_IMPLEMENTATION.md](V0.28.1_IMPLEMENTATION.md) is retained as the prior external-path milestone; [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md) is retained as the prior GeoIP/listener/cache milestone, and [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. 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.
+23 -75
View File
@@ -1,87 +1,35 @@
# Verification report - RustProxy 0.29.0 # Verification report - RustProxy 0.29.2
## Scope ## Changes
This report records verification of the 0.29.0 release source in the packaging environment. The release adds canonical www/non-www redirects, first-class Custom location redirects, automatic HTTP health-check Host selection, per-vhost forced-cache targets, bounded gzip log reading, and regression coverage for 503 maintenance pages and nested URL query strings. SQLite remains schema 9 and Cold Cache remains format 3. Same-version 0.29.2 correction: Web Server main routes no longer inherit `transient_unavailable` from the vhost backend configuration. When the Web Server source has no enabled Reverse Proxy Custom Locations, backend definitions are not compiled. An enabled reverse-proxy Custom Location still compiles and uses the backend pool. Removed fallback-only Prometheus metrics, retained PHP/Web Server metrics.
## Source and release checks Validated operator-controlled PHP_ADMIN_VALUE FastCGI configuration per Web Service,
with distinct PHP_VALUE/ADMIN directive sets, explicit panel controls and OpenAPI
schema. Prometheus PHP Web Service request/status counters, response header
latency and available concurrency permit gauge, plus Web Server
service-scoped response statuses and header latency.
The repository release gates are executed with: ## Acceptance limitations
```bash The build environment has no Rust compiler (`cargo`/`rustc`). Rust compilation,
./dev.sh test unit tests, FastCGI integration with a live PHP-FPM pool and runtime load tests
./dev.sh check have not been performed here. Rust-independent checks are recorded separately.
``` This source package is not certified for production deployment.
`./dev.sh test` runs the Rust-independent source/module, release-smoke, memory, Web Server, security, dependency, browser-UI, documentation and translation checks before attempting Cargo. The 0.29.0 source checks cover the new configuration/UI/runtime contracts as well as retained release behavior. ## Checks performed
Additional syntax checks used for the edited browser assets are: - Same-version correction: `python3 tools/checks/check_source.py` and `python3 tools/checks/release_smoke.py` passed. `bash ./dev.sh test` passed the Rust-independent checks and stopped at missing Rust/Cargo (exit 127). No live 503 regression test or Rust compilation could be performed.
```bash - `./dev.sh test`: all Rust-independent release, upgrade, security, JS, UI, documentation and source checks passed; exited 127 at the missing `cargo` toolchain guard.
node --check src/admin/web/vhosts/core.js - `node --check src/admin/web/web-server.js`: passed.
node --check src/admin/web/vhosts/editor.js - `python -m json.tool src/admin/openapi.json`: passed.
node --check src/admin/web/vhosts/render.js - `./dev.sh check`: exited 127 because Rust/Cargo is unavailable.
```
## Environment limitation ## Debian deployment maintenance (same 0.29.2)
Rust/Cargo is not installed or not available on PATH in this packaging environment. Therefore Rust parsing, type checking, linking and execution of `cargo test --locked --all-targets` cannot be claimed here. `./dev.sh test` was executed and all Rust-independent gates printed PASS before the script stopped at the explicit Rust/Cargo guard with exit 127. `./dev.sh check` was also executed and stopped at the same guard with exit 127. Installer/update now installs /etc/logrotate.d/rustproxy using the recorded service user/group. Updates repair /etc/profile.d/rustproxy-path.sh rather than warning. logrotate uses copytruncate because RustProxy holds open log descriptors; some log records may be lost during truncate. Rust build and live Debian/systemd acceptance remain unverified in this environment.
## Version identity (0.29.2 source maintenance)
Observed auxiliary checks: `Cargo.toml` package version is the canonical release identity. Rust uses `CARGO_PKG_VERSION` at build time; `build.rs` substitutes the admin HTML placeholder. Packaging, upgrade and dev scripts read the manifest with `scripts/version.py`. Run `python3 tools/checks/version_contracts.py` to check that executable paths do not hardcode a version. Historical documentation and external dashboards may retain manually managed versions. Rust compilation/runtime verification is still pending.
```text
node --check src/admin/web/vhosts/core.js PASS
node --check src/admin/web/vhosts/editor.js PASS
node --check src/admin/web/vhosts/render.js PASS
(cd docker-agent && go test ./...) PASS
```
An attempt to fetch a user-local Rust toolchain was unavailable from this environment, so no compiler result is inferred from the source/static checks.
A Rust-enabled release host must still run:
```bash
cargo fmt --all -- --check
cargo check --locked --all-targets
cargo test --locked --all-targets
```
before promoting the binary package.
## Functional regressions included in source
The Rust test source includes coverage for:
- www/non-www companion routing and canonical redirect behavior;
- default and explicitly overridden HTTP health-check Host selection;
- Custom location redirects with a separate capture regex and query preservation;
- per-vhost forced-cache target matching and retained protocol-safety exclusions;
- bounded discovery/read of rotated `.gz` Stored Logs;
- a static/maintenance HTML response with HTTP 503;
- preservation of a valid nested-URL query string in the upstream URI.
Literal/encoded backslashes remain intentionally rejected by the ambiguous-routing guard. For the reported curl case, use normal query separators inside a quoted URL rather than `\?`, `\=` or `\&`.
## Post-release source revision: multi-host vhost and canonical selector
The vhost editor now selects a canonical hostname from the configured host list, with a legacy first-host fallback. An additional Rust regression test verifies independent `suba.local` and `subb.local` aliases and a selected redirect target; invalid foreign redirect targets are rejected. Compiler/runtime tests remain unverified in this environment.
## Frontends WWW-routing UX revision
The editor groups host chips and WWW redirect controls in the same Frontends tab. The legacy select is replaced by accessible paired radio choices showing both `www -> bare` and `bare -> www` for each exact host (also while the feature is off), plus a list of non-redirected aliases and the number of configured names. A `www.` destination can be implicit: routing claims both hosts even if only the bare name was listed in the vhost. Wildcards are not offered as redirect targets. The selected target survives unrelated edits and automatically falls back to a valid hostname when removed; wildcard-only configurations disable the redirect control and show guidance. The persisted fields (`www_redirect` and `www_redirect_target`) are unchanged.
`node tools/checks/vhost_www_ui_test.mjs` runs the actual WWW-routing editor component in a DOM stand-in and covers its switch, selected-target restore, alias visibility, both redirect directions, implicit `www.` target, wildcard handling, removal fallback, and serialization. Rust source includes a complementary runtime regression for the implicit www routing pair. These checks do not substitute for executing Rust proxy runtime tests.
## WWW UX correction (source revision)
The Frontends tab now shows the WWW target choices next to its hostname editor, grouped by domain and available for preview even when the feature is disabled. Both www to bare and bare to www destinations are available. A WWW destination may be implicit (`www.subb.local` with only `subb.local` configured); the runtime claims both hostnames for the selected pair, while a second domain (`suba.local`) remains an independent alias of the same vhost and backend. A Rust regression test covers the implicit WWW direction; static checks and JS syntax tests are described above. Full Rust compiler/runtime verification still requires Cargo.
### WWW UX packaging verification
In this source revision, `node tools/checks/www_redirect_ui_test.mjs` and
`node tools/checks/vhost_www_ui_test.mjs` both pass their DOM-stand-in selection
and synchronization checks. `./dev.sh test` passed all pre-Cargo checks and
stopped at the missing Cargo guard with exit 127. Syntax checks for the edited
JavaScript and the two language JSON packs also passed. This does not establish
Rust compile/test success or end-to-end browser behavior.
+127
View File
@@ -0,0 +1,127 @@
# RustProxy 0.29.1 implementation note
RustProxy 0.29.1 adds explicit Nginx-style internal error routing without parsing Nginx configuration, plus multipart byte ranges and optional PHP `PATH_INFO` support. SQLite remains schema 9 and Cold Cache remains format 3.
## Internal error fallback
A vhost main source and each Custom location can define:
```json
"error_fallback": {
"target": "redirect-check",
"statuses": [404]
}
```
`target` is the stable `id` of an enabled Custom location in the same vhost with `"internal": true`. Internal locations are retained in the compiled route table but never participate in normal public URI matching. The supported intercepted status set is deliberately bounded to `403`, `404`, `500`, `502`, `503` and `504`; omitting `statuses` defaults to `404`.
The dispatch is internal, not a `3xx` client redirect. RustProxy rebuilds the fallback request with the original HTTP method, URI, query string, HTTP version, request headers and the existing transport/request context. The target location then executes through the normal vhost/location path, including ACL/authentication, header rules, cache policy, compression and response processing. Vhost rate limiting is not charged a second time for the same client request.
For requests with a body, RustProxy can replay a body only when its exact size is known and no larger than 8 MiB. Such a request is buffered once so the same bytes can be sent to the fallback target. Unknown-size or larger streaming bodies continue to the original target, but an error fallback cannot replay them; the original error response is retained.
Configuration validation rejects missing/public fallback targets and recursive fallback cycles. A runtime depth limit of 8 internal dispatches remains as a second guard against loops after hot-reload/state inconsistencies.
Configured upstream response statuses are intercepted before normal response finalization and cache publication. RustProxy-generated reverse-proxy failures also participate where applicable: missing backend (`500`), unavailable/capacity (`503`), connect failure (`503`), invalid upstream response (`502`) and response timeout (`504`). A configured stale cached response still wins before transport-error fallback.
## Example: static files -> application -> redirector
The following vhost fragment expresses the same request flow as an Nginx `try_files`/`error_page` chain, without an HTTP redirect between stages:
```json
{
"source": {
"type": "web_server",
"web_service_id": "www-linuxiarz"
},
"error_fallback": {
"target": "to8000",
"statuses": [404]
},
"backends": [
{
"id": "app",
"url": "http://127.0.0.1:8000",
"enabled": true,
"allow_private": true,
"address_allowlist": ["127.0.0.1/32"]
},
{
"id": "redirector",
"url": "http://127.0.0.1:9000",
"enabled": true,
"allow_private": true,
"address_allowlist": ["127.0.0.1/32"]
}
],
"routes": [
{
"id": "to8000",
"internal": true,
"backend_id": "app",
"source": {"type": "reverse_proxy"},
"error_fallback": {
"target": "redirect-check",
"statuses": [404]
}
},
{
"id": "redirect-check",
"internal": true,
"backend_id": "redirector",
"source": {"type": "reverse_proxy"},
"cache": false
}
]
}
```
The effective flow is:
```text
public request
-> Web Server
2xx/3xx -> return normally
404 -> @to8000 (internal)
-> backend app
non-404 -> return normally
404 -> @redirect-check (internal)
-> redirector backend
```
Normal public Custom locations can still route `/stats`, `/user-info`, `/health`, RBL files or other paths directly before the main source is used. An internal location's `path_prefix` is not used for public matching; its `id` is the fallback name.
## Multipart byte ranges
The Web Server now accepts comma-separated byte ranges such as:
```http
Range: bytes=0-99,200-299
```
One satisfiable range retains the existing single-part `206` response. Multiple satisfiable ranges produce a streaming `multipart/byteranges` response with per-part `Content-Type` and `Content-Range`. At most 16 ranges are accepted. Malformed or entirely unsatisfiable range sets return `416`. Multipart responses use the identity file representation rather than `.br`/`.gz` sidecars so the multipart MIME framing and `Content-Encoding` semantics cannot conflict.
## PHP PATH_INFO
Web Service PHP configuration gains the default-off field:
```json
"php": {
"enabled": true,
"path_info": true
}
```
When enabled, RustProxy may resolve a request such as `/index.php/foo/bar` by finding the longest existing configured PHP script prefix under the Web Service document root. FastCGI then receives, conceptually:
```text
SCRIPT_FILENAME=/document/root/index.php
SCRIPT_NAME=/index.php
PATH_INFO=/foo/bar
REQUEST_URI=/index.php/foo/bar
```
The split uses the same document-root confinement and PHP file checks as normal script resolution. It is disabled by default because it changes URL-to-script resolution semantics.
## Compatibility
No SQLite migration is required. Existing vhosts, locations and Web Services deserialize unchanged because `internal`, `error_fallback` and PHP `path_info` use backward-compatible defaults. Existing single-range responses are retained.
+6
View File
@@ -38,6 +38,12 @@ health/traffic history. Because the vhost configuration participates in cache co
a rename also rotates that compatibility identity and invalidates incompatible cached objects. a rename also rotates that compatibility identity and invalidates incompatible cached objects.
The editor warns before applying the rename. The editor warns before applying the rename.
## Internal error routing
The main vhost `source` may define `error_fallback`, and each Custom location may do the same. A fallback references the `id` of an enabled `internal` Custom location in the same vhost. Supported statuses are `403`, `404`, `500`, `502`, `503` and `504`; omitted status lists default to `404`. Internal targets are not reachable by ordinary path matching.
Configured upstream/Web Server failures are intercepted before final response/cache publication. The original request method, URI/query and context are internally dispatched to the target, which then follows normal vhost/location processing. Validation rejects fallback cycles and runtime dispatch is depth-limited. See [V0.29.1_IMPLEMENTATION.md](V0.29.1_IMPLEMENTATION.md) for body replay bounds and an Nginx-style static -> app -> redirector example.
## Frontend selection and private IP isolation ## Frontend selection and private IP isolation
`frontend.hosts` is a normalized hostname-pattern list. Exact hostnames and a single `frontend.hosts` is a normalized hostname-pattern list. Exact hostnames and a single
+5 -4
View File
@@ -217,20 +217,21 @@ Optional precompressed delivery checks sidecar files only after the original res
The client must advertise the matching coding in `Accept-Encoding`. Brotli is preferred when both are available. The response keeps the original file's MIME type and adds the appropriate `Content-Encoding` and `Vary: Accept-Encoding`. The client must advertise the matching coding in `Accept-Encoding`. Brotli is preferred when both are available. The response keeps the original file's MIME type and adds the appropriate `Content-Encoding` and `Vary: Accept-Encoding`.
Validators, streaming and a single Range request operate on the selected representation. RustProxy does not compress the file on every request in this mode. Validators, streaming and a single Range request operate on the selected representation. Multipart Range responses use the identity representation so MIME boundaries are never mixed with a precompressed sidecar. RustProxy does not compress the file on every request in this mode.
## Range and downloads ## Range and downloads
Static and precompressed responses support: Static responses support:
- single `Range: bytes=...`; - single `Range: bytes=...`;
- multipart `Range: bytes=0-99,200-299` with at most 16 ranges;
- `If-Range`; - `If-Range`;
- `206 Partial Content`; - `206 Partial Content`;
- `416 Range Not Satisfiable`; - `416 Range Not Satisfiable`;
- `Accept-Ranges` and `Content-Range`; - `Accept-Ranges` and per-part `Content-Range`;
- streaming rather than whole-file buffering. - streaming rather than whole-file buffering.
Multipart ranges remain intentionally unsupported. Single ranges can operate on a selected precompressed representation as before. Multipart ranges intentionally use the identity representation and stream `multipart/byteranges`.
Configured `download_extensions` add `Content-Disposition: attachment` using an encoded filename. This is useful for storage/download services without changing the MIME type. Configured `download_extensions` add `Content-Disposition: attachment` using an encoded filename. This is useful for storage/download services without changing the MIME type.
+1 -1
View File
@@ -19,7 +19,7 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw
are intentionally absent from Prometheus labels. are intentionally absent from Prometheus labels.
Import the JSON in Grafana and select a Prometheus datasource when prompted. The Import the JSON in Grafana and select a Prometheus datasource when prompted. The
dashboard expects RustProxy 0.29.0 metric names and a scrape interval around 15s. dashboard expects RustProxy 0.29.2 metric names and a scrape interval around 15s.
Collectors can be disabled in RustProxy settings; panels for disabled collectors Collectors can be disabled in RustProxy settings; panels for disabled collectors
will simply show no series. will simply show no series.
+2 -2
View File
@@ -4529,9 +4529,9 @@
}, },
"timepicker": {}, "timepicker": {},
"timezone": "browser", "timezone": "browser",
"title": "RustProxy 0.29.0 - Operations", "title": "RustProxy 0.29.2 - Operations",
"uid": "rustproxy-0170", "uid": "rustproxy-0170",
"version": 5, "version": 5,
"weekStart": "", "weekStart": "",
"description": "RustProxy 0.29.0 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.29.2 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
} }
+1 -1
View File
@@ -73,7 +73,7 @@ echo "RustProxy bootstrap mode: $MODE"
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
apt-get update apt-get update
apt-get install -y build-essential gcc git ca-certificates python3 python3-venv python3-pip pkg-config wget sudo apt-get install -y build-essential gcc git ca-certificates python3 python3-venv python3-pip pkg-config wget sudo logrotate
created_builder=0 created_builder=0
if [[ -n ${SUDO_USER:-} && ${SUDO_USER} != root ]] && id "${SUDO_USER}" >/dev/null 2>&1; then if [[ -n ${SUDO_USER:-} && ${SUDO_USER} != root ]] && id "${SUDO_USER}" >/dev/null 2>&1; then
builder=${SUDO_USER} builder=${SUDO_USER}
+10 -2
View File
@@ -1825,7 +1825,7 @@
"webServicePrecompressedGzip": "Gzip (.gz)", "webServicePrecompressedGzip": "Gzip (.gz)",
"webServiceDownloadExtensions": "Download extensions", "webServiceDownloadExtensions": "Download extensions",
"webServiceDownloadExtensionsHelp": "Files with these extensions receive Content-Disposition: attachment.", "webServiceDownloadExtensionsHelp": "Files with these extensions receive Content-Disposition: attachment.",
"webServiceRangeHelp": "Static and precompressed representations keep validators, single-range requests, 206/416 and streaming delivery.", "webServiceRangeHelp": "Static and precompressed representations keep validators, single and multipart byte-range requests, 206/416 and streaming delivery.",
"webServicePhpEnabled": "Enable PHP-FPM / FastCGI", "webServicePhpEnabled": "Enable PHP-FPM / FastCGI",
"webServicePhpEndpoint": "PHP-FPM endpoint", "webServicePhpEndpoint": "PHP-FPM endpoint",
"webServicePhpEndpointHelp": "Unix: unix:/run/php/php-fpm.sock. TCP: tcp:127.0.0.1:9000 or tcp:[::1]:9000. TCP is intentionally limited to loopback addresses.", "webServicePhpEndpointHelp": "Unix: unix:/run/php/php-fpm.sock. TCP: tcp:127.0.0.1:9000 or tcp:[::1]:9000. TCP is intentionally limited to loopback addresses.",
@@ -2227,6 +2227,14 @@
"vhWwwOtherDomains": "Other domains (no redirect):", "vhWwwOtherDomains": "Other domains (no redirect):",
"vhWwwNoHosts": "Add an exact hostname above to enable a WWW redirect. Wildcards cannot be the destination.", "vhWwwNoHosts": "Add an exact hostname above to enable a WWW redirect. Wildcards cannot be the destination.",
"vhWwwSharedCount": "{count} names / one vhost", "vhWwwSharedCount": "{count} names / one vhost",
"vhWwwEnable": "Enable 308 redirect" "vhWwwEnable": "Enable 308 redirect",
"vhErrorFallbackTarget": "Main error fallback target",
"vhErrorFallbackStatuses": "Intercepted status codes",
"vhErrorFallbackHelp": "Target must be an enabled internal Custom Location. Supported statuses: 403, 404, 500, 502, 503, 504. The request is dispatched internally without an HTTP redirect.",
"locationInternal": "Internal named location",
"locationErrorFallbackTarget": "Error fallback target",
"locationErrorFallbackStatuses": "Intercepted status codes",
"locationErrorFallbackHelp": "Optional Nginx-style error fallback. Target an enabled internal location by ID. Supported statuses: 403, 404, 500, 502, 503, 504.",
"webServicePhpPathInfo": "Enable PHP PATH_INFO splitting"
} }
} }
+10 -2
View File
@@ -1825,7 +1825,7 @@
"webServicePrecompressedGzip": "Gzip (.gz)", "webServicePrecompressedGzip": "Gzip (.gz)",
"webServiceDownloadExtensions": "Rozszerzenia do pobierania", "webServiceDownloadExtensions": "Rozszerzenia do pobierania",
"webServiceDownloadExtensionsHelp": "Pliki z tymi rozszerzeniami otrzymają Content-Disposition: attachment.", "webServiceDownloadExtensionsHelp": "Pliki z tymi rozszerzeniami otrzymają Content-Disposition: attachment.",
"webServiceRangeHelp": "Statyczne i gotowe skompresowane reprezentacje zachowują walidatory, pojedynczy Range, 206/416 i streaming.", "webServiceRangeHelp": "Statyczne i prekompresowane reprezentacje zachowują walidatory, pojedyncze i wieloczęściowe zakresy bajtów, odpowiedzi 206/416 oraz streaming.",
"webServicePhpEnabled": "Włącz PHP-FPM / FastCGI", "webServicePhpEnabled": "Włącz PHP-FPM / FastCGI",
"webServicePhpEndpoint": "Endpoint PHP-FPM", "webServicePhpEndpoint": "Endpoint PHP-FPM",
"webServicePhpEndpointHelp": "Unix: unix:/run/php/php-fpm.sock. TCP: tcp:127.0.0.1:9000 albo tcp:[::1]:9000. TCP jest celowo ograniczone do adresów loopback.", "webServicePhpEndpointHelp": "Unix: unix:/run/php/php-fpm.sock. TCP: tcp:127.0.0.1:9000 albo tcp:[::1]:9000. TCP jest celowo ograniczone do adresów loopback.",
@@ -2227,6 +2227,14 @@
"vhWwwOtherDomains": "Pozostałe domeny (bez przekierowania):", "vhWwwOtherDomains": "Pozostałe domeny (bez przekierowania):",
"vhWwwNoHosts": "Dodaj powyżej konkretną domenę. Wildcard nie może być adresem docelowym.", "vhWwwNoHosts": "Dodaj powyżej konkretną domenę. Wildcard nie może być adresem docelowym.",
"vhWwwSharedCount": "{count} nazw / jeden vhost", "vhWwwSharedCount": "{count} nazw / jeden vhost",
"vhWwwEnable": "Włącz przekierowanie 308" "vhWwwEnable": "Włącz przekierowanie 308",
"vhErrorFallbackTarget": "Główny cel fallbacku błędów",
"vhErrorFallbackStatuses": "Przechwytywane kody statusu",
"vhErrorFallbackHelp": "Cel musi być włączoną wewnętrzną Custom Location. Obsługiwane statusy: 403, 404, 500, 502, 503, 504. Request jest przekazywany wewnętrznie, bez redirectu HTTP.",
"locationInternal": "Wewnętrzna nazwana lokalizacja",
"locationErrorFallbackTarget": "Cel fallbacku błędów",
"locationErrorFallbackStatuses": "Przechwytywane kody statusu",
"locationErrorFallbackHelp": "Opcjonalny fallback błędów w stylu Nginx. Wskaż ID włączonej lokalizacji wewnętrznej. Obsługiwane statusy: 403, 404, 500, 502, 503, 504.",
"webServicePhpPathInfo": "Włącz rozdzielanie PHP PATH_INFO"
} }
} }
+2 -1
View File
@@ -9,6 +9,7 @@ import os
from pathlib import Path from pathlib import Path
import stat import stat
import zipfile import zipfile
from scripts.version import project_version
ROOT = Path(__file__).resolve().parent ROOT = Path(__file__).resolve().parent
MANAGED = [ MANAGED = [
@@ -86,7 +87,7 @@ def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.29.0.zip') parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / f'RustProxy-{project_version(ROOT)}.zip')
output = parser.parse_args().output.resolve() output = parser.parse_args().output.resolve()
paths = files() paths = files()
checksum = ROOT / 'SHA256SUMS' checksum = ROOT / 'SHA256SUMS'
+1 -1
View File
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
"""TLS verification uses Python's default trust store; never executes a shell.""" """TLS verification uses Python's default trust store; never executes a shell."""
started = time.monotonic() started = time.monotonic()
opener = build_opener(HttpsRedirects()) opener = build_opener(HttpsRedirects())
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.29.0"}) request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build"})
with opener.open(request, timeout=30) as response, destination.open("wb") as output: with opener.open(request, timeout=30) as response, destination.open("wb") as output:
length = response.headers.get("Content-Length") length = response.headers.get("Content-Length")
if length and int(length) > MAX_ARCHIVE: if length and int(length) > MAX_ARCHIVE:
+2 -1
View File
@@ -50,7 +50,8 @@ def main() -> None:
'start_service':not args.no_start, 'start_service':not args.no_start,
'geoip_action':'skip' if args.skip_geoip else ('download-and-verify' if args.mode == 'install' else 'verify-and-refresh-if-stale'), 'geoip_action':'skip' if args.skip_geoip else ('download-and-verify' if args.mode == 'install' else 'verify-and-refresh-if-stale'),
'shell_path_profile':str(SHELL_PATH_PROFILE), 'shell_path_profile':str(SHELL_PATH_PROFILE),
'shell_path_action':'install' if args.mode == 'install' else 'check-only', 'shell_path_action':'install-or-repair',
'logrotate_path':str(LOGROTATE_PATH), 'logrotate_action':'install-or-repair',
'reset_saved_config':bool(args.reset_config), 'changes_made':False}, indent=2)); return 'reset_saved_config':bool(args.reset_config), 'changes_made':False}, indent=2)); return
if sys.platform != 'linux' or os.geteuid() != 0 or not shutil.which('systemctl') or not shutil.which('runuser'): if sys.platform != 'linux' or os.geteuid() != 0 or not shutil.which('systemctl') or not shutil.which('runuser'):
raise ValueError('Installation requires root on Linux with systemd and runuser. Use --plan for inspection.') raise ValueError('Installation requires root on Linux with systemd and runuser. Use --plan for inspection.')
+20 -1
View File
@@ -34,6 +34,7 @@ import urllib.parse
ROOT = Path(__file__).resolve().parents[2] ROOT = Path(__file__).resolve().parents[2]
UNIT_PATH = Path('/etc/systemd/system/rustproxy.service') UNIT_PATH = Path('/etc/systemd/system/rustproxy.service')
SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh') SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh')
LOGROTATE_PATH = Path('/etc/logrotate.d/rustproxy')
SERVICE = 'rustproxy.service' SERVICE = 'rustproxy.service'
GEOIP_SOURCES = { GEOIP_SOURCES = {
'user-country-ipv4.csv': 'https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv4.csv', 'user-country-ipv4.csv': 'https://github.com/sapics/ip-location-db/releases/download/latest/user-country-ipv4.csv',
@@ -95,7 +96,7 @@ def digest(path: Path) -> str:
def fetch_https(url: str, max_bytes: int) -> bytes: def fetch_https(url: str, max_bytes: int) -> bytes:
if urllib.parse.urlparse(url).scheme != 'https': if urllib.parse.urlparse(url).scheme != 'https':
raise ValueError('GeoIP downloads require HTTPS.') raise ValueError('GeoIP downloads require HTTPS.')
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.29.0'}) request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer'})
with urllib.request.urlopen(request, timeout=30) as response: with urllib.request.urlopen(request, timeout=30) as response:
if urllib.parse.urlparse(response.geturl()).scheme != 'https': if urllib.parse.urlparse(response.geturl()).scheme != 'https':
raise ValueError('GeoIP download redirected away from HTTPS.') raise ValueError('GeoIP download redirected away from HTTPS.')
@@ -301,6 +302,24 @@ def install_shell_path(prefix: Path) -> None:
write_private(SHELL_PATH_PROFILE, shell_path_profile(prefix), 0o644) write_private(SHELL_PATH_PROFILE, shell_path_profile(prefix), 0o644)
def logrotate_config(user: str, group: str) -> bytes:
# User/group are validated system account names, never interpolated from a request.
for value in (user, group):
if not re.fullmatch(r'[a-z_][a-z0-9_-]*\$?', value):
raise ValueError('Unsafe service account for logrotate configuration.')
return (f"# Managed by RustProxy installer; service account: {user}:{group}\n"
'/var/log/rustproxy/*.jsonl /var/log/rustproxy/*.log /var/log/rustproxy/vhosts/*.jsonl /var/log/rustproxy/vhosts/*.log {\n'
' daily\n maxsize 100M\n rotate 14\n missingok\n notifempty\n'
' compress\n delaycompress\n copytruncate\n'
f' su {user} {group}\n'
'}\n').encode()
def install_logrotate(record: dict) -> None:
"""Keep logrotate in sync with the actual managed service account."""
write_private(LOGROTATE_PATH, logrotate_config(record['user'], record['group']), 0o644)
def check_shell_path(prefix: Path) -> bool: def check_shell_path(prefix: Path) -> bool:
expected = shell_path_profile(prefix) expected = shell_path_profile(prefix)
try: try:
+5 -2
View File
@@ -359,8 +359,8 @@ def update(record: dict, binary: Path, sha: str, start: bool, refresh_geoip: boo
geoip_lock_fd: int | None = None geoip_lock_fd: int | None = None
geoip_stage: Path | None = None geoip_stage: Path | None = None
try: try:
# Updates deliberately do not rewrite shell environment. Check the installer- # Repair missing or stale installer-owned shell setup on upgrades.
# managed PATH profile early and report the exact remediation target instead. install_shell_path(prefix)
check_shell_path(prefix) check_shell_path(prefix)
# Repair stale root:root/0700 metadata before staging/network work and before # Repair stale root:root/0700 metadata before staging/network work and before
# stopping the currently healthy service. # stopping the currently healthy service.
@@ -412,6 +412,8 @@ def update(record: dict, binary: Path, sha: str, start: bool, refresh_geoip: boo
if not healthy(record, record['version']): 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}. 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 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
install_shell_path(prefix)
install_logrotate(replacement)
print(f'Backup (contains private keys): {backup}') print(f'Backup (contains private keys): {backup}')
return replacement return replacement
finally: finally:
@@ -443,6 +445,7 @@ def install(record: dict, binary: Path, sha: str, start: bool, install_geoip: bo
record.update(release=release, version=version, sha256=sha) record.update(release=release, version=version, sha256=sha)
switch(prefix, release) switch(prefix, release)
install_shell_path(prefix) install_shell_path(prefix)
install_logrotate(record)
# Record is written even if initial startup fails, so an update can repair it. # Record is written even if initial startup fails, so an update can repair it.
write_private(prefix / 'install.json', json.dumps(record, indent=2).encode()) write_private(prefix / 'install.json', json.dumps(record, indent=2).encode())
write_private(UNIT_PATH, service_unit(record).encode(), 0o644) write_private(UNIT_PATH, service_unit(record).encode(), 0o644)
+5 -3
View File
@@ -8,6 +8,8 @@ from pathlib import Path
import shutil import shutil
import sys import sys
import uuid import uuid
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from scripts.version import project_version
MANAGED = ["build.rs", "langs", "deploy", "debian", "grafana", "make_zip.py", "src", "tests", "scripts", "tools", "integrations", "nagios", "docker-agent", "migrations", "examples", "docs", ".github", ".cargo", MANAGED = ["build.rs", "langs", "deploy", "debian", "grafana", "make_zip.py", "src", "tests", "scripts", "tools", "integrations", "nagios", "docker-agent", "migrations", "examples", "docs", ".github", ".cargo",
"Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "dev.sh", "upgrade.sh", "install.sh", "update.sh", "Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "dev.sh", "upgrade.sh", "install.sh", "update.sh",
@@ -46,7 +48,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
for name in MANAGED: for name in MANAGED:
if (target / name).is_symlink(): if (target / name).is_symlink():
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.") raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
backup = target / ".upgrade-backup" / ("before-0.29.0-" + uuid.uuid4().hex[:12]) backup = target / ".upgrade-backup" / ("before-" + project_version(target) + "-" + uuid.uuid4().hex[:12])
backup.mkdir(parents=True, mode=0o700) backup.mkdir(parents=True, mode=0o700)
installed: list[str] = [] installed: list[str] = []
moved: list[str] = [] moved: list[str] = []
@@ -84,7 +86,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
remove_legacy = legacy.exists() remove_legacy = legacy.exists()
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()): if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
raise ValueError("Obsolete renewal observation sidecar must be a regular file.") raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.29.0", "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":project_version(source), "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
if remove_legacy: if remove_legacy:
legacy.unlink() legacy.unlink()
except BaseException: except BaseException:
@@ -115,7 +117,7 @@ def main() -> None:
backup = upgrade(Path(__file__).resolve().parents[2], args.target, stopped=args.stopped, data_dir=data_dir) backup = upgrade(Path(__file__).resolve().parents[2], args.target, stopped=args.stopped, data_dir=data_dir)
except (OSError, ValueError) as error: except (OSError, ValueError) as error:
parser.exit(2, f"Upgrade refused or rolled back: {error}\n") parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
print(f"Installed RustProxy 0.29.0. Previous managed source files: {backup}") print(f"Installed RustProxy {project_version(args.target)}. Previous managed source files: {backup}")
print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.") print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.")
print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh") print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh")
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env python3
"""Read the canonical RustProxy release version from Cargo.toml."""
from pathlib import Path
import sys
import tomllib
def project_version(root: Path | None = None) -> str:
manifest = (root or Path(__file__).resolve().parents[1]) / "Cargo.toml"
data = tomllib.loads(manifest.read_text(encoding="utf-8"))
return data["package"]["version"]
if __name__ == "__main__":
print(project_version(Path(sys.argv[1]) if len(sys.argv) > 1 else None))
+1
View File
@@ -409,6 +409,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
"/docs/V0.25.1_AUDIT.md" "/docs/V0.25.1_AUDIT.md"
)), )),
), ),
reader_doc!("docs/V0.29.1_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.29.0_IMPLEMENTATION.md", "technical"), reader_doc!("docs/V0.29.0_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.28.2_IMPLEMENTATION.md", "technical"), reader_doc!("docs/V0.28.2_IMPLEMENTATION.md", "technical"),
reader_doc!("docs/V0.28.1_IMPLEMENTATION.md", "technical"), reader_doc!("docs/V0.28.1_IMPLEMENTATION.md", "technical"),
+77 -2
View File
@@ -2,7 +2,7 @@
"openapi": "3.0.3", "openapi": "3.0.3",
"info": { "info": {
"title": "RustProxy Administration API", "title": "RustProxy Administration API",
"version": "0.29.0", "version": "0.29.2",
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust." "description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
}, },
"servers": [ "servers": [
@@ -412,6 +412,19 @@
"target" "target"
], ],
"description": "Backend-less client redirect for this location." "description": "Backend-less client redirect for this location."
},
"internal": {
"type": "boolean",
"default": false,
"description": "Internal named location excluded from public URI matching; reachable only by internal dispatch."
},
"error_fallback": {
"allOf": [
{
"$ref": "#/components/schemas/ErrorFallback"
}
],
"nullable": true
} }
}, },
"additionalProperties": false, "additionalProperties": false,
@@ -438,7 +451,10 @@
"description": "Claim the direct www/non-www companion of the selected exact DNS hostname and redirect it permanently to that hostname." "description": "Claim the direct www/non-www companion of the selected exact DNS hostname and redirect it permanently to that hostname."
}, },
"www_redirect_target": { "www_redirect_target": {
"type": ["string", "null"], "type": [
"string",
"null"
],
"description": "An exact hostname from frontend.hosts to use as the canonical redirect destination. Null uses the first hostname for compatibility. Other hostnames continue serving the same vhost without redirects." "description": "An exact hostname from frontend.hosts to use as the canonical redirect destination. Null uses the first hostname for compatibility. Other hostnames continue serving the same vhost without redirects."
}, },
"forward_client_ip": { "forward_client_ip": {
@@ -598,6 +614,15 @@
}, },
"compression": { "compression": {
"$ref": "#/components/schemas/CompressionOverrides" "$ref": "#/components/schemas/CompressionOverrides"
},
"error_fallback": {
"allOf": [
{
"$ref": "#/components/schemas/ErrorFallback"
}
],
"nullable": true,
"description": "Error fallback for the main vhost source."
} }
}, },
"additionalProperties": true, "additionalProperties": true,
@@ -5084,6 +5109,21 @@
"maxLength": 8192 "maxLength": 8192
}, },
"description": "Trusted php.ini directive/value overrides combined into the FastCGI PHP_VALUE parameter. PHP_ADMIN_VALUE is not exposed." "description": "Trusted php.ini directive/value overrides combined into the FastCGI PHP_VALUE parameter. PHP_ADMIN_VALUE is not exposed."
},
"path_info": {
"type": "boolean",
"default": false,
"description": "Safely split /script.php/extra/path into SCRIPT_FILENAME/SCRIPT_NAME for the existing script plus PATH_INFO."
},
"php_admin_values": {
"type": "object",
"maxProperties": 64,
"default": {},
"additionalProperties": {
"type": "string",
"maxLength": 8192
},
"description": "Trusted operator PHP-FPM administrative php.ini directives packed into PHP_ADMIN_VALUE. Restrict access to FastCGI endpoints."
} }
}, },
"description": "Optional bounded direct FastCGI connection to a PHP-FPM pool." "description": "Optional bounded direct FastCGI connection to a PHP-FPM pool."
@@ -5574,6 +5614,41 @@
"updated_files", "updated_files",
"database" "database"
] ]
},
"ErrorFallback": {
"type": "object",
"additionalProperties": false,
"properties": {
"target": {
"type": "string",
"maxLength": 64,
"description": "Enabled internal Custom Location ID in the same vhost."
},
"statuses": {
"type": "array",
"minItems": 1,
"maxItems": 6,
"uniqueItems": true,
"default": [
404
],
"items": {
"type": "integer",
"enum": [
403,
404,
500,
502,
503,
504
]
}
}
},
"required": [
"target"
],
"description": "Nginx-style internal error fallback. The original request is internally dispatched without a client redirect."
} }
} }
}, },
+7 -1
View File
@@ -2,10 +2,16 @@
use crate::{auth::Session, runtime::ApiError}; use crate::{auth::Session, runtime::ApiError};
use axum::{extract::rejection::JsonRejection, response::Html, Extension}; use axum::{extract::rejection::JsonRejection, response::Html, Extension};
use http::StatusCode; use http::StatusCode;
use std::sync::OnceLock;
static ADMIN_INDEX: OnceLock<String> = OnceLock::new();
pub(super) async fn index(session: Option<Extension<Session>>) -> Html<&'static str> { pub(super) async fn index(session: Option<Extension<Session>>) -> Html<&'static str> {
if session.is_some() { if session.is_some() {
Html(include_str!("web/index.html")) Html(ADMIN_INDEX.get_or_init(|| {
include_str!("web/index.html")
.replace("__RUSTPROXY_VERSION__", env!("CARGO_PKG_VERSION"))
}).as_str())
} else { } else {
Html(include_str!("web/login.html")) Html(include_str!("web/login.html"))
} }
+6 -4
View File
@@ -2891,7 +2891,7 @@
</section> </section>
</div> </div>
</section> </section>
<footer class="page-footer"><span>RustProxy <b>0.29.0</b></span><span id="footer-status">API - / <footer class="page-footer"><span>RustProxy <b>__RUSTPROXY_VERSION__</b></span><span id="footer-status">API - /
dropped: 0</span></footer> dropped: 0</span></footer>
</div> </div>
</main> </main>
@@ -3335,6 +3335,8 @@
id="web-service-php-extensions" maxlength="512" placeholder="php" /></label> id="web-service-php-extensions" maxlength="512" placeholder="php" /></label>
<label class="check"><input id="web-service-php-pass-authorization" type="checkbox" /><span <label class="check"><input id="web-service-php-pass-authorization" type="checkbox" /><span
data-i18n="webServicePhpPassAuthorization">Forward Authorization header</span></label> data-i18n="webServicePhpPassAuthorization">Forward Authorization header</span></label>
<label class="check"><input id="web-service-php-path-info" type="checkbox" /><span
data-i18n="webServicePhpPathInfo">Enable PHP PATH_INFO splitting</span></label>
<label><span data-i18n="webServicePhpConnectTimeout">Connect timeout (ms)</span><input <label><span data-i18n="webServicePhpConnectTimeout">Connect timeout (ms)</span><input
id="web-service-php-connect-timeout" min="50" max="60000" step="1" type="number" /></label> id="web-service-php-connect-timeout" min="50" max="60000" step="1" type="number" /></label>
<label><span data-i18n="webServicePhpRequestTimeout">Total FastCGI request timeout (ms)</span><input <label><span data-i18n="webServicePhpRequestTimeout">Total FastCGI request timeout (ms)</span><input
@@ -3360,8 +3362,8 @@
spellcheck="false" spellcheck="false"
placeholder="memory_limit = 256M&#10;upload_max_filesize = 64M&#10;post_max_size = 64M"></textarea><small placeholder="memory_limit = 256M&#10;upload_max_filesize = 64M&#10;post_max_size = 64M"></textarea><small
class="fine" data-i18n="webServicePhpValuesHelp">One php.ini directive = value per line. class="fine" data-i18n="webServicePhpValuesHelp">One php.ini directive = value per line.
RustProxy sends the validated set as PHP_VALUE; PHP_ADMIN_VALUE is never accepted RustProxy sends the validated set as PHP_VALUE; Use the separate operator-only PHP_ADMIN_VALUE field for administrative overrides.</small></label>
here.</small></label> <label class="vh-wide"><span>PHP administrator INI overrides (PHP_ADMIN_VALUE)</span><textarea id="web-service-php-admin-values" maxlength="65536" rows="6" spellcheck="false" placeholder="open_basedir = /var/www/webmail/:/tmp/&#10;display_errors = Off"></textarea><small class="fine">Trusted operator settings only. Restrict PHP-FPM socket/network access; do not use shared PHP-FPM pools as a security boundary.</small></label>
<div class="vh-wide form-actions"><button class="outline" data-i18n="webServicePhpDocsButton" <div class="vh-wide form-actions"><button class="outline" data-i18n="webServicePhpDocsButton"
id="web-service-php-help" type="button">FastCGI syntax and safety</button></div> id="web-service-php-help" type="button">FastCGI syntax and safety</button></div>
<p class="fine vh-wide" data-i18n="webServicePhpHelp">RustProxy speaks FastCGI directly to a <p class="fine vh-wide" data-i18n="webServicePhpHelp">RustProxy speaks FastCGI directly to a
@@ -3635,7 +3637,7 @@ max_execution_time = 30</code></pre>
<article class="card about-card"> <article class="card about-card">
<div class="about-product"> <div class="about-product">
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span> <span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
<div><span class="small-label">RustProxy</span><strong>0.29.0</strong></div> <div><span class="small-label">RustProxy</span><strong>__RUSTPROXY_VERSION__</strong></div>
</div> </div>
<dl class="about-meta"> <dl class="about-meta">
<div> <div>
+13 -5
View File
@@ -47,7 +47,7 @@
frontend: { hosts: [], listener_ids: [] }, frontend: { hosts: [], listener_ids: [] },
source: { type: "reverse_proxy" }, source: { type: "reverse_proxy" },
backends: [{ id: "origin-1", url: "", weight: 1, enabled: true, pool_eligible: true, allow_private: true, accept_local_listener_risk: false, preserve_host: true, protocol: "http1", tls_verify: false }], backends: [{ id: "origin-1", url: "", weight: 1, enabled: true, pool_eligible: true, allow_private: true, accept_local_listener_risk: false, preserve_host: true, protocol: "http1", tls_verify: false }],
cache: { enabled: false }, compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, transform: { enabled: false }, rate_limit: {}, method_policy: { mode: "off", methods: [] }, routes: [], headers: {}, acl_profile_id: null, acl: null, tls_policy: null, proxy_timeouts: defaultProxyTimeouts() error_fallback: null, cache: { enabled: false }, compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, transform: { enabled: false }, rate_limit: {}, method_policy: { mode: "off", methods: [] }, routes: [], headers: {}, acl_profile_id: null, acl: null, tls_policy: null, proxy_timeouts: defaultProxyTimeouts()
}); });
function activateTab(name) { document.querySelectorAll("#vhost-tabs button").forEach(b => { const active = b.dataset.tab === name; b.classList.toggle("active", active); b.setAttribute("aria-pressed", String(active)); b.setAttribute("aria-selected", String(active)); b.tabIndex = active ? 0 : -1; }); container.querySelectorAll(".vh-group").forEach(g => g.hidden = g.dataset.tab !== name); } function activateTab(name) { document.querySelectorAll("#vhost-tabs button").forEach(b => { const active = b.dataset.tab === name; b.classList.toggle("active", active); b.setAttribute("aria-pressed", String(active)); b.setAttribute("aria-selected", String(active)); b.tabIndex = active ? 0 : -1; }); container.querySelectorAll(".vh-group").forEach(g => g.hidden = g.dataset.tab !== name); }
@@ -313,8 +313,10 @@
return [...rows.children].map(r => { return [...rows.children].map(r => {
const action = str("location-action", r), rewriteMode = str("location-rewrite-mode", r); const action = str("location-action", r), rewriteMode = str("location-rewrite-mode", r);
const handled = action === "proxy" || action === "web_server"; const handled = action === "proxy" || action === "web_server";
const internal = yes("location-internal", r), fallbackTarget = str("location-error-target", r);
const fallbackStatuses = lines(str("location-error-statuses", r)).map(value => Number(value));
return { return {
id: str("location-id", r), path_prefix: str("location-path", r), match_type: str("location-match", r), id: str("location-id", r), internal, path_prefix: internal ? "/" : str("location-path", r), match_type: internal ? "prefix" : str("location-match", r),
source: action === "web_server" ? { type: "web_server", web_service_id: str("location-web-service", r) } : { type: "reverse_proxy" }, source: action === "web_server" ? { type: "web_server", web_service_id: str("location-web-service", r) } : { type: "reverse_proxy" },
backend_id: action === "proxy" ? (str("location-backend", r) || null) : null, backend_id: action === "proxy" ? (str("location-backend", r) || null) : null,
enabled: yes("location-enabled", r), cache: action === "proxy" ? (str("location-cache", r) === "" ? null : str("location-cache", r) === "on") : false, enabled: yes("location-enabled", r), cache: action === "proxy" ? (str("location-cache", r) === "" ? null : str("location-cache", r) === "on") : false,
@@ -325,7 +327,8 @@
headers: r.readHeaders(), headers: r.readHeaders(),
rewrite: { mode: handled ? rewriteMode : "preserve", value: handled && rewriteMode !== "preserve" ? read("location-rewrite-value", r).value.trim() : "" }, rewrite: { mode: handled ? rewriteMode : "preserve", value: handled && rewriteMode !== "preserve" ? read("location-rewrite-value", r).value.trim() : "" },
response: action === "return" ? { status: Number(str("location-response-status", r)), content_type: str("location-response-type", r), body: read("location-response-body", r).value } : null, response: action === "return" ? { status: Number(str("location-response-status", r)), content_type: str("location-response-type", r), body: read("location-response-body", r).value } : null,
redirect: action === "redirect" ? { status: Number(str("location-redirect-status", r)), target: str("location-redirect-target", r), rewrite_pattern: nullable("location-redirect-pattern", r), preserve_query: yes("location-redirect-query", r) } : null redirect: action === "redirect" ? { status: Number(str("location-redirect-status", r)), target: str("location-redirect-target", r), rewrite_pattern: nullable("location-redirect-pattern", r), preserve_query: yes("location-redirect-query", r) } : null,
error_fallback: fallbackTarget ? { target: fallbackTarget, statuses: fallbackStatuses.length ? fallbackStatuses : [404] } : null
}; };
}); });
} }
@@ -339,6 +342,7 @@
field(body, "location-id", "locationId", value.id || `location-${suffix}`, "text", { required: true, pattern: "[A-Za-z0-9_-]{1,64}", maxLength: 64 }); field(body, "location-id", "locationId", value.id || `location-${suffix}`, "text", { required: true, pattern: "[A-Za-z0-9_-]{1,64}", maxLength: 64 });
const match = field(body, "location-match", "locationMatchType", value.match_type || "prefix", "select", { values: [["prefix", "locationMatchPrefix"], ["prefix_priority", "locationMatchPriority"], ["exact", "locationMatchExact"], ["regex", "locationMatchRegex"], ["regex_insensitive", "locationMatchRegexI"]] }); const match = field(body, "location-match", "locationMatchType", value.match_type || "prefix", "select", { values: [["prefix", "locationMatchPrefix"], ["prefix_priority", "locationMatchPriority"], ["exact", "locationMatchExact"], ["regex", "locationMatchRegex"], ["regex_insensitive", "locationMatchRegexI"]] });
const path = field(body, "location-path", "locationPathPattern", value.path_prefix || "/", "text", { required: true, maxLength: 1024 }); path.placeholder = "/api or ^/api/v[0-9]+/"; const path = field(body, "location-path", "locationPathPattern", value.path_prefix || "/", "text", { required: true, maxLength: 1024 }); path.placeholder = "/api or ^/api/v[0-9]+/";
const internal = field(body, "location-internal", "locationInternal", value.internal === true, "checkbox");
const initialAction = value.response ? "return" : value.redirect ? "redirect" : value.source?.type === "web_server" ? "web_server" : "proxy"; const initialAction = value.response ? "return" : value.redirect ? "redirect" : value.source?.type === "web_server" ? "web_server" : "proxy";
const action = field(body, "location-action", "locationAction", initialAction, "select", { values: [["proxy", "locationProxyAction"], ["web_server", "locationWebServerAction"], ["redirect", "locationRedirectAction"], ["return", "locationReturnAction"]] }); const action = field(body, "location-action", "locationAction", initialAction, "select", { values: [["proxy", "locationProxyAction"], ["web_server", "locationWebServerAction"], ["redirect", "locationRedirectAction"], ["return", "locationReturnAction"]] });
const backend = field(body, "location-backend", "backend", value.backend_id || "", "select"); memberOptions(backend, value.backend_id || ""); const backend = field(body, "location-backend", "backend", value.backend_id || "", "select"); memberOptions(backend, value.backend_id || "");
@@ -347,6 +351,9 @@
const cache = field(body, "location-cache", "cacheEnabled", value.cache === true ? "on" : value.cache === false ? "off" : "", "select", { values: [["", "inherit"], ["on", "enabled"], ["off", "notEnabled"]] }); const cache = field(body, "location-cache", "cacheEnabled", value.cache === true ? "on" : value.cache === false ? "off" : "", "select", { values: [["", "inherit"], ["on", "enabled"], ["off", "notEnabled"]] });
const handling = el("details", undefined, "vh-wide location-section"), handlingBody = el("div", undefined, "vh-grid"); handling.append(text("summary", "locationHandling"), handlingBody); body.append(handling); const handling = el("details", undefined, "vh-wide location-section"), handlingBody = el("div", undefined, "vh-grid"); handling.append(text("summary", "locationHandling"), handlingBody); body.append(handling);
field(handlingBody, "location-error-target", "locationErrorFallbackTarget", value.error_fallback?.target || "", "text", { maxLength: 64, pattern: "[A-Za-z0-9_-]{0,64}" });
field(handlingBody, "location-error-statuses", "locationErrorFallbackStatuses", (value.error_fallback?.statuses || [404]).join(", "), "text", { maxLength: 64 });
note(handlingBody, "locationErrorFallbackHelp");
const rewriteMode = field(handlingBody, "location-rewrite-mode", "locationRewriteMode", value.rewrite?.mode || "preserve", "select", { values: [["preserve", "locationRewritePreserve"], ["strip_prefix", "locationRewriteStrip"], ["replace_prefix", "locationRewriteReplace"], ["regex", "locationRewriteRegex"]] }); const rewriteMode = field(handlingBody, "location-rewrite-mode", "locationRewriteMode", value.rewrite?.mode || "preserve", "select", { values: [["preserve", "locationRewritePreserve"], ["strip_prefix", "locationRewriteStrip"], ["replace_prefix", "locationRewriteReplace"], ["regex", "locationRewriteRegex"]] });
const rewriteValue = field(handlingBody, "location-rewrite-value", "locationRewriteValue", value.rewrite?.value || "", "text", { maxLength: 2048 }); const rewriteValue = field(handlingBody, "location-rewrite-value", "locationRewriteValue", value.rewrite?.value || "", "text", { maxLength: 2048 });
const returnBox = el("div", undefined, "vh-wide vh-grid location-return-box"); handlingBody.append(returnBox); const returnBox = el("div", undefined, "vh-wide vh-grid location-return-box"); handlingBody.append(returnBox);
@@ -395,14 +402,15 @@
const syncUi = () => { const syncUi = () => {
const isReturn = action.value === "return", isRedirect = action.value === "redirect", isWeb = action.value === "web_server", isProxy = action.value === "proxy", isRegex = match.value === "regex" || match.value === "regex_insensitive"; const isReturn = action.value === "return", isRedirect = action.value === "redirect", isWeb = action.value === "web_server", isProxy = action.value === "proxy", isRegex = match.value === "regex" || match.value === "regex_insensitive";
match.disabled = path.disabled = internal.checked;
backend.disabled = !isProxy; cache.disabled = !isProxy; dedicated.disabled = !isProxy; backend.disabled = !isProxy; cache.disabled = !isProxy; dedicated.disabled = !isProxy;
webService.disabled = !isWeb; webService.required = isWeb; webService.disabled = !isWeb; webService.required = isWeb;
returnBox.hidden = !isReturn; redirectBox.hidden = !isRedirect; rewriteMode.disabled = isReturn || isRedirect; rewriteValue.disabled = isReturn || isRedirect || rewriteMode.value === "preserve" || rewriteMode.value === "strip_prefix"; returnBox.hidden = !isReturn; redirectBox.hidden = !isRedirect; rewriteMode.disabled = isReturn || isRedirect; rewriteValue.disabled = isReturn || isRedirect || rewriteMode.value === "preserve" || rewriteMode.value === "strip_prefix";
for (const option of rewriteMode.options) if (option.value === "regex") option.disabled = !isRegex; for (const option of rewriteMode.options) if (option.value === "regex") option.disabled = !isRegex;
if (!isRegex && rewriteMode.value === "regex") rewriteMode.value = "preserve"; if (!isRegex && rewriteMode.value === "regex") rewriteMode.value = "preserve";
path.placeholder = isRegex ? "^/api/v[0-9]+/" : "/api/"; path.placeholder = isRegex ? "^/api/v[0-9]+/" : "/api/";
const id = str("location-id", row) || t("locationId"), pattern = str("location-path", row) || "/"; const id = str("location-id", row) || t("locationId"), pattern = internal.checked ? `@${id}` : (str("location-path", row) || "/");
summary.textContent = `${id} · ${t(`locationMatch_${match.value}`)} · ${pattern} · ${t(isReturn ? "locationReturnShort" : isRedirect ? "locationRedirectShort" : isWeb ? "locationWebServerShort" : "locationProxyShort")}`; summary.textContent = `${id} · ${internal.checked ? t("locationInternal") : t(`locationMatch_${match.value}`)} · ${pattern} · ${t(isReturn ? "locationReturnShort" : isRedirect ? "locationRedirectShort" : isWeb ? "locationWebServerShort" : "locationProxyShort")}`;
previewStatus.hidden = yes("location-enabled", row); previewStatus.hidden = yes("location-enabled", row);
previewValues.match.textContent = `${t(`locationMatch_${match.value}`)} · ${pattern}`; previewValues.match.textContent = `${t(`locationMatch_${match.value}`)} · ${pattern}`;
+7 -2
View File
@@ -257,7 +257,11 @@
g = group("vhAdvanced", true); field(g, "paths", "vhPaths", JSON.stringify(value.routes || [], null, 2), "textarea", { rows: 5, maxLength: 131072 }); g = group("vhAdvanced", true); field(g, "paths", "vhPaths", JSON.stringify(value.routes || [], null, 2), "textarea", { rows: 5, maxLength: 131072 });
field(g, "acl", "vhAcl", JSON.stringify(value.acl ?? null, null, 2), "textarea", { rows: 5, maxLength: 32768 }); field(g, "acl", "vhAcl", JSON.stringify(value.acl ?? null, null, 2), "textarea", { rows: 5, maxLength: 32768 });
field(g, "headers", "vhHeaders", JSON.stringify(value.headers || {}, null, 2), "textarea", { rows: 5, maxLength: 65536 }); field(g, "headers", "vhHeaders", JSON.stringify(value.headers || {}, null, 2), "textarea", { rows: 5, maxLength: 65536 });
g = group("vhLocations"); locationsReader = locationsEditor(g, value.routes || []); g = group("vhLocations");
field(g, "main-error-target", "vhErrorFallbackTarget", value.error_fallback?.target || "", "text", { maxLength: 64, pattern: "[A-Za-z0-9_-]{0,64}" });
field(g, "main-error-statuses", "vhErrorFallbackStatuses", (value.error_fallback?.statuses || [404]).join(", "), "text", { maxLength: 64 });
note(g, "vhErrorFallbackHelp");
locationsReader = locationsEditor(g, value.routes || []);
g = group("vhSsl"); g = group("vhSsl");
const selector = field(g, "certificate-id", "vhCertificate", value.certificate_id || "", "select"); const selector = field(g, "certificate-id", "vhCertificate", value.certificate_id || "", "select");
const fillCertificates = () => { const selected = selector.value || value.certificate_id || ""; const inherit = text("option", "vhCertificateDefault"); inherit.value = ""; selector.replaceChildren(inherit, ...[...(envelope?.config.tls?.certificates || []), ...stagedCertificates].map(c => { const o = el("option", c.id); o.value = c.id; return o; })); selector.value = selected; }; fillCertificates(); const fillCertificates = () => { const selected = selector.value || value.certificate_id || ""; const inherit = text("option", "vhCertificateDefault"); inherit.value = ""; selector.replaceChildren(inherit, ...[...(envelope?.config.tls?.certificates || []), ...stagedCertificates].map(c => { const o = el("option", c.id); o.value = c.id; return o; })); selector.value = selected; }; fillCertificates();
@@ -386,6 +390,7 @@
const members = [...form.querySelectorAll(".vh-member")].map(row => ({ id: str("member-id", row), url: str("member-url", row), weight: num("member-weight", row), enabled: yes("member-enabled", row), pool_eligible: yes("member-pool", row), backup: yes("member-backup", row), drain: yes("member-drain", row), allow_private: yes("member-private", row), accept_local_listener_risk: yes("member-local-risk", row), preserve_host: yes("member-host", row), protocol: str("member-protocol", row), address_allowlist: lines(str("member-allowlist", row)), tls_server_name: nullable("member-tls-name", row), tls_verify: yes("member-tls-verify", row), tls_ca_pem: nullable("member-ca", row), max_concurrent_requests: num("member-max-concurrent", row), queue_timeout_ms: num("member-queue-timeout", row) })).filter(member => member.url); const members = [...form.querySelectorAll(".vh-member")].map(row => ({ id: str("member-id", row), url: str("member-url", row), weight: num("member-weight", row), enabled: yes("member-enabled", row), pool_eligible: yes("member-pool", row), backup: yes("member-backup", row), drain: yes("member-drain", row), allow_private: yes("member-private", row), accept_local_listener_risk: yes("member-local-risk", row), preserve_host: yes("member-host", row), protocol: str("member-protocol", row), address_allowlist: lines(str("member-allowlist", row)), tls_server_name: nullable("member-tls-name", row), tls_verify: yes("member-tls-verify", row), tls_ca_pem: nullable("member-ca", row), max_concurrent_requests: num("member-max-concurrent", row), queue_timeout_ms: num("member-queue-timeout", row) })).filter(member => member.url);
const bindingSelection = readBindings(); const bindingSelection = readBindings();
const mainSourceType = str("source-type"); const mainSourceType = str("source-type");
const mainFallbackTarget = str("main-error-target"), mainFallbackStatuses = lines(str("main-error-statuses")).map(value => Number(value));
const needsProxy = mainSourceType === "reverse_proxy" || paths.some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy"); const needsProxy = mainSourceType === "reverse_proxy" || paths.some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy");
if (!bindingSelection.length) throw new Error(t("vhListenerRequired")); if (!bindingSelection.length) throw new Error(t("vhListenerRequired"));
if (needsProxy && !members.length) throw new Error(t("vhBackendRequired")); if (needsProxy && !members.length) throw new Error(t("vhBackendRequired"));
@@ -419,7 +424,7 @@
if (JSON.stringify(selected) !== JSON.stringify(globalPolicy)) throw new Error(t("vhTlsHttp3Conflict")); if (JSON.stringify(selected) !== JSON.stringify(globalPolicy)) throw new Error(t("vhTlsHttp3Conflict"));
} }
return { return {
id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, force_ssl: yes("force-ssl"), www_redirect: yes("www-redirect"), www_redirect_target: str("www-redirect-target") || null, forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), compression: collectCompression(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: nullable("acl-profile-id"), id: str("id"), enabled: yes("enabled"), source: mainSourceType === "web_server" ? { type: "web_server", web_service_id: str("source-web-service") } : { type: "reverse_proxy" }, error_fallback: mainFallbackTarget ? { target: mainFallbackTarget, statuses: mainFallbackStatuses.length ? mainFallbackStatuses : [404] } : null, force_ssl: yes("force-ssl"), www_redirect: yes("www-redirect"), www_redirect_target: str("www-redirect-target") || null, forward_client_ip: yes("forward-client-ip"), websockets: yes("websockets"), websocket_legacy_support: yes("websockets") && yes("websocket-legacy-support"), websocket_rfc8441: yes("websockets") && ["h2", "h2h3"].includes(str("websocket-mode")), websocket_rfc9220: yes("websockets") && ["h3", "h2h3"].includes(str("websocket-mode")), http2_enabled: yes("http2-enabled"), http3_enabled: yes("http3-enabled"), cloudflare_client_ip: yes("cloudflare-client-ip"), timing_headers: yes("timing-headers"), backend_header: yes("backend-header"), backend_timing_header: yes("backend-timing-header"), basic_auth: basicEditor.read(), certificate_id: nullable("certificate-id"), tls_policy: yes("tls-policy-custom") ? { tls12: yes("tls-policy-12"), tls13: yes("tls-policy-13"), cipher_suites: lines(str("tls-policy-ciphers")) } : null, cache: collectCache(), compression: collectCompression(), transform: collectTransform(), pages: pagesEditor.read(), frontend: { hosts: readHosts(), listener_ids: bindingSelection }, acl_profile_id: nullable("acl-profile-id"),
proxy_timeouts: { connect_timeout_seconds: num("proxy-connect-timeout"), response_header_timeout_seconds: num("proxy-header-timeout"), read_timeout_seconds: num("proxy-read-timeout"), send_timeout_seconds: num("proxy-send-timeout") }, proxy_timeouts: { connect_timeout_seconds: num("proxy-connect-timeout"), response_header_timeout_seconds: num("proxy-header-timeout"), read_timeout_seconds: num("proxy-read-timeout"), send_timeout_seconds: num("proxy-send-timeout") },
rate_limit: { enabled: yes("rate-enabled"), requests: num("rate-requests"), window_seconds: num("rate-window"), burst: num("rate-burst"), country_access: { mode: str("country-mode"), countries: lines(str("country-list")).map(value => value.toUpperCase()) }, country_limits: countryRatesReader() }, rate_limit: { enabled: yes("rate-enabled"), requests: num("rate-requests"), window_seconds: num("rate-window"), burst: num("rate-burst"), country_access: { mode: str("country-mode"), countries: lines(str("country-list")).map(value => value.toUpperCase()) }, country_limits: countryRatesReader() },
method_policy: { mode: str("method-mode"), methods: lines(str("method-list")).map(value => value.toUpperCase()) }, method_policy: { mode: str("method-mode"), methods: lines(str("method-list")).map(value => value.toUpperCase()) },
+17 -8
View File
@@ -80,8 +80,10 @@
return result; return result;
} }
function validatePhpCustomData(customParams, phpValues) { function validatePhpCustomData(customParams, phpValues, phpAdminValues = {}) {
const size = [...Object.entries(customParams), ...Object.entries(phpValues)] // Never allow an admin override to conflict with ordinary PHP_VALUE.
if (Object.keys(phpValues).some(name => Object.hasOwn(phpAdminValues, name))) throw new Error(t("webServicePhpValuesInvalid"));
const size = [...Object.entries(customParams), ...Object.entries(phpValues), ...Object.entries(phpAdminValues)]
.reduce((total, [name, value]) => total + name.length + value.length + 2, 0); .reduce((total, [name, value]) => total + name.length + value.length + 2, 0);
if (size > 128 * 1024) throw new Error(t("webServicePhpCustomDataTooLarge")); if (size > 128 * 1024) throw new Error(t("webServicePhpCustomDataTooLarge"));
} }
@@ -156,8 +158,9 @@
for (const id of [ for (const id of [
"web-service-php-endpoint", "web-service-php-extensions", "web-service-php-connect-timeout", "web-service-php-endpoint", "web-service-php-extensions", "web-service-php-connect-timeout",
"web-service-php-request-timeout", "web-service-php-concurrency", "web-service-php-pass-authorization", "web-service-php-request-timeout", "web-service-php-concurrency", "web-service-php-pass-authorization",
"web-service-php-path-info",
"web-service-php-body-limit", "web-service-php-header-limit", "web-service-php-stderr-limit", "web-service-php-body-limit", "web-service-php-header-limit", "web-service-php-stderr-limit",
"web-service-php-custom-params", "web-service-php-values" "web-service-php-custom-params", "web-service-php-values", "web-service-php-admin-values"
]) $(id).disabled = !enabled; ]) $(id).disabled = !enabled;
} }
@@ -194,9 +197,9 @@
mime_types: {}, default_mime_type: "application/octet-stream", cache_control: null, cache_rules: [], response_headers: {}, mime_types: {}, default_mime_type: "application/octet-stream", cache_control: null, cache_rules: [], response_headers: {},
try_files: [], rewrites: [], compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, precompressed: { enabled: false, brotli: true, gzip: true }, download_extensions: [], try_files: [], rewrites: [], compression: { enabled: null, gzip: null, br: null, zstd: null, deflate: null }, precompressed: { enabled: false, brotli: true, gzip: true }, download_extensions: [],
symlink_policy: "deny", php: { symlink_policy: "deny", php: {
enabled: false, extensions: ["php"], endpoint: "unix:/run/php/php-fpm.sock", connect_timeout_ms: 1000, enabled: false, path_info: false, extensions: ["php"], endpoint: "unix:/run/php/php-fpm.sock", connect_timeout_ms: 1000,
request_timeout_ms: 30000, max_concurrent_requests: 64, pass_authorization: true, max_request_body_bytes: 0, request_timeout_ms: 30000, max_concurrent_requests: 64, pass_authorization: true, max_request_body_bytes: 0,
max_response_header_bytes: 65536, stderr_log_bytes: 4096, custom_params: {}, php_values: {} max_response_header_bytes: 65536, stderr_log_bytes: 4096, custom_params: {}, php_values: {}, php_admin_values: {}
}, },
htaccess: { enabled: false, filename: ".htaccess", scan_subdirectories: true, strict: false }, htaccess: { enabled: false, filename: ".htaccess", scan_subdirectories: true, strict: false },
hardening: { block_common_attacks: false, strip_unnecessary_headers: false } hardening: { block_common_attacks: false, strip_unnecessary_headers: false }
@@ -241,6 +244,7 @@
$("web-service-precompressed-gzip").checked = item.precompressed?.gzip !== false; $("web-service-precompressed-gzip").checked = item.precompressed?.gzip !== false;
$("web-service-download-extensions").value = (item.download_extensions || []).join("\n"); $("web-service-download-extensions").value = (item.download_extensions || []).join("\n");
$("web-service-php-enabled").checked = item.php?.enabled === true; $("web-service-php-enabled").checked = item.php?.enabled === true;
$("web-service-php-path-info").checked = item.php?.path_info === true;
$("web-service-php-endpoint").value = item.php?.endpoint || "unix:/run/php/php-fpm.sock"; $("web-service-php-endpoint").value = item.php?.endpoint || "unix:/run/php/php-fpm.sock";
$("web-service-php-extensions").value = (item.php?.extensions || ["php"]).join(", "); $("web-service-php-extensions").value = (item.php?.extensions || ["php"]).join(", ");
$("web-service-php-connect-timeout").value = item.php?.connect_timeout_ms ?? 1000; $("web-service-php-connect-timeout").value = item.php?.connect_timeout_ms ?? 1000;
@@ -252,6 +256,7 @@
$("web-service-php-stderr-limit").value = item.php?.stderr_log_bytes ?? 4096; $("web-service-php-stderr-limit").value = item.php?.stderr_log_bytes ?? 4096;
$("web-service-php-custom-params").value = mapLines(item.php?.custom_params); $("web-service-php-custom-params").value = mapLines(item.php?.custom_params);
$("web-service-php-values").value = mapLines(item.php?.php_values); $("web-service-php-values").value = mapLines(item.php?.php_values);
$("web-service-php-admin-values").value = mapLines(item.php?.php_admin_values);
$("web-service-symlink-policy").value = item.symlink_policy || "deny"; $("web-service-symlink-policy").value = item.symlink_policy || "deny";
$("web-service-htaccess-enabled").checked = item.htaccess?.enabled === true; $("web-service-htaccess-enabled").checked = item.htaccess?.enabled === true;
$("web-service-htaccess-filename").value = item.htaccess?.filename || ".htaccess"; $("web-service-htaccess-filename").value = item.htaccess?.filename || ".htaccess";
@@ -280,12 +285,14 @@
preset.php.request_timeout_ms = Number($("web-service-php-request-timeout").value || preset.php.request_timeout_ms); preset.php.request_timeout_ms = Number($("web-service-php-request-timeout").value || preset.php.request_timeout_ms);
preset.php.max_concurrent_requests = Number($("web-service-php-concurrency").value || preset.php.max_concurrent_requests); preset.php.max_concurrent_requests = Number($("web-service-php-concurrency").value || preset.php.max_concurrent_requests);
preset.php.pass_authorization = $("web-service-php-pass-authorization").checked; preset.php.pass_authorization = $("web-service-php-pass-authorization").checked;
preset.php.path_info = $("web-service-php-path-info").checked;
preset.php.max_request_body_bytes = Number($("web-service-php-body-limit").value || 0); preset.php.max_request_body_bytes = Number($("web-service-php-body-limit").value || 0);
preset.php.max_response_header_bytes = Number($("web-service-php-header-limit").value || preset.php.max_response_header_bytes); preset.php.max_response_header_bytes = Number($("web-service-php-header-limit").value || preset.php.max_response_header_bytes);
preset.php.stderr_log_bytes = Number($("web-service-php-stderr-limit").value || 0); preset.php.stderr_log_bytes = Number($("web-service-php-stderr-limit").value || 0);
preset.php.custom_params = parseFastCgiParams($("web-service-php-custom-params").value); preset.php.custom_params = parseFastCgiParams($("web-service-php-custom-params").value);
preset.php.php_values = parsePhpValues($("web-service-php-values").value); preset.php.php_values = parsePhpValues($("web-service-php-values").value);
validatePhpCustomData(preset.php.custom_params, preset.php.php_values); preset.php.php_admin_values = parsePhpValues($("web-service-php-admin-values").value);
validatePhpCustomData(preset.php.custom_params, preset.php.php_values, preset.php.php_admin_values);
preset.hardening.block_common_attacks = $("web-service-hardening-attacks").checked; preset.hardening.block_common_attacks = $("web-service-hardening-attacks").checked;
preset.hardening.strip_unnecessary_headers = $("web-service-hardening-headers").checked; preset.hardening.strip_unnecessary_headers = $("web-service-hardening-headers").checked;
populateSettings(preset); populateSettings(preset);
@@ -423,6 +430,7 @@
}, },
php: { php: {
enabled: phpEnabled, enabled: phpEnabled,
path_info: $("web-service-php-path-info").checked,
extensions: lines($("web-service-php-extensions").value).map(value => value.replace(/^\./, "").toLowerCase()), extensions: lines($("web-service-php-extensions").value).map(value => value.replace(/^\./, "").toLowerCase()),
endpoint: $("web-service-php-endpoint").value.trim() || "unix:/run/php/php-fpm.sock", endpoint: $("web-service-php-endpoint").value.trim() || "unix:/run/php/php-fpm.sock",
connect_timeout_ms: Number($("web-service-php-connect-timeout").value || 1000), connect_timeout_ms: Number($("web-service-php-connect-timeout").value || 1000),
@@ -433,10 +441,11 @@
max_response_header_bytes: Number($("web-service-php-header-limit").value || 65536), max_response_header_bytes: Number($("web-service-php-header-limit").value || 65536),
stderr_log_bytes: Number($("web-service-php-stderr-limit").value || 0), stderr_log_bytes: Number($("web-service-php-stderr-limit").value || 0),
custom_params: parseFastCgiParams($("web-service-php-custom-params").value), custom_params: parseFastCgiParams($("web-service-php-custom-params").value),
php_values: parsePhpValues($("web-service-php-values").value) php_values: parsePhpValues($("web-service-php-values").value),
php_admin_values: parsePhpValues($("web-service-php-admin-values").value)
} }
}; };
validatePhpCustomData(item.php.custom_params, item.php.php_values); validatePhpCustomData(item.php.custom_params, item.php.php_values, item.php.php_admin_values);
if (phpEnabled && item.symlink_policy !== "deny") throw new Error(t("webServicePhpSymlinkConflict")); if (phpEnabled && item.symlink_policy !== "deny") throw new Error(t("webServicePhpSymlinkConflict"));
const next = structuredClone(envelope.config.web_services || []); const next = structuredClone(envelope.config.web_services || []);
const duplicate = next.find(service => service.id === item.id && service.id !== editing); const duplicate = next.find(service => service.id === item.id && service.id !== editing);
+1
View File
@@ -1,6 +1,7 @@
//! Desired configuration and its validated, immutable routing snapshot. //! Desired configuration and its validated, immutable routing snapshot.
include!("config/model.rs"); include!("config/model.rs");
include!("config/snapshot.rs"); include!("config/snapshot.rs");
include!("config/internal_routing.rs");
include!("config/backend.rs"); include!("config/backend.rs");
include!("config/routing.rs"); include!("config/routing.rs");
include!("config/compile.rs"); include!("config/compile.rs");
+1
View File
@@ -292,6 +292,7 @@ pub fn compile_with_active(
"HTTP/3 listeners require TLS 1.3 and a TLS 1.3 cipher suite in the global TLS policy" "HTTP/3 listeners require TLS 1.3 and a TLS 1.3 cipher suite in the global TLS policy"
); );
Ok(Snapshot { Ok(Snapshot {
web_php_services: web_services,
vhost_metrics, vhost_metrics,
forwarding, forwarding,
quic_tls, quic_tls,
+34
View File
@@ -0,0 +1,34 @@
impl Snapshot {
/// Resolve a vhost-local internal named location. Internal locations are
/// deliberately excluded from ordinary URI matching and can only be reached
/// by an explicit internal dispatch such as an error fallback.
pub fn select_internal_on(
&self,
host: &str,
vhost_id: &str,
target: &str,
local: SocketAddr,
tls: bool,
) -> Option<&CompiledRoute> {
for pattern in Self::host_patterns(host) {
let owns_host = self.fallbacks.get(&pattern).is_some_and(|routes| {
routes.iter().any(|route| {
route.frontend.as_ref().is_some_and(|frontend| frontend.id == vhost_id)
&& route.bindings.iter().any(|binding| binding.matches(local, tls))
})
});
if !owns_host {
continue;
}
return self.routes.get(&pattern).and_then(|routes| {
routes.iter().find(|route| {
route.internal
&& route.name == target
&& route.frontend.as_ref().is_some_and(|frontend| frontend.id == vhost_id)
&& route.bindings.iter().any(|binding| binding.matches(local, tls))
})
});
}
None
}
}
+5
View File
@@ -197,13 +197,17 @@ pub struct CompiledRoute {
pub frontend: Option<Arc<CompiledFrontend>>, pub frontend: Option<Arc<CompiledFrontend>>,
pub pool: Option<Arc<crate::balance::Pool>>, pub pool: Option<Arc<crate::balance::Pool>>,
pub cache: Option<bool>, pub cache: Option<bool>,
/// Vhost-local stable location ID (`main` for the main source).
pub name: String,
pub id: String, pub id: String,
pub internal: bool,
pub path_prefix: String, pub path_prefix: String,
pub match_type: crate::vhost::PathMatch, pub match_type: crate::vhost::PathMatch,
pub path_regex: Option<regex::Regex>, pub path_regex: Option<regex::Regex>,
pub rewrite: crate::vhost::PathRewrite, pub rewrite: crate::vhost::PathRewrite,
pub response: Option<crate::vhost::LocationResponse>, pub response: Option<crate::vhost::LocationResponse>,
pub redirect: Option<CompiledLocationRedirect>, pub redirect: Option<CompiledLocationRedirect>,
pub error_fallback: Option<crate::vhost::ErrorFallback>,
pub source: CompiledSource, pub source: CompiledSource,
pub backend: Option<Arc<CompiledBackend>>, pub backend: Option<Arc<CompiledBackend>>,
pub acl_profile_id: Option<String>, pub acl_profile_id: Option<String>,
@@ -265,6 +269,7 @@ pub struct ResolvedLogVhost {
} }
pub struct Snapshot { pub struct Snapshot {
pub web_php_services: HashMap<String, Arc<crate::web_server::CompiledService>>,
pub vhost_metrics: std::collections::BTreeMap<String, Arc<crate::metrics::VhostMetrics>>, pub vhost_metrics: std::collections::BTreeMap<String, Arc<crate::metrics::VhostMetrics>>,
pub forwarding: crate::forwarding::Compiled, pub forwarding: crate::forwarding::Compiled,
pub quic_tls: Option<Arc<quinn::crypto::rustls::QuicServerConfig>>, pub quic_tls: Option<Arc<quinn::crypto::rustls::QuicServerConfig>>,
+19 -2
View File
@@ -91,8 +91,17 @@ fn compile_routing(
} }
claimed.extend(host_bindings.iter().copied()); claimed.extend(host_bindings.iter().copied());
} }
// Backend definitions are irrelevant to a Web Server main source unless
// an enabled Custom Location explicitly uses Reverse Proxy.
let needs_proxy_pool = host.source.is_reverse_proxy()
|| host.routes.iter().any(|route| {
route.enabled
&& route.response.is_none()
&& route.redirect.is_none()
&& route.source.is_reverse_proxy()
});
let mut compiled_members = Vec::new(); let mut compiled_members = Vec::new();
for definition in &host.backends { for definition in host.backends.iter().filter(|_| needs_proxy_pool) {
let mut backend = compile_backend_with_timeout( let mut backend = compile_backend_with_timeout(
&definition.backend(), &definition.backend(),
&desired_policy, &desired_policy,
@@ -240,13 +249,16 @@ fn compile_routing(
frontend: Some(frontend.clone()), frontend: Some(frontend.clone()),
pool: fallback_pool, pool: fallback_pool,
cache: None, cache: None,
name: "main".into(),
id: format!("{}/main", host.id), id: format!("{}/main", host.id),
internal: false,
path_prefix: "/".into(), path_prefix: "/".into(),
match_type: crate::vhost::PathMatch::Prefix, match_type: crate::vhost::PathMatch::Prefix,
path_regex: None, path_regex: None,
rewrite: crate::vhost::PathRewrite::default(), rewrite: crate::vhost::PathRewrite::default(),
response: None, response: None,
redirect: None, redirect: None,
error_fallback: host.error_fallback.clone(),
source: fallback_source, source: fallback_source,
backend: fallback_backend, backend: fallback_backend,
acl_profile_id: None, acl_profile_id: None,
@@ -256,7 +268,7 @@ fn compile_routing(
country_access: crate::rate_limit::CountryAccess::default(), country_access: crate::rate_limit::CountryAccess::default(),
method_policy: crate::vhost::MethodPolicy::default(), method_policy: crate::vhost::MethodPolicy::default(),
headers: crate::headers::CompiledHeaders::default(), headers: crate::headers::CompiledHeaders::default(),
unavailable: host.transient_unavailable, unavailable: host.transient_unavailable && host.source.is_reverse_proxy(),
}; };
for name in &routing_hosts { for name in &routing_hosts {
fallbacks fallbacks
@@ -305,10 +317,12 @@ fn compile_routing(
None None
} }
}; };
if !route.internal {
ensure!( ensure!(
path_bindings.insert((route.match_type, route.path_prefix.clone())), path_bindings.insert((route.match_type, route.path_prefix.clone())),
"duplicate vhost location matcher" "duplicate vhost location matcher"
); );
}
validate_route_rewrite(route, path_regex.as_ref())?; validate_route_rewrite(route, path_regex.as_ref())?;
validate_location_response(route.response.as_ref())?; validate_location_response(route.response.as_ref())?;
let compiled_redirect = validate_location_redirect(route.redirect.as_ref())?; let compiled_redirect = validate_location_redirect(route.redirect.as_ref())?;
@@ -377,13 +391,16 @@ fn compile_routing(
frontend: Some(frontend.clone()), frontend: Some(frontend.clone()),
pool: route_pool, pool: route_pool,
cache: route.cache, cache: route.cache,
name: route.id.clone(),
id: format!("{}/{}", host.id, route.id), id: format!("{}/{}", host.id, route.id),
internal: route.internal,
path_prefix: route.path_prefix.clone(), path_prefix: route.path_prefix.clone(),
match_type: route.match_type, match_type: route.match_type,
path_regex, path_regex,
rewrite: route.rewrite.clone(), rewrite: route.rewrite.clone(),
response: route.response.clone(), response: route.response.clone(),
redirect: compiled_redirect, redirect: compiled_redirect,
error_fallback: route.error_fallback.clone(),
source, source,
backend, backend,
acl_profile_id: route.acl_profile_id.clone(), acl_profile_id: route.acl_profile_id.clone(),
+7 -3
View File
@@ -76,6 +76,7 @@ impl Snapshot {
} }
None None
} }
pub fn frontend( pub fn frontend(
&self, &self,
host: &str, host: &str,
@@ -224,7 +225,8 @@ where
F: Fn(&CompiledRoute) -> bool, F: Fn(&CompiledRoute) -> bool,
{ {
if let Some(exact) = routes.iter().find(|route| { if let Some(exact) = routes.iter().find(|route| {
allowed(route) !route.internal
&& allowed(route)
&& route.match_type == crate::vhost::PathMatch::Exact && route.match_type == crate::vhost::PathMatch::Exact
&& route.path_prefix == path && route.path_prefix == path
}) { }) {
@@ -233,7 +235,8 @@ where
let prefix = routes let prefix = routes
.iter() .iter()
.filter(|route| { .filter(|route| {
allowed(route) !route.internal
&& allowed(route)
&& matches!( && matches!(
route.match_type, route.match_type,
crate::vhost::PathMatch::Prefix | crate::vhost::PathMatch::PrefixPriority crate::vhost::PathMatch::Prefix | crate::vhost::PathMatch::PrefixPriority
@@ -245,7 +248,8 @@ where
return prefix; return prefix;
} }
if let Some(regex) = routes.iter().find(|route| { if let Some(regex) = routes.iter().find(|route| {
allowed(route) !route.internal
&& allowed(route)
&& matches!( && matches!(
route.match_type, route.match_type,
crate::vhost::PathMatch::Regex | crate::vhost::PathMatch::RegexInsensitive crate::vhost::PathMatch::Regex | crate::vhost::PathMatch::RegexInsensitive
+1 -1
View File
@@ -41,7 +41,7 @@ use zeroize::Zeroizing;
#[command( #[command(
name = "rust-proxy", name = "rust-proxy",
version, version,
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.29.0" about = "RustProxy - independent Rust reverse proxy and static Web Server"
)] )]
struct Cli { struct Cli {
#[arg( #[arg(
+1 -1
View File
@@ -239,7 +239,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
} }
}); });
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal(); let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
println!("RustProxy live logs 0.29.0 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits."); println!("RustProxy live logs {} - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.", env!("CARGO_PKG_VERSION"));
if interactive { print!("> "); io::stdout().flush()?; } if interactive { print!("> "); io::stdout().flush()?; }
let mut filters = LiveLogFilters::default(); let mut filters = LiveLogFilters::default();
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new(); let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
+1
View File
@@ -3,6 +3,7 @@
include!("prometheus/model.rs"); include!("prometheus/model.rs");
include!("prometheus/core.rs"); include!("prometheus/core.rs");
include!("prometheus/traffic.rs"); include!("prometheus/traffic.rs");
include!("prometheus/web_php.rs");
include!("prometheus/vhosts_backends.rs"); include!("prometheus/vhosts_backends.rs");
include!("prometheus/docker_misc.rs"); include!("prometheus/docker_misc.rs");
+1
View File
@@ -428,6 +428,7 @@ fn render_vhosts(snapshot: &crate::config::Snapshot, f: &mut Families) {
counter.load(Ordering::Relaxed), counter.load(Ordering::Relaxed),
); );
} }
} }
} }
} }
+1
View File
@@ -239,6 +239,7 @@ pub fn render(app: &App) -> String {
render_core(app, &snapshot, &mut out, &mut families); render_core(app, &snapshot, &mut out, &mut families);
render_vhosts(&snapshot, &mut families); render_vhosts(&snapshot, &mut families);
render_traffic(app, &snapshot, &mut families); render_traffic(app, &snapshot, &mut families);
render_web_php(&snapshot, &mut families);
render_backends(&snapshot, &docker_targets, &mut families); render_backends(&snapshot, &docker_targets, &mut families);
render_docker(&snapshot, &docker_targets, &mut families); render_docker(&snapshot, &docker_targets, &mut families);
render_misc(app, &snapshot, &mut families); render_misc(app, &snapshot, &mut families);
+28
View File
@@ -0,0 +1,28 @@
fn render_web_php(snapshot: &crate::config::Snapshot, f: &mut Families) {
let config = &snapshot.envelope.config;
for service in &config.web_services {
let Some(compiled) = snapshot.web_php_services.get(&service.id) else { continue };
let web = &compiled.web_metrics;
let wl = labels(&[("web_service", service.id.as_str())]);
f.add("rust_proxy_web_server_requests_total", "counter", "Web Server requests completed through response headers, including PHP requests.", &wl, web.requests.load(Ordering::Relaxed));
for (class, count) in [
("2xx", &web.status_2xx), ("3xx", &web.status_3xx),
("4xx", &web.status_4xx), ("5xx", &web.status_5xx),
] {
f.add("rust_proxy_web_server_responses_total", "counter", "Web Server responses by status class.", &format!("{wl},class=\"{class}\""), count.load(Ordering::Relaxed));
}
f.add("rust_proxy_web_server_response_header_duration_microseconds_total", "counter", "Web Server response header latency, excluding streamed body transfer.", &wl, web.response_headers_us.load(Ordering::Relaxed));
let Some(php) = &compiled.php else { continue };
let l = labels(&[("web_service", service.id.as_str())]);
let m = &php.metrics;
f.add("rust_proxy_php_requests_total", "counter", "PHP-FPM requests completed through response headers.", &l, m.requests.load(Ordering::Relaxed));
for (class, count) in [
("2xx", &m.status_2xx), ("3xx", &m.status_3xx),
("4xx", &m.status_4xx), ("5xx", &m.status_5xx),
] {
f.add("rust_proxy_php_responses_total", "counter", "PHP response headers by status class.", &format!("{l},class=\"{class}\""), count.load(Ordering::Relaxed));
}
f.add("rust_proxy_php_response_header_duration_microseconds_total", "counter", "Cumulative PHP response header latency in microseconds, excluding streamed body time.", &l, m.response_headers_us.load(Ordering::Relaxed));
f.add("rust_proxy_php_concurrency_available", "gauge", "Available PHP-FPM request permits in this Web Service.", &l, php.permits.available_permits());
}
}
+155
View File
@@ -0,0 +1,155 @@
#[derive(Clone)]
struct InternalFallbackRoute {
vhost_id: String,
target: String,
depth: u8,
}
#[derive(Clone)]
struct FinalRouteContext(crate::config::CompiledRoute);
#[derive(Clone)]
struct ErrorFallbackRequest {
method: Method,
uri: Uri,
version: Version,
headers: HeaderMap,
transport: Option<Transport>,
snapshot: Arc<crate::config::Snapshot>,
exchange: Option<Arc<Exchange>>,
connection_lease: Option<Arc<ConnectionLease>>,
stop: Option<ServerShutdown>,
depth: u8,
}
impl ErrorFallbackRequest {
fn capture(request: &Request, snapshot: &Arc<crate::config::Snapshot>) -> Self {
Self {
method: request.method().clone(),
uri: request.uri().clone(),
version: request.version(),
headers: request.headers().clone(),
transport: request.extensions().get::<Transport>().copied(),
snapshot: snapshot.clone(),
exchange: request.extensions().get::<Arc<Exchange>>().cloned(),
connection_lease: request
.extensions()
.get::<Arc<ConnectionLease>>()
.cloned(),
stop: request.extensions().get::<ServerShutdown>().cloned(),
depth: request
.extensions()
.get::<InternalFallbackRoute>()
.map(|dispatch| dispatch.depth)
.unwrap_or(0),
}
}
fn rebuild(&self, body: Bytes, peer: PeerAddr, local: LocalAddr, dispatch: InternalFallbackRoute) -> Request {
let mut request = Request::new(Body::from(body));
*request.method_mut() = self.method.clone();
*request.uri_mut() = self.uri.clone();
*request.version_mut() = self.version;
*request.headers_mut() = self.headers.clone();
request.extensions_mut().insert(peer);
request.extensions_mut().insert(local);
if let Some(transport) = self.transport {
request.extensions_mut().insert(transport);
}
request.extensions_mut().insert(self.snapshot.clone());
if let Some(exchange) = &self.exchange {
request.extensions_mut().insert(exchange.clone());
}
if let Some(lease) = &self.connection_lease {
request.extensions_mut().insert(lease.clone());
}
if let Some(stop) = &self.stop {
request.extensions_mut().insert(stop.clone());
}
request.extensions_mut().insert(dispatch);
request
}
}
enum ErrorFallbackBody {
Disabled,
Replayable(Bytes),
Unavailable,
}
const MAX_ERROR_FALLBACK_DEPTH: u8 = 8;
const MAX_ERROR_FALLBACK_REPLAY_BODY_BYTES: u64 = 8 * 1024 * 1024;
async fn prepare_error_fallback_body(request: &mut Request, enabled: bool) -> Result<ErrorFallbackBody, Response> {
if !enabled {
return Ok(ErrorFallbackBody::Disabled);
}
if request.body().is_end_stream() {
return Ok(ErrorFallbackBody::Replayable(Bytes::new()));
}
let Some(length) = request.body().size_hint().exact() else {
return Ok(ErrorFallbackBody::Unavailable);
};
if length > MAX_ERROR_FALLBACK_REPLAY_BODY_BYTES {
return Ok(ErrorFallbackBody::Unavailable);
}
let body = std::mem::replace(request.body_mut(), Body::empty());
let collected = body.collect().await.map_err(|_| {
error(
StatusCode::BAD_REQUEST,
"RustProxy: invalid or incomplete request body\n",
)
})?;
let bytes = collected.to_bytes();
*request.body_mut() = Body::from(bytes.clone());
Ok(ErrorFallbackBody::Replayable(bytes))
}
#[allow(clippy::too_many_arguments)]
async fn execute_error_fallback(
state: Arc<App>,
peer: PeerAddr,
local: LocalAddr,
snapshot: &Arc<crate::config::Snapshot>,
host: &str,
tls: bool,
route: &crate::config::CompiledRoute,
status: StatusCode,
template: &ErrorFallbackRequest,
body: &ErrorFallbackBody,
) -> Option<Response> {
let fallback = route
.error_fallback
.as_ref()
.filter(|fallback| fallback.matches(status))?;
let frontend = route.frontend.as_ref()?;
if template.depth >= MAX_ERROR_FALLBACK_DEPTH {
tracing::warn!(vhost = %frontend.id, route = %route.id, target = %fallback.target, "error fallback depth limit reached");
return None;
}
let Some(target) = snapshot
.select_internal_on(host, &frontend.id, &fallback.target, local.0, tls)
.cloned()
else {
return None;
};
let ErrorFallbackBody::Replayable(body) = body else {
tracing::warn!(vhost = %frontend.id, route = %route.id, target = %fallback.target, "error fallback request body is not replayable");
return None;
};
let request = template.rebuild(
body.clone(),
peer,
local,
InternalFallbackRoute {
vhost_id: frontend.id.clone(),
target: fallback.target.clone(),
depth: template.depth.saturating_add(1),
},
);
let mut response = Box::pin(forward_request(state, peer, local, request)).await;
if response.extensions().get::<FinalRouteContext>().is_none() {
response.extensions_mut().insert(FinalRouteContext(target));
}
Some(response)
}
+18
View File
@@ -1,6 +1,12 @@
{ {
match upstream_result { match upstream_result {
Ok(Ok(mut response)) => { Ok(Ok(mut response)) => {
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
).await {
return fallback;
}
let (origin_validation_headers, origin_plan, origin_reason, mut response) = let (origin_validation_headers, origin_plan, origin_reason, mut response) =
include!("success_prepare.rs"); include!("success_prepare.rs");
include!("success_finish.rs") include!("success_finish.rs")
@@ -53,6 +59,12 @@
return stale; return stale;
} }
} }
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
status, &fallback_request, &fallback_body,
).await {
return fallback;
}
let mut response = error( let mut response = error(
status, status,
if status == StatusCode::SERVICE_UNAVAILABLE { if status == StatusCode::SERVICE_UNAVAILABLE {
@@ -95,6 +107,12 @@
return stale; return stale;
} }
} }
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
StatusCode::GATEWAY_TIMEOUT, &fallback_request, &fallback_body,
).await {
return fallback;
}
error( error(
StatusCode::GATEWAY_TIMEOUT, StatusCode::GATEWAY_TIMEOUT,
"RustProxy: upstream response deadline exceeded\n", "RustProxy: upstream response deadline exceeded\n",
+32 -1
View File
@@ -39,6 +39,12 @@ if route.unavailable {
response response
.headers_mut() .headers_mut()
.insert("retry-after", HeaderValue::from_static("1")); .insert("retry-after", HeaderValue::from_static("1"));
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
).await {
return fallback;
}
return response; return response;
} }
if let crate::config::CompiledSource::WebServer(service) = &route.source { if let crate::config::CompiledSource::WebServer(service) = &route.source {
@@ -88,6 +94,12 @@ if let crate::config::CompiledSource::WebServer(service) = &route.source {
}, },
) )
.await; .await;
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
).await {
return fallback;
}
response.extensions_mut().insert(StaticSourceResponse); response.extensions_mut().insert(StaticSourceResponse);
return response; return response;
} }
@@ -108,6 +120,12 @@ let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool
response response
.headers_mut() .headers_mut()
.insert("retry-after", HeaderValue::from_static("1")); .insert("retry-after", HeaderValue::from_static("1"));
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
).await {
return fallback;
}
return response; return response;
} }
Err(crate::balance::SelectionFailure::Capacity) => { Err(crate::balance::SelectionFailure::Capacity) => {
@@ -118,6 +136,12 @@ let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool
response response
.headers_mut() .headers_mut()
.insert("retry-after", HeaderValue::from_static("1")); .insert("retry-after", HeaderValue::from_static("1"));
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
).await {
return fallback;
}
return response; return response;
} }
}; };
@@ -128,10 +152,17 @@ let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool
) )
} else { } else {
let Some(backend) = route.backend.clone() else { let Some(backend) = route.backend.clone() else {
return error( let response = error(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
"RustProxy: Reverse Proxy route has no backend\n", "RustProxy: Reverse Proxy route has no backend\n",
); );
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
).await {
return fallback;
}
return response;
}; };
(backend, None, None) (backend, None, None)
}; };
+29 -1
View File
@@ -2,6 +2,15 @@ async fn forward(
State(state): State<Arc<App>>, State(state): State<Arc<App>>,
Extension(peer): Extension<PeerAddr>, Extension(peer): Extension<PeerAddr>,
Extension(local): Extension<LocalAddr>, Extension(local): Extension<LocalAddr>,
request: Request,
) -> Response {
forward_request(state, peer, local, request).await
}
async fn forward_request(
state: Arc<App>,
peer: PeerAddr,
local: LocalAddr,
mut request: Request, mut request: Request,
) -> Response { ) -> Response {
if request.method() == Method::CONNECT if request.method() == Method::CONNECT
@@ -88,6 +97,10 @@ async fn forward(
.get::<Arc<crate::config::Snapshot>>() .get::<Arc<crate::config::Snapshot>>()
.cloned() .cloned()
.unwrap_or_else(|| state.config.load_full()); .unwrap_or_else(|| state.config.load_full());
let internal_dispatch = request
.extensions()
.get::<InternalFallbackRoute>()
.cloned();
let Ok(path) = crate::acl::routing_path(request.uri().path()) else { let Ok(path) = crate::acl::routing_path(request.uri().path()) else {
return error( return error(
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
@@ -226,6 +239,7 @@ async fn forward(
"RustProxy: denied by access policy\n", "RustProxy: denied by access policy\n",
); );
} }
if internal_dispatch.is_none() {
if let Err(limited) = state.rate_limiter.check( if let Err(limited) = state.rate_limiter.check(
snapshot.envelope.revision, snapshot.envelope.revision,
&frontend.id, &frontend.id,
@@ -243,6 +257,7 @@ async fn forward(
return response; return response;
} }
} }
}
// Global host ACLs remain an independent restrictive scope and also apply to // Global host ACLs remain an independent restrictive scope and also apply to
// maintenance/fallback/direct-IP responses. Profile/vhost IP+country policy // maintenance/fallback/direct-IP responses. Profile/vhost IP+country policy
// was evaluated above as one scope each. // was evaluated above as one scope each.
@@ -272,7 +287,15 @@ async fn forward(
}); });
return response; return response;
} }
let selected = snapshot.select_on(&host, &path, local.0, tls); let selected = if let Some(dispatch) = &internal_dispatch {
if frontend.is_none_or(|frontend| frontend.id != dispatch.vhost_id) {
None
} else {
snapshot.select_internal_on(&host, &dispatch.vhost_id, &dispatch.target, local.0, tls)
}
} else {
snapshot.select_on(&host, &path, local.0, tls)
};
if let Some(route) = selected { if let Some(route) = selected {
if let Some(response) = method_policy_denial(&route.method_policy, request.method()) { if let Some(response) = method_policy_denial(&route.method_policy, request.method()) {
return response; return response;
@@ -306,6 +329,7 @@ async fn forward(
); );
} }
} }
let fallback_request = ErrorFallbackRequest::capture(&request, &snapshot);
// The admin cookie is never ordinary application state. Strip it BEFORE // The admin cookie is never ordinary application state. Strip it BEFORE
// auth/cache and keep only the removed fragment locally. A verified self-admin // auth/cache and keep only the removed fragment locally. A verified self-admin
// backend may restore it after the actual upstream has been selected. // backend may restore it after the actual upstream has been selected.
@@ -374,6 +398,10 @@ async fn forward(
) )
}); });
}; };
let fallback_body = match prepare_error_fallback_body(&mut request, route.error_fallback.is_some()).await {
Ok(body) => body,
Err(response) => return response,
};
let ( let (
backend, backend,
backend_lease, backend_lease,
+1
View File
@@ -1,4 +1,5 @@
include!("types.rs"); include!("types.rs");
include!("error_fallback.rs");
include!("cache_helpers.rs"); include!("cache_helpers.rs");
include!("upstream_helpers.rs"); include!("upstream_helpers.rs");
include!("redirect_helpers.rs"); include!("redirect_helpers.rs");
+2
View File
@@ -20,6 +20,7 @@ use http::{
}; };
use http_body::Body as HttpBody; use http_body::Body as HttpBody;
use http_body_util::BodyExt; use http_body_util::BodyExt;
use bytes::Bytes;
#[derive(Clone)] #[derive(Clone)]
struct ResponseRulesApplied; struct ResponseRulesApplied;
#[derive(Clone)] #[derive(Clone)]
@@ -38,6 +39,7 @@ struct LocalHostResponse {
struct OriginTiming(Duration); struct OriginTiming(Duration);
#[derive(Clone)] #[derive(Clone)]
struct OriginBackend(String); struct OriginBackend(String);
use std::{ use std::{
sync::{ sync::{
atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering}, atomic::{AtomicBool, AtomicU16, AtomicU64, Ordering},
+8 -3
View File
@@ -191,6 +191,11 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
)) ))
}); });
let mut response = next.run(request).await; let mut response = next.run(request).await;
let final_route_context = response
.extensions()
.get::<FinalRouteContext>()
.map(|context| context.0.clone());
let effective_route_context = final_route_context.as_ref().or(route_context.as_ref());
let minimal_welcome = response let minimal_welcome = response
.extensions() .extensions()
.get::<crate::pages::MinimalWelcome>() .get::<crate::pages::MinimalWelcome>()
@@ -255,7 +260,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
response.headers_mut(), response.headers_mut(),
&snapshot, &snapshot,
frontend_context.as_deref(), frontend_context.as_deref(),
route_context.as_ref(), effective_route_context,
false, false,
); );
} }
@@ -289,7 +294,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
if let Some(frontend) = frontend_context.as_ref() { if let Some(frontend) = frontend_context.as_ref() {
compression = frontend.compression.resolve(global_compression, &compression); compression = frontend.compression.resolve(global_compression, &compression);
} }
if let Some(route) = route_context.as_ref() { if let Some(route) = effective_route_context {
if let crate::config::CompiledSource::WebServer(service) = &route.source { if let crate::config::CompiledSource::WebServer(service) = &route.source {
compression = service.compression.resolve(global_compression, &compression); compression = service.compression.resolve(global_compression, &compression);
} }
@@ -355,7 +360,7 @@ async fn observe(State(state): State<Arc<App>>, mut request: Request, next: Next
response.headers_mut(), response.headers_mut(),
&snapshot, &snapshot,
frontend_context.as_deref(), frontend_context.as_deref(),
route_context.as_ref(), effective_route_context,
); );
} }
if response if response
+1
View File
@@ -2,6 +2,7 @@
//! A Host header is never evidence that traffic arrived on a private interface. //! A Host header is never evidence that traffic arrived on a private interface.
include!("vhost/model.rs"); include!("vhost/model.rs");
include!("vhost/error_fallback.rs");
include!("vhost/redirect.rs"); include!("vhost/redirect.rs");
include!("vhost/validation.rs"); include!("vhost/validation.rs");
#[cfg(test)] #[cfg(test)]
+50
View File
@@ -0,0 +1,50 @@
pub const ERROR_FALLBACK_STATUSES: [u16; 6] = [403, 404, 500, 502, 503, 504];
fn default_error_fallback_statuses() -> Vec<u16> {
vec![404]
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default, deny_unknown_fields)]
pub struct ErrorFallback {
/// Stable ID of an internal Custom Location within the same vhost.
pub target: String,
/// HTTP statuses intercepted before normal response finalization/cache publication.
#[serde(default = "default_error_fallback_statuses")]
pub statuses: Vec<u16>,
}
impl Default for ErrorFallback {
fn default() -> Self {
Self {
target: String::new(),
statuses: default_error_fallback_statuses(),
}
}
}
impl ErrorFallback {
pub fn validate(&mut self) -> Result<()> {
self.target = self.target.trim().to_owned();
ensure!(crate::config::valid_id(&self.target), "invalid error fallback target");
ensure!(
!self.statuses.is_empty() && self.statuses.len() <= ERROR_FALLBACK_STATUSES.len(),
"error fallback requires 1..={} status codes",
ERROR_FALLBACK_STATUSES.len()
);
self.statuses.sort_unstable();
self.statuses.dedup();
ensure!(
self.statuses
.iter()
.all(|status| ERROR_FALLBACK_STATUSES.contains(status)),
"error fallback status must be one of 403, 404, 500, 502, 503, 504"
);
Ok(())
}
pub fn matches(&self, status: http::StatusCode) -> bool {
self.statuses.contains(&status.as_u16())
}
}
+11 -9
View File
@@ -7,7 +7,6 @@ use crate::{
use anyhow::{ensure, Result}; use anyhow::{ensure, Result};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
fn yes() -> bool { fn yes() -> bool {
true true
} }
@@ -17,12 +16,10 @@ fn root() -> String {
fn weight() -> u16 { fn weight() -> u16 {
1 1
} }
pub const DEFAULT_PROXY_CONNECT_TIMEOUT_SECONDS: u64 = 10; pub const DEFAULT_PROXY_CONNECT_TIMEOUT_SECONDS: u64 = 10;
pub const DEFAULT_PROXY_RESPONSE_HEADER_TIMEOUT_SECONDS: u64 = 120; pub const DEFAULT_PROXY_RESPONSE_HEADER_TIMEOUT_SECONDS: u64 = 120;
pub const DEFAULT_PROXY_READ_TIMEOUT_SECONDS: u64 = 60; pub const DEFAULT_PROXY_READ_TIMEOUT_SECONDS: u64 = 60;
pub const DEFAULT_PROXY_SEND_TIMEOUT_SECONDS: u64 = 60; pub const DEFAULT_PROXY_SEND_TIMEOUT_SECONDS: u64 = 60;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
pub struct ProxyTimeouts { pub struct ProxyTimeouts {
@@ -53,7 +50,6 @@ pub enum MethodPolicyMode {
Allow, Allow,
Block, Block,
} }
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
pub struct MethodPolicy { pub struct MethodPolicy {
@@ -78,7 +74,6 @@ impl MethodPolicy {
); );
Ok(()) Ok(())
} }
pub fn allows(&self, method: &http::Method) -> bool { pub fn allows(&self, method: &http::Method) -> bool {
let listed = self let listed = self
.methods .methods
@@ -90,14 +85,12 @@ impl MethodPolicy {
MethodPolicyMode::Block => !listed, MethodPolicyMode::Block => !listed,
} }
} }
pub fn allow_header(&self) -> Option<http::HeaderValue> { pub fn allow_header(&self) -> Option<http::HeaderValue> {
(self.mode == MethodPolicyMode::Allow) (self.mode == MethodPolicyMode::Allow)
.then(|| self.methods.join(", ").parse::<http::HeaderValue>().ok()) .then(|| self.methods.join(", ").parse::<http::HeaderValue>().ok())
.flatten() .flatten()
} }
} }
impl ProxyTimeouts { impl ProxyTimeouts {
pub fn validate(&self) -> Result<()> { pub fn validate(&self) -> Result<()> {
for (name, value, max) in [ for (name, value, max) in [
@@ -136,7 +129,6 @@ impl ProxyTimeouts {
.map(std::time::Duration::from_secs) .map(std::time::Duration::from_secs)
} }
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum PathMatch { pub enum PathMatch {
@@ -148,7 +140,6 @@ pub enum PathMatch {
Regex, Regex,
RegexInsensitive, RegexInsensitive,
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum RewriteMode { pub enum RewriteMode {
@@ -301,6 +292,9 @@ pub struct Vhost {
/// Main source for the vhost fallback. Missing in older configs means Reverse Proxy. /// Main source for the vhost fallback. Missing in older configs means Reverse Proxy.
#[serde(default)] #[serde(default)]
pub source: Source, pub source: Source,
/// Nginx-style internal error_page/proxy_intercept_errors behavior for the main source.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_fallback: Option<ErrorFallback>,
#[serde(default)] #[serde(default)]
pub backends: Vec<PoolMember>, pub backends: Vec<PoolMember>,
#[serde(default)] #[serde(default)]
@@ -461,6 +455,10 @@ pub struct PathRoute {
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub backend_id: Option<String>, pub backend_id: Option<String>,
pub id: String, pub id: String,
/// Internal named location. It is never selected by the public URI matcher and
/// can only be reached through an error fallback from this vhost.
#[serde(default)]
pub internal: bool,
/// Prefix/exact path or a Rust-regex pattern, depending on `match_type`. /// Prefix/exact path or a Rust-regex pattern, depending on `match_type`.
#[serde(default = "root")] #[serde(default = "root")]
pub path_prefix: String, pub path_prefix: String,
@@ -492,4 +490,8 @@ pub struct PathRoute {
/// Local HTTP redirect; mutually exclusive with `response`. /// Local HTTP redirect; mutually exclusive with `response`.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub redirect: Option<LocationRedirect>, pub redirect: Option<LocationRedirect>,
/// Intercept selected upstream/Web Server error statuses and internally dispatch
/// the original request to another internal Custom Location.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_fallback: Option<ErrorFallback>,
} }
+38
View File
@@ -147,6 +147,44 @@ mod default_listener_tests {
.unwrap(); .unwrap();
assert!(vhost.validate().is_ok()); assert!(vhost.validate().is_ok());
} }
#[test]
fn error_fallback_requires_internal_target_and_rejects_cycles() {
let base = json!({
"id": "fallback-test",
"frontend": {"hosts": ["example.test"], "listener_ids": ["http"]},
"backends": [{"id": "origin", "url": "http://127.0.0.1:9001", "allow_private": true}],
"error_fallback": {"target": "redirect-check", "statuses": [503, 404, 404]},
"routes": [{
"id": "redirect-check",
"internal": true,
"path_prefix": "/",
"source": {"type": "reverse_proxy"}
}]
});
let mut valid: Vhost = serde_json::from_value(base.clone()).unwrap();
valid.validate().unwrap();
assert_eq!(valid.error_fallback.as_ref().unwrap().statuses, vec![404, 503]);
let mut public_target: Vhost = serde_json::from_value(base.clone()).unwrap();
public_target.routes[0].internal = false;
assert!(public_target
.validate()
.unwrap_err()
.to_string()
.contains("internal location"));
let mut cyclic: Vhost = serde_json::from_value(base).unwrap();
cyclic.routes[0].error_fallback = Some(ErrorFallback {
target: "redirect-check".into(),
statuses: vec![404],
});
assert!(cyclic
.validate()
.unwrap_err()
.to_string()
.contains("recursive error fallback"));
}
#[test] #[test]
fn collision_explains_addresses_owners_and_protocol_limits() { fn collision_explains_addresses_owners_and_protocol_limits() {
let mut config = Config::default(); let mut config = Config::default();
+56
View File
@@ -4,6 +4,7 @@ impl Default for PathRoute {
source: Source::default(), source: Source::default(),
backend_id: None, backend_id: None,
id: "root".into(), id: "root".into(),
internal: false,
path_prefix: root(), path_prefix: root(),
match_type: PathMatch::Prefix, match_type: PathMatch::Prefix,
enabled: true, enabled: true,
@@ -16,6 +17,7 @@ impl Default for PathRoute {
rewrite: PathRewrite::default(), rewrite: PathRewrite::default(),
response: None, response: None,
redirect: None, redirect: None,
error_fallback: None,
} }
} }
} }
@@ -31,6 +33,7 @@ impl PathRoute {
&& self.enabled && self.enabled
&& self.source == Source::ReverseProxy && self.source == Source::ReverseProxy
&& self.backend_id.is_none() && self.backend_id.is_none()
&& !self.internal
&& self.cache.is_none() && self.cache.is_none()
&& self.acl_profile_id.is_none() && self.acl_profile_id.is_none()
&& self.acl.is_none() && self.acl.is_none()
@@ -42,6 +45,7 @@ impl PathRoute {
&& self.rewrite.value.is_empty() && self.rewrite.value.is_empty()
&& self.response.is_none() && self.response.is_none()
&& self.redirect.is_none() && self.redirect.is_none()
&& self.error_fallback.is_none()
} }
} }
impl Vhost { impl Vhost {
@@ -140,7 +144,15 @@ impl Vhost {
// Normalize the untouched pre-0.16 synthetic root route out of the custom // Normalize the untouched pre-0.16 synthetic root route out of the custom
// location list so adding/removing Custom locations cannot delete fallback. // location list so adding/removing Custom locations cannot delete fallback.
self.routes.retain(|route| !route.is_legacy_main_fallback()); self.routes.retain(|route| !route.is_legacy_main_fallback());
if let Some(fallback) = &mut self.error_fallback {
fallback.validate()?;
}
let mut route_ids = std::collections::HashSet::new();
for route in &mut self.routes { for route in &mut self.routes {
ensure!(
crate::config::valid_id(&route.id) && route_ids.insert(route.id.clone()),
"invalid/duplicate vhost path id"
);
route.source.validate()?; route.source.validate()?;
ensure!( ensure!(
route.response.is_none() || route.redirect.is_none(), route.response.is_none() || route.redirect.is_none(),
@@ -155,11 +167,55 @@ impl Vhost {
route.source.is_reverse_proxy() || route.backend_id.is_none(), route.source.is_reverse_proxy() || route.backend_id.is_none(),
"Web Server location cannot pin a reverse-proxy backend" "Web Server location cannot pin a reverse-proxy backend"
); );
if let Some(fallback) = &mut route.error_fallback {
fallback.validate()?;
}
} }
ensure!( ensure!(
self.routes.len() <= 64, self.routes.len() <= 64,
"at most 64 custom locations per vhost" "at most 64 custom locations per vhost"
); );
let resolve_target = |target: &str| {
self.routes
.iter()
.find(|route| route.id == target && route.enabled && route.internal)
};
if let Some(fallback) = &self.error_fallback {
ensure!(
resolve_target(&fallback.target).is_some(),
"vhost error fallback target must reference an enabled internal location"
);
}
for route in &self.routes {
if let Some(fallback) = &route.error_fallback {
ensure!(
resolve_target(&fallback.target).is_some(),
"location error fallback target must reference an enabled internal location"
);
}
}
// Reject cycles at configuration time; a runtime depth guard remains as a
// second line of defense for hot-reload/state inconsistencies.
let mut starts = vec![None];
starts.extend(self.routes.iter().filter(|route| route.enabled).map(|route| Some(route.id.as_str())));
for start in starts {
let mut seen = std::collections::HashSet::new();
let mut current = start;
loop {
let key = current.unwrap_or("<main>");
ensure!(seen.insert(key.to_owned()), "recursive error fallback loop detected");
let fallback = match current {
None => self.error_fallback.as_ref(),
Some(id) => self
.routes
.iter()
.find(|route| route.id == id)
.and_then(|route| route.error_fallback.as_ref()),
};
let Some(fallback) = fallback else { break };
current = Some(fallback.target.as_str());
}
}
self.load_balancing.validate()?; self.load_balancing.validate()?;
self.proxy_timeouts.validate()?; self.proxy_timeouts.validate()?;
self.rate_limit.validate()?; self.rate_limit.validate()?;
+7
View File
@@ -242,11 +242,14 @@ impl Service {
); );
self.php.custom_params = normalize_fastcgi_params(std::mem::take(&mut self.php.custom_params))?; self.php.custom_params = normalize_fastcgi_params(std::mem::take(&mut self.php.custom_params))?;
self.php.php_values = normalize_php_values(std::mem::take(&mut self.php.php_values))?; self.php.php_values = normalize_php_values(std::mem::take(&mut self.php.php_values))?;
self.php.php_admin_values = normalize_php_values(std::mem::take(&mut self.php.php_admin_values))?;
ensure!(!self.php.php_values.keys().any(|key| self.php.php_admin_values.contains_key(key)), "PHP_VALUE and PHP_ADMIN_VALUE must not define the same directive");
let php_custom_bytes = self let php_custom_bytes = self
.php .php
.custom_params .custom_params
.iter() .iter()
.chain(self.php.php_values.iter()) .chain(self.php.php_values.iter())
.chain(self.php.php_admin_values.iter())
.map(|(name, value)| name.len() + value.len() + 2) .map(|(name, value)| name.len() + value.len() + 2)
.sum::<usize>(); .sum::<usize>();
ensure!( ensure!(
@@ -350,6 +353,7 @@ impl Service {
let php = if self.php.enabled { let php = if self.php.enabled {
Some(CompiledPhp { Some(CompiledPhp {
extensions: self.php.extensions.iter().cloned().collect(), extensions: self.php.extensions.iter().cloned().collect(),
path_info: self.php.path_info,
endpoint: FastCgiEndpoint::parse(&self.php.endpoint)?, endpoint: FastCgiEndpoint::parse(&self.php.endpoint)?,
connect_timeout: Duration::from_millis(self.php.connect_timeout_ms), connect_timeout: Duration::from_millis(self.php.connect_timeout_ms),
request_timeout: Duration::from_millis(self.php.request_timeout_ms), request_timeout: Duration::from_millis(self.php.request_timeout_ms),
@@ -360,11 +364,14 @@ impl Service {
stderr_log_bytes: self.php.stderr_log_bytes, stderr_log_bytes: self.php.stderr_log_bytes,
custom_params: self.php.custom_params.clone(), custom_params: self.php.custom_params.clone(),
php_values: self.php.php_values.clone(), php_values: self.php.php_values.clone(),
php_admin_values: self.php.php_admin_values.clone(),
metrics: Arc::new(crate::web_server::metrics::PhpMetrics::default()),
}) })
} else { } else {
None None
}; };
Ok(CompiledService { Ok(CompiledService {
web_metrics: Arc::new(crate::web_server::metrics::PhpMetrics::default()),
id: self.id.clone(), id: self.id.clone(),
name: self.name.clone(), name: self.name.clone(),
root, root,
+6
View File
@@ -257,6 +257,9 @@ impl Default for Precompressed {
#[serde(default, deny_unknown_fields)] #[serde(default, deny_unknown_fields)]
pub struct Php { pub struct Php {
pub enabled: bool, pub enabled: bool,
/// Safely split `/script.php/extra/path` into SCRIPT_FILENAME + PATH_INFO.
/// Disabled by default because it changes URL-to-script resolution semantics.
pub path_info: bool,
#[serde(default = "default_php_extensions")] #[serde(default = "default_php_extensions")]
pub extensions: Vec<String>, pub extensions: Vec<String>,
/// `unix:/run/php/php-fpm.sock` or `tcp:127.0.0.1:9000`. /// `unix:/run/php/php-fpm.sock` or `tcp:127.0.0.1:9000`.
@@ -282,12 +285,14 @@ pub struct Php {
pub custom_params: BTreeMap<String, String>, pub custom_params: BTreeMap<String, String>,
/// Trusted operator-defined PHP INI overrides encoded through a single PHP_VALUE parameter. /// Trusted operator-defined PHP INI overrides encoded through a single PHP_VALUE parameter.
pub php_values: BTreeMap<String, String>, pub php_values: BTreeMap<String, String>,
pub php_admin_values: BTreeMap<String, String>,
} }
impl Default for Php { impl Default for Php {
fn default() -> Self { fn default() -> Self {
Self { Self {
enabled: false, enabled: false,
path_info: false,
extensions: default_php_extensions(), extensions: default_php_extensions(),
endpoint: "unix:/run/php/php-fpm.sock".into(), endpoint: "unix:/run/php/php-fpm.sock".into(),
connect_timeout_ms: default_php_connect_timeout_ms(), connect_timeout_ms: default_php_connect_timeout_ms(),
@@ -299,6 +304,7 @@ impl Default for Php {
stderr_log_bytes: default_php_stderr_log_bytes(), stderr_log_bytes: default_php_stderr_log_bytes(),
custom_params: BTreeMap::new(), custom_params: BTreeMap::new(),
php_values: BTreeMap::new(), php_values: BTreeMap::new(),
php_admin_values: BTreeMap::new(),
} }
} }
} }
+7 -3
View File
@@ -24,23 +24,27 @@ impl FastCgiEndpoint {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(super) struct CompiledPhp { pub(crate) struct CompiledPhp {
pub(super) extensions: HashSet<String>, pub(super) extensions: HashSet<String>,
pub(super) path_info: bool,
pub(super) endpoint: FastCgiEndpoint, pub(super) endpoint: FastCgiEndpoint,
pub(super) connect_timeout: Duration, pub(super) connect_timeout: Duration,
pub(super) request_timeout: Duration, pub(super) request_timeout: Duration,
pub(super) permits: Arc<Semaphore>, pub(crate) permits: Arc<Semaphore>,
pub(super) pass_authorization: bool, pub(super) pass_authorization: bool,
pub(super) max_request_body_bytes: u64, pub(super) max_request_body_bytes: u64,
pub(super) max_response_header_bytes: usize, pub(super) max_response_header_bytes: usize,
pub(super) stderr_log_bytes: usize, pub(super) stderr_log_bytes: usize,
pub(super) custom_params: BTreeMap<String, String>, pub(super) custom_params: BTreeMap<String, String>,
pub(super) php_values: BTreeMap<String, String>, pub(super) php_values: BTreeMap<String, String>,
pub(super) php_admin_values: BTreeMap<String, String>,
pub(crate) metrics: Arc<crate::web_server::metrics::PhpMetrics>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CompiledService { pub struct CompiledService {
pub id: String, pub id: String,
pub(crate) web_metrics: Arc<crate::web_server::metrics::PhpMetrics>,
pub name: String, pub name: String,
pub(super) root: PathBuf, pub(super) root: PathBuf,
pub(super) index: Vec<String>, pub(super) index: Vec<String>,
@@ -57,7 +61,7 @@ pub struct CompiledService {
pub(super) download_extensions: HashSet<String>, pub(super) download_extensions: HashSet<String>,
pub(super) symlink_policy: SymlinkPolicy, pub(super) symlink_policy: SymlinkPolicy,
pub(super) script_extensions: HashSet<String>, pub(super) script_extensions: HashSet<String>,
pub(super) php: Option<CompiledPhp>, pub(crate) php: Option<CompiledPhp>,
pub(super) htaccess: Option<htaccess::Compiled>, pub(super) htaccess: Option<htaccess::Compiled>,
pub(super) hardening: Hardening, pub(super) hardening: Hardening,
#[cfg(unix)] #[cfg(unix)]
+9 -1
View File
@@ -123,6 +123,7 @@ pub(super) fn fastcgi_params(
headers: &HeaderMap, headers: &HeaderMap,
original_uri: &Uri, original_uri: &Uri,
script_path: &str, script_path: &str,
path_info: Option<&str>,
query: Option<&str>, query: Option<&str>,
context: &RequestContext, context: &RequestContext,
) -> Result<Vec<u8>> { ) -> Result<Vec<u8>> {
@@ -141,6 +142,9 @@ pub(super) fn fastcgi_params(
params.insert("REQUEST_METHOD".into(), method.as_str().into()); params.insert("REQUEST_METHOD".into(), method.as_str().into());
params.insert("SCRIPT_FILENAME".into(), script_filename); params.insert("SCRIPT_FILENAME".into(), script_filename);
params.insert("SCRIPT_NAME".into(), script_path.into()); params.insert("SCRIPT_NAME".into(), script_path.into());
if let Some(path_info) = path_info.filter(|value| !value.is_empty()) {
params.insert("PATH_INFO".into(), path_info.into());
}
params.insert("QUERY_STRING".into(), query.unwrap_or("").into()); params.insert("QUERY_STRING".into(), query.unwrap_or("").into());
params.insert("REQUEST_URI".into(), request_uri.into()); params.insert("REQUEST_URI".into(), request_uri.into());
params.insert("DOCUMENT_ROOT".into(), document_root); params.insert("DOCUMENT_ROOT".into(), document_root);
@@ -159,7 +163,7 @@ pub(super) fn fastcgi_params(
if context.tls { "on" } else { "off" }.into(), if context.tls { "on" } else { "off" }.into(),
); );
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into()); params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.29.0".into()); params.insert("SERVER_SOFTWARE".into(), concat!("RustProxy/", env!("CARGO_PKG_VERSION")).into());
params.insert("REDIRECT_STATUS".into(), "200".into()); params.insert("REDIRECT_STATUS".into(), "200".into());
if let Some(value) = headers if let Some(value) = headers
.get(CONTENT_TYPE) .get(CONTENT_TYPE)
@@ -213,6 +217,10 @@ pub(super) fn fastcgi_params(
} }
params.insert("PHP_VALUE".into(), value); params.insert("PHP_VALUE".into(), value);
} }
if !php.php_admin_values.is_empty() {
let value = php.php_admin_values.iter().map(|(name, item)| format!("{name}={item}")).collect::<Vec<_>>().join("\n");
params.insert("PHP_ADMIN_VALUE".into(), value);
}
// Request headers cannot synthesize PHP_VALUE/PHP_ADMIN_VALUE because only HTTP_* // Request headers cannot synthesize PHP_VALUE/PHP_ADMIN_VALUE because only HTTP_*
// names are derived above. PHP_VALUE comes exclusively from validated operator config. // names are derived above. PHP_VALUE comes exclusively from validated operator config.
let mut encoded = Vec::with_capacity(4096); let mut encoded = Vec::with_capacity(4096);
+20
View File
@@ -5,6 +5,25 @@ pub(super) async fn php_response(
headers: HeaderMap, headers: HeaderMap,
original_uri: Uri, original_uri: Uri,
script_path: String, script_path: String,
path_info: Option<String>,
query: Option<String>,
body: Body,
context: RequestContext,
) -> Response {
let start = Instant::now();
let response = php_response_inner( service, php, method, headers, original_uri, script_path, path_info, query, body, context).await;
php.metrics.record(response.status(), start.elapsed().as_micros().min(u64::MAX as u128) as u64);
response
}
async fn php_response_inner(
service: &CompiledService,
php: &CompiledPhp,
method: Method,
headers: HeaderMap,
original_uri: Uri,
script_path: String,
path_info: Option<String>,
query: Option<String>, query: Option<String>,
body: Body, body: Body,
context: RequestContext, context: RequestContext,
@@ -45,6 +64,7 @@ pub(super) async fn php_response(
&headers, &headers,
&original_uri, &original_uri,
&script_path, &script_path,
path_info.as_deref(),
query.as_deref(), query.as_deref(),
&context, &context,
) { ) {
+27
View File
@@ -0,0 +1,27 @@
//! Fixed-cardinality PHP counters. Values are per compiled Web Service snapshot.
use std::sync::atomic::{AtomicU64, Ordering};
#[derive(Debug, Default)]
pub(crate) struct PhpMetrics {
pub(crate) requests: AtomicU64,
pub(crate) status_2xx: AtomicU64,
pub(crate) status_3xx: AtomicU64,
pub(crate) status_4xx: AtomicU64,
pub(crate) status_5xx: AtomicU64,
pub(crate) response_headers_us: AtomicU64,
}
impl PhpMetrics {
pub fn record(&self, status: http::StatusCode, elapsed_us: u64) {
self.requests.fetch_add(1, Ordering::Relaxed);
let counter = match status.as_u16() / 100 {
2 => Some(&self.status_2xx),
3 => Some(&self.status_3xx),
4 => Some(&self.status_4xx),
5 => Some(&self.status_5xx),
_ => None,
};
if let Some(counter) = counter { counter.fetch_add(1, Ordering::Relaxed); }
self.response_headers_us.fetch_add(elapsed_us, Ordering::Relaxed);
}
}
+1
View File
@@ -8,6 +8,7 @@ mod config;
mod fastcgi; mod fastcgi;
mod filesystem; mod filesystem;
mod htaccess; mod htaccess;
mod metrics;
mod request; mod request;
mod response; mod response;
mod routing; mod routing;
+64 -12
View File
@@ -30,9 +30,9 @@ pub(super) async fn file_response(
} }
let range_header = headers.get(RANGE).and_then(|value| value.to_str().ok()); let range_header = headers.get(RANGE).and_then(|value| value.to_str().ok());
let range = if range_header.is_some() && if_range_allows(headers, etag.as_deref(), modified) { let ranges = if range_header.is_some() && if_range_allows(headers, etag.as_deref(), modified) {
match range_header.and_then(|value| parse_range(value, length)) { match range_header.and_then(|value| parse_ranges(value, length)) {
Some(range) => Some(range), Some(ranges) => Some(ranges),
None => { None => {
let mut response = Response::new(Body::empty()); let mut response = Response::new(Body::empty());
*response.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE; *response.status_mut() = StatusCode::RANGE_NOT_SATISFIABLE;
@@ -64,25 +64,49 @@ pub(super) async fn file_response(
None None
}; };
let (status, start, body_length) = match range { let multipart = ranges.as_ref().is_some_and(|ranges| ranges.len() > 1);
Some(range) => ( let single_range = ranges.as_ref().and_then(|ranges| (ranges.len() == 1).then_some(ranges[0]));
StatusCode::PARTIAL_CONTENT, let (status, start, body_length) = match single_range {
range.start, Some(range) => (StatusCode::PARTIAL_CONTENT, range.start, range.end - range.start + 1),
range.end - range.start + 1, None if multipart => (StatusCode::PARTIAL_CONTENT, 0, 0),
),
None => (StatusCode::OK, 0, length), None => (StatusCode::OK, 0, length),
}; };
let mut response = Response::new(Body::empty()); let mut response = Response::new(Body::empty());
*response.status_mut() = status; *response.status_mut() = status;
let boundary = multipart.then(|| format!("rustproxy-{:016x}", rand::random::<u64>()));
if let Some(boundary) = &boundary {
response.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_str(&format!("multipart/byteranges; boundary={boundary}")).unwrap(),
);
let mime_text = mime.to_str().unwrap_or("application/octet-stream");
let mut total = 0u64;
for range in ranges.as_ref().expect("multipart range set") {
let header = format!(
"--{boundary}\r\nContent-Type: {mime_text}\r\nContent-Range: bytes {}-{}/{length}\r\n\r\n",
range.start, range.end
);
total = total
.saturating_add(header.len() as u64)
.saturating_add(range.end - range.start + 1)
.saturating_add(2);
}
total = total.saturating_add(format!("--{boundary}--\r\n").len() as u64);
response.headers_mut().insert(
CONTENT_LENGTH,
HeaderValue::from_str(&total.to_string()).unwrap(),
);
} else {
response.headers_mut().insert(CONTENT_TYPE, mime.clone()); response.headers_mut().insert(CONTENT_TYPE, mime.clone());
response.headers_mut().insert( response.headers_mut().insert(
CONTENT_LENGTH, CONTENT_LENGTH,
HeaderValue::from_str(&body_length.to_string()).unwrap(), HeaderValue::from_str(&body_length.to_string()).unwrap(),
); );
}
response response
.headers_mut() .headers_mut()
.insert(ACCEPT_RANGES, HeaderValue::from_static("bytes")); .insert(ACCEPT_RANGES, HeaderValue::from_static("bytes"));
if let Some(range) = range { if let Some(range) = single_range {
response.headers_mut().insert( response.headers_mut().insert(
CONTENT_RANGE, CONTENT_RANGE,
HeaderValue::from_str(&format!("bytes {}-{}/{length}", range.start, range.end)) HeaderValue::from_str(&format!("bytes {}-{}/{length}", range.start, range.end))
@@ -100,11 +124,39 @@ pub(super) async fn file_response(
.headers_mut() .headers_mut()
.insert(VARY, HeaderValue::from_static("Accept-Encoding")); .insert(VARY, HeaderValue::from_static("Accept-Encoding"));
} }
if method == Method::HEAD || body_length == 0 { if method == Method::HEAD || (!multipart && body_length == 0) {
return response; return response;
} }
let mut file = tokio::fs::File::from_std(opened.file); let mut file = tokio::fs::File::from_std(opened.file);
if let Some(boundary) = boundary {
let ranges = ranges.expect("multipart ranges");
let mime_text = mime.to_str().unwrap_or("application/octet-stream").to_owned();
let (mut writer, reader) = tokio::io::duplex(64 * 1024);
tokio::spawn(async move {
use tokio::io::AsyncWriteExt;
for range in ranges {
let header = format!(
"--{boundary}\r\nContent-Type: {mime_text}\r\nContent-Range: bytes {}-{}/{length}\r\n\r\n",
range.start, range.end
);
if writer.write_all(header.as_bytes()).await.is_err()
|| file.seek(std::io::SeekFrom::Start(range.start)).await.is_err()
{
return;
}
let mut limited = (&mut file).take(range.end - range.start + 1);
if tokio::io::copy(&mut limited, &mut writer).await.is_err()
|| writer.write_all(b"\r\n").await.is_err()
{
return;
}
}
let _ = writer.write_all(format!("--{boundary}--\r\n").as_bytes()).await;
});
*response.body_mut() = Body::from_stream(ReaderStream::new(reader));
return response;
}
if start != 0 { if start != 0 {
if file.seek(std::io::SeekFrom::Start(start)).await.is_err() { if file.seek(std::io::SeekFrom::Start(start)).await.is_err() {
return status_response( return status_response(
@@ -185,7 +237,7 @@ pub(super) fn directory_listing(path: &str, entries: &[DirectoryEntry]) -> Vec<u
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>" "<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
)); ));
} }
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.29.0</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#); html.push_str(&format!(r#"</div></section><footer class="page-footer"><span>RustProxy <b>{}</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#, env!("CARGO_PKG_VERSION")));
html.into_bytes() html.into_bytes()
} }
+32 -13
View File
@@ -308,33 +308,52 @@ struct ByteRange {
end: u64, end: u64,
} }
fn parse_range(value: &str, length: u64) -> Option<ByteRange> { const MAX_BYTE_RANGES: usize = 16;
let value = value.strip_prefix("bytes=")?.trim();
if value.contains(',') || value.is_empty() || length == 0 { fn parse_range_item(value: &str, length: u64) -> Result<Option<ByteRange>, ()> {
return None; if length == 0 {
return Ok(None);
} }
let (start, end) = value.split_once('-')?; let (start, end) = value.split_once('-').ok_or(())?;
if start.is_empty() { if start.is_empty() {
let suffix: u64 = end.parse().ok()?; let suffix: u64 = end.parse().map_err(|_| ())?;
if suffix == 0 { if suffix == 0 {
return None; return Ok(None);
} }
let size = suffix.min(length); let size = suffix.min(length);
return Some(ByteRange { return Ok(Some(ByteRange {
start: length - size, start: length - size,
end: length - 1, end: length - 1,
}); }));
} }
let start: u64 = start.parse().ok()?; let start: u64 = start.parse().map_err(|_| ())?;
if start >= length { if start >= length {
return None; return Ok(None);
} }
let end = if end.is_empty() { let end = if end.is_empty() {
length - 1 length - 1
} else { } else {
end.parse::<u64>().ok()?.min(length - 1) end.parse::<u64>().map_err(|_| ())?.min(length - 1)
}; };
(end >= start).then_some(ByteRange { start, end }) Ok((end >= start).then_some(ByteRange { start, end }))
}
fn parse_ranges(value: &str, length: u64) -> Option<Vec<ByteRange>> {
let value = value.strip_prefix("bytes=")?.trim();
if value.is_empty() {
return None;
}
let mut ranges = Vec::new();
for (index, item) in value.split(',').enumerate() {
if index >= MAX_BYTE_RANGES {
return None;
}
match parse_range_item(item.trim(), length).ok()? {
Some(range) => ranges.push(range),
None => continue,
}
}
(!ranges.is_empty()).then_some(ranges)
} }
fn if_range_allows(headers: &HeaderMap, etag: Option<&str>, modified: Option<SystemTime>) -> bool { fn if_range_allows(headers: &HeaderMap, etag: Option<&str>, modified: Option<SystemTime>) -> bool {
+10
View File
@@ -126,6 +126,16 @@ pub(super) async fn select_precompressed(
if !service.precompressed.enabled || components.is_empty() { if !service.precompressed.enabled || components.is_empty() {
return None; return None;
} }
// Multipart byte ranges mix MIME boundaries with representation bytes. Keep
// those responses on the identity representation so Content-Encoding never
// describes only the individual file fragments.
if headers
.get("range")
.and_then(|value| value.to_str().ok())
.is_some_and(|value| value.contains(','))
{
return None;
}
let candidates = [ let candidates = [
(service.precompressed.brotli, "br", ".br"), (service.precompressed.brotli, "br", ".br"),
(service.precompressed.gzip, "gzip", ".gz"), (service.precompressed.gzip, "gzip", ".gz"),
+57 -1
View File
@@ -20,6 +20,35 @@ use http::{
}; };
use std::{io, path::PathBuf}; use std::{io, path::PathBuf};
async fn resolve_php_path_info(
service: &CompiledService,
components: &[String],
) -> Option<(String, String)> {
let php = service.php.as_ref().filter(|php| php.path_info)?;
if components.len() < 2 {
return None;
}
for script_end in 0..components.len() - 1 {
let script_path = format!("/{}", components[..=script_end].join("/"));
if !php.extensions.iter().any(|extension| {
PathBuf::from(&script_path)
.extension()
.and_then(|value| value.to_str())
.is_some_and(|value| value.eq_ignore_ascii_case(extension))
}) {
continue;
}
match open_components(service, components[..=script_end].to_vec()).await {
Ok(opened) if opened.metadata.is_file() => {
let path_info = format!("/{}", components[script_end + 1..].join("/"));
return Some((script_path, path_info));
}
_ => continue,
}
}
None
}
async fn serve_inner( async fn serve_inner(
service: &CompiledService, service: &CompiledService,
method: Method, method: Method,
@@ -104,11 +133,34 @@ async fn serve_inner(
{ {
return builtin_error_response(StatusCode::FORBIDDEN, &method); return builtin_error_response(StatusCode::FORBIDDEN, &method);
} }
let mut request_body = Some(body);
let opened = match open_components(service, components.clone()).await { let opened = match open_components(service, components.clone()).await {
Ok(opened) => opened, Ok(opened) => opened,
Err(error)
if matches!(error.kind(), io::ErrorKind::NotFound | io::ErrorKind::NotADirectory) =>
{
if let Some((script_path, path_info)) = resolve_php_path_info(service, &components).await {
let public_path = PathBuf::from(&script_path);
if let Some(php) = php_for_path(service, &public_path) {
return php_response(
service,
php,
method,
headers,
uri,
script_path,
Some(path_info),
effective_query,
request_body.take().unwrap_or_else(Body::empty),
context,
)
.await;
}
}
return io_response(&error);
}
Err(error) => return io_response(&error), Err(error) => return io_response(&error),
}; };
let mut request_body = Some(body);
if opened.metadata.is_file() { if opened.metadata.is_file() {
let public_path = PathBuf::from(&effective_path); let public_path = PathBuf::from(&effective_path);
@@ -120,6 +172,7 @@ async fn serve_inner(
headers, headers,
uri, uri,
effective_path, effective_path,
None,
effective_query, effective_query,
request_body.take().unwrap_or_else(Body::empty), request_body.take().unwrap_or_else(Body::empty),
context, context,
@@ -172,6 +225,7 @@ async fn serve_inner(
headers, headers,
uri, uri,
script_path, script_path,
None,
effective_query, effective_query,
request_body.take().unwrap_or_else(Body::empty), request_body.take().unwrap_or_else(Body::empty),
context, context,
@@ -289,9 +343,11 @@ pub async fn serve(
body: Body, body: Body,
context: RequestContext, context: RequestContext,
) -> Response { ) -> Response {
let start = std::time::Instant::now();
let mut response = serve_inner(service, method, headers, uri, path, body, context).await; let mut response = serve_inner(service, method, headers, uri, path, body, context).await;
add_service_headers(&mut response, service); add_service_headers(&mut response, service);
htaccess::apply_response(service.htaccess.as_ref(), &mut response, path); htaccess::apply_response(service.htaccess.as_ref(), &mut response, path);
strip_unnecessary_headers(&mut response, service); strip_unnecessary_headers(&mut response, service);
service.web_metrics.record(response.status(), start.elapsed().as_micros().min(u64::MAX as u128) as u64);
response response
} }
+37
View File
@@ -182,6 +182,43 @@ async fn validators_and_single_ranges_work() {
assert_eq!(response.headers()[CONTENT_LENGTH], "10"); assert_eq!(response.headers()[CONTENT_LENGTH], "10");
} }
#[tokio::test]
async fn multipart_byte_ranges_stream_each_requested_part() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("file.txt"), b"0123456789").unwrap();
let service = service(tmp.path(), false);
let mut ranged = request(Method::GET, "/file.txt");
ranged
.headers_mut()
.insert(RANGE, "bytes=0-1,6-8".parse().unwrap());
let response = serve_request(&service, &ranged, "/file.txt").await;
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
let content_type = response.headers()[CONTENT_TYPE].to_str().unwrap().to_owned();
assert!(content_type.starts_with("multipart/byteranges; boundary="));
assert!(!response.headers().contains_key(CONTENT_RANGE));
let body = response.into_body().collect().await.unwrap().to_bytes();
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains("Content-Range: bytes 0-1/10"));
assert!(body.contains("Content-Range: bytes 6-8/10"));
assert!(body.contains("\r\n01\r\n"));
assert!(body.contains("\r\n678\r\n"));
}
#[tokio::test]
async fn too_many_byte_ranges_are_rejected() {
let tmp = TempDir::new().unwrap();
std::fs::write(tmp.path().join("file.txt"), b"0123456789").unwrap();
let service = service(tmp.path(), false);
let ranges = std::iter::repeat("0-0").take(17).collect::<Vec<_>>().join(",");
let mut ranged = request(Method::GET, "/file.txt");
ranged
.headers_mut()
.insert(RANGE, format!("bytes={ranges}").parse().unwrap());
let response = serve_request(&service, &ranged, "/file.txt").await;
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
assert_eq!(response.headers()[CONTENT_RANGE], "bytes */10");
}
#[tokio::test] #[tokio::test]
async fn listing_escapes_html_and_encodes_urls() { async fn listing_escapes_html_and_encodes_urls() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
+1 -1
View File
@@ -202,7 +202,7 @@ async fn empty_index_can_go_directly_to_directory_listing() {
let body = std::str::from_utf8(&body).unwrap(); let body = std::str::from_utf8(&body).unwrap();
assert!(body.contains("file.txt")); assert!(body.contains("file.txt"));
assert!(body.contains("DIRECTORY LISTING")); assert!(body.contains("DIRECTORY LISTING"));
assert!(body.contains("RustProxy <b>0.29.0</b>")); assert!(body.contains(&format!("RustProxy <b>{}</b>", env!("CARGO_PKG_VERSION"))));
} }
#[tokio::test] #[tokio::test]
+76
View File
@@ -22,6 +22,7 @@ fn fastcgi_never_forwards_proxy_header_as_http_proxy() {
&headers, &headers,
&uri, &uri,
"/index.php", "/index.php",
None,
Some("q=1"), Some("q=1"),
&RequestContext { &RequestContext {
client_ip: "127.0.0.1".parse().unwrap(), client_ip: "127.0.0.1".parse().unwrap(),
@@ -69,6 +70,7 @@ fn php_custom_fastcgi_params_and_php_values_are_validated() {
&uri, &uri,
"/index.php", "/index.php",
None, None,
None,
&RequestContext { &RequestContext {
client_ip: "127.0.0.1".parse().unwrap(), client_ip: "127.0.0.1".parse().unwrap(),
client_port: None, client_port: None,
@@ -86,6 +88,49 @@ fn php_custom_fastcgi_params_and_php_values_are_validated() {
assert!(!encoded.windows(b"HTTP_AUTHORIZATION".len()).any(|window| window == b"HTTP_AUTHORIZATION")); assert!(!encoded.windows(b"HTTP_AUTHORIZATION".len()).any(|window| window == b"HTTP_AUTHORIZATION"));
} }
#[test]
fn fastcgi_path_info_keeps_script_filename_separate() {
let tmp = TempDir::new().unwrap();
let mut configured = Service {
id: "php-path-info".into(),
name: "PHP PATH_INFO".into(),
document_root: tmp.path().to_path_buf(),
php: Php {
enabled: true,
path_info: true,
endpoint: "tcp:127.0.0.1:9000".into(),
..Default::default()
},
..Default::default()
};
configured.validate().unwrap();
let service = configured.compile().unwrap();
assert!(service.php.as_ref().unwrap().path_info);
let uri: Uri = "/index.php/foo/bar?q=1".parse().unwrap();
let encoded = fastcgi_params(
&service,
service.php.as_ref().unwrap(),
&Method::GET,
&HeaderMap::new(),
&uri,
"/index.php",
Some("/foo/bar"),
Some("q=1"),
&RequestContext {
client_ip: "127.0.0.1".parse().unwrap(),
client_port: Some(12345),
server_name: "example.test".into(),
server_port: 80,
tls: false,
version: Version::HTTP_11,
},
)
.unwrap();
assert!(encoded.windows(b"PATH_INFO".len()).any(|window| window == b"PATH_INFO"));
assert!(encoded.windows(b"/foo/bar".len()).any(|window| window == b"/foo/bar"));
assert!(encoded.windows(b"SCRIPT_FILENAME".len()).any(|window| window == b"SCRIPT_FILENAME"));
}
#[test] #[test]
fn php_custom_fastcgi_params_cannot_override_core_or_php_admin_values() { fn php_custom_fastcgi_params_cannot_override_core_or_php_admin_values() {
let tmp = TempDir::new().unwrap(); let tmp = TempDir::new().unwrap();
@@ -209,3 +254,34 @@ async fn hardening_strips_fingerprint_headers_after_source_rules() {
assert!(!response.headers().contains_key("x-powered-by")); assert!(!response.headers().contains_key("x-powered-by"));
assert_eq!(response.headers()["x-custom"], "kept"); assert_eq!(response.headers()["x-custom"], "kept");
} }
#[test]
fn php_admin_values_are_operator_only_and_serialized() {
let tmp = TempDir::new().unwrap();
let mut configured = Service {
id: "php-admin".into(), name: "PHP admin".into(),
document_root: tmp.path().to_path_buf(),
php: Php {
enabled: true, endpoint: "tcp:127.0.0.1:9000".into(),
php_admin_values: BTreeMap::from([("open_basedir".into(), "/var/www/webmail/:/tmp/".into())]),
..Default::default()
}, ..Default::default()
};
configured.validate().unwrap();
let service = configured.compile().unwrap();
let encoded = fastcgi_params(
&service, service.php.as_ref().unwrap(), &Method::GET, &HeaderMap::new(),
&"/index.php".parse().unwrap(), "/index.php", None, None,
&RequestContext { client_ip: "127.0.0.1".parse().unwrap(), client_port: None,
server_name: "webmail.example.test".into(), server_port: 8589, tls: false,
version: Version::HTTP_11 },
).unwrap();
assert!(encoded.windows(b"PHP_ADMIN_VALUE".len()).any(|w| w == b"PHP_ADMIN_VALUE"));
assert!(encoded.windows(b"open_basedir=/var/www/webmail/:/tmp/".len())
.any(|w| w == b"open_basedir=/var/www/webmail/:/tmp/"));
configured.php.php_values.insert("open_basedir".into(), "/".into());
assert!(configured.validate().is_err());
configured.php.php_values.clear();
configured.php.php_admin_values.insert("unsafe".into(), "value\nother=1".into());
assert!(configured.validate().is_err());
}
+41 -13
View File
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
preflight(root) preflight(root)
manifest = tomllib.loads((root / "Cargo.toml").read_text()) manifest = tomllib.loads((root / "Cargo.toml").read_text())
assert manifest["package"]["version"] == "0.29.0" assert manifest["package"]["version"] == "0.29.2"
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12" assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
h3dep = manifest["dependencies"]["h3"] h3dep = manifest["dependencies"]["h3"]
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"] assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
lock_path = root / "Cargo.lock" lock_path = root / "Cargo.lock"
if lock_path.exists(): if lock_path.exists():
lock = lock_path.read_text() lock = lock_path.read_text()
assert 'name = "rust-proxy"\nversion = "0.29.0"' in lock assert 'name = "rust-proxy"\nversion = "0.29.2"' in lock
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries" assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
for release_doc, marker in { for release_doc, marker in {
"README.md": "# RustProxy 0.29.0", "README.md": "# RustProxy 0.29.2",
"CHANGELOG.md": "## 0.29.0", "CHANGELOG.md": "## 0.29.2",
"ROADMAP.md": "## 0.29.0", "ROADMAP.md": "## 0.29.2",
"AGENTS.md": "Current source version: 0.29.0.", "AGENTS.md": "Current source version: 0.29.2.",
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.29.0", "docs/TEST_REPORT.md": "# Verification report - RustProxy 0.29.2",
}.items(): }.items():
assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}" assert marker in (root / release_doc).read_text(), f"stale release identity in {release_doc}"
assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable" assert tomllib.loads((root / "rust-toolchain.toml").read_text())["toolchain"]["channel"] == "stable"
@@ -285,7 +285,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
assert '"StreamMetrics"' in openapi and '"streams"' in openapi assert '"StreamMetrics"' in openapi and '"streams"' in openapi
for marker in ['"version": "0.29.0"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']: for marker in ['"version": "0.29.2"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
assert marker in openapi, marker assert marker in openapi, marker
for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']: for marker in ['"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker assert marker in openapi, marker
@@ -1096,7 +1096,7 @@ assert 'throw fieldError(row.querySelector(".docker-agent-listener-choices")' in
assert '#docker-agents-form>.form-actions {' in style_source and 'justify-content: flex-start' in style_source assert '#docker-agents-form>.form-actions {' in style_source and 'justify-content: flex-start' in style_source
deploy_source = read_deploy_source() deploy_source = read_deploy_source()
assert "SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh')" in deploy_source assert "SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh')" in deploy_source
assert 'install_shell_path(prefix)' in deploy_source and 'check_shell_path(prefix)' in deploy_source and "'shell_path_action':'install' if args.mode == 'install' else 'check-only'" in deploy_source assert 'install_shell_path(prefix)' in deploy_source and 'check_shell_path(prefix)' in deploy_source and 'install_logrotate(' in deploy_source and "'shell_path_action':'install-or-repair'" in deploy_source
assert 'grid-template-columns: minmax(0, 1fr)' in style_source and '.log-column-option:hover' in style_source assert 'grid-template-columns: minmax(0, 1fr)' in style_source and '.log-column-option:hover' in style_source
assert '.check>span {' in style_source and 'transform: translateY(-1px)' in style_source and '.cache-toolbar-status .check' in style_source and '.docker-toolbar .check' in style_source and 'grid-column: span 3' in style_source assert '.check>span {' in style_source and 'transform: translateY(-1px)' in style_source and '.cache-toolbar-status .check' in style_source and '.docker-toolbar .check' in style_source and 'grid-column: span 3' in style_source
assert '.docker-alias-field>span:first-child {' in style_source and 'margin-bottom: 0' in style_source and '.docker-alias-editor {' in style_source and 'min-height: 38px' in style_source and 'height: 30px' in style_source assert '.docker-alias-field>span:first-child {' in style_source and 'margin-bottom: 0' in style_source and '.docker-alias-editor {' in style_source and 'min-height: 38px' in style_source and 'height: 30px' in style_source
@@ -1396,7 +1396,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241 assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
assert 'location.pathname === "/logging"' in settings_js_0241 assert 'location.pathname === "/logging"' in settings_js_0241
assert '"RuntimeInformation"' in openapi and '"version": "0.29.0"' in openapi assert '"RuntimeInformation"' in openapi and '"version": "0.29.2"' in openapi
# 0.24.1 compiler and hostname-cardinality regressions. # 0.24.1 compiler and hostname-cardinality regressions.
tls_0241 = rust_source(root, "src/tls.rs") tls_0241 = rust_source(root, "src/tls.rs")
@@ -1518,7 +1518,35 @@ assert 'listener-active-item' in runtime_web_0280 and 'listener-active-feature'
assert 'cache-via-enabled' in cache_web_0280 and 'via_header:' in cache_web_0280 assert 'cache-via-enabled' in cache_web_0280 and 'via_header:' in cache_web_0280
assert 'location-header-direction location-header-editor' in vhost_core_0280 assert 'location-header-direction location-header-editor' in vhost_core_0280
# 0.29.0 redirect/health/cache/gzip-log contracts. # 0.29.2 internal-error/multipart/PATH_INFO contracts.
vhost_model_0291 = rust_source(root, "src/vhost.rs")
vhost_validation_0291 = rust_source(root, "src/vhost/validation.rs")
config_snapshot_0291 = rust_source(root, "src/config/internal_routing.rs")
proxy_types_0291 = rust_source(root, "src/proxy/error_fallback.rs")
proxy_response_0291 = rust_source(root, "src/proxy/forward/response.rs")
web_headers_0291 = rust_source(root, "src/web_server/response/headers.rs")
web_file_0291 = rust_source(root, "src/web_server/response/file.rs")
php_protocol_0291 = rust_source(root, "src/web_server/fastcgi/protocol.rs")
php_model_0291 = rust_source(root, "src/web_server/config/model.rs")
prometheus_0291 = rust_source(root, "src/prometheus/core.rs")
for marker_0291 in ['pub struct ErrorFallback', 'ERROR_FALLBACK_STATUSES', 'pub internal: bool', 'pub error_fallback: Option<ErrorFallback>']:
assert marker_0291 in vhost_model_0291, marker_0291
assert 'recursive error fallback loop detected' in vhost_validation_0291
assert 'select_internal_on' in config_snapshot_0291
for marker_0291 in ['MAX_ERROR_FALLBACK_DEPTH', 'MAX_ERROR_FALLBACK_REPLAY_BODY_BYTES', 'execute_error_fallback']:
assert marker_0291 in proxy_types_0291, marker_0291
assert 'execute_error_fallback' in proxy_response_0291
assert 'MAX_BYTE_RANGES: usize = 16' in web_headers_0291 and 'parse_ranges' in web_headers_0291
assert 'multipart/byteranges' in web_file_0291
assert 'pub path_info: bool' in php_model_0291 and 'PATH_INFO' in php_protocol_0291
for marker_0291 in ['rust_proxy_vhost_error_fallback_triggered_total', 'rust_proxy_vhost_error_fallback_successful_total', 'rust_proxy_vhost_error_fallback_failed_total']:
assert marker_0291 not in prometheus_0291, marker_0291
assert 'unavailable: host.transient_unavailable && host.source.is_reverse_proxy()' in rust_source(root, "src/config/routing.rs")
assert 'filter(|_| needs_proxy_pool)' in rust_source(root, "src/config/routing.rs")
assert (root / 'docs/V0.29.1_IMPLEMENTATION.md').is_file()
assert '"ErrorFallback"' in openapi and '"path_info"' in openapi and '"internal"' in openapi
# Retained 0.29.0 redirect/health/cache/gzip-log contracts.
vhost_model_0290 = rust_source(root, "src/vhost/model.rs") vhost_model_0290 = rust_source(root, "src/vhost/model.rs")
vhost_validation_0290 = rust_source(root, "src/vhost/validation.rs") vhost_validation_0290 = rust_source(root, "src/vhost/validation.rs")
config_model_0290 = rust_source(root, "src/config/model.rs") config_model_0290 = rust_source(root, "src/config/model.rs")
@@ -1534,7 +1562,7 @@ for marker_0290 in ['www_redirect', 'redirect: Option<LocationRedirect>']:
for marker_0290 in ['routing_hosts', 'www_redirect_pair']: for marker_0290 in ['routing_hosts', 'www_redirect_pair']:
assert marker_0290 in vhost_validation_0290, marker_0290 assert marker_0290 in vhost_validation_0290, marker_0290
assert 'www_redirect: Option<(String, String)>' in config_model_0290 assert 'www_redirect: Option<(String, String)>' in config_model_0290
assert 'health_check.host.is_none()' in config_routing_0290 and 'HealthMode::Http' in config_routing_0290 assert 'health_check.host.is_none()' in config_routing_0290 and 'HealthMode::Http' in rust_source(root, "src/config/routing.rs")
assert 'canonical_host_redirect' in proxy_redirect_0290 and 'PERMANENT_REDIRECT' in proxy_redirect_0290 assert 'canonical_host_redirect' in proxy_redirect_0290 and 'PERMANENT_REDIRECT' in proxy_redirect_0290
assert 'redirect_location' in rust_source(root, "src/config/snapshot.rs") and 'local_redirect' in route_cache_0290 assert 'redirect_location' in rust_source(root, "src/config/snapshot.rs") and 'local_redirect' in route_cache_0290
assert 'pub struct UnsafeForce' in cache_entry_0290 and 'unsafe_force: UnsafeForce' in cache_entry_0290 assert 'pub struct UnsafeForce' in cache_entry_0290 and 'unsafe_force: UnsafeForce' in cache_entry_0290
@@ -1607,5 +1635,5 @@ for marker_0282 in [
assert 'Zewnętrzne ścieżki plików' in (root / 'langs/pl_pl.json').read_text() assert 'Zewnętrzne ścieżki plików' in (root / 'langs/pl_pl.json').read_text()
assert 'setfacl -m u:rustproxy:--x /etc/ssl/' in (root / 'docs/V0.28.2_IMPLEMENTATION.md').read_text() assert 'setfacl -m u:rustproxy:--x /etc/ssl/' in (root / 'docs/V0.28.2_IMPLEMENTATION.md').read_text()
print("PASS: 0.29.0 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.") print("PASS: 0.29.2 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.")
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.") print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
+6 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Rust-independent documentation/Reader consistency checks for the 0.29.0 release.""" """Rust-independent documentation/Reader consistency checks for the 0.29.2 release."""
from pathlib import Path from pathlib import Path
import re import re
from rust_source import rust_source from rust_source import rust_source
@@ -14,6 +14,7 @@ required = [
DOCS / "README.md", DOCS / "README.md",
DOCS / "WEB_SERVER.md", DOCS / "WEB_SERVER.md",
DOCS / "TODO.md", DOCS / "TODO.md",
DOCS / "V0.29.1_IMPLEMENTATION.md",
DOCS / "V0.29.0_IMPLEMENTATION.md", DOCS / "V0.29.0_IMPLEMENTATION.md",
DOCS / "V0.28.2_IMPLEMENTATION.md", DOCS / "V0.28.2_IMPLEMENTATION.md",
DOCS / "V0.28.1_IMPLEMENTATION.md", DOCS / "V0.28.1_IMPLEMENTATION.md",
@@ -63,16 +64,16 @@ assert '"/docs/content.json"' in admin
readme = (ROOT / "README.md").read_text(encoding="utf-8") readme = (ROOT / "README.md").read_text(encoding="utf-8")
roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8") roadmap = (ROOT / "ROADMAP.md").read_text(encoding="utf-8")
changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8") changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
assert readme.startswith("# RustProxy 0.29.0") assert readme.startswith("# RustProxy 0.29.2")
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive" assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
assert roadmap.startswith("# RustProxy roadmap") and "## 0.29.0" in roadmap and "## 0.28.2" in roadmap and "## 0.28.1" in roadmap and "## 0.27.3" in roadmap and "## 0.27.2" in roadmap and "## 0.27.1" in roadmap and "## 0.27.0" in roadmap and "## 0.26.7" in roadmap and "## 0.26.6" in roadmap and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap assert roadmap.startswith("# RustProxy roadmap") and "## 0.29.2" and "## 0.29.1" in roadmap and "## 0.29.0" in roadmap and "## 0.28.2" in roadmap and "## 0.28.1" in roadmap and "## 0.27.3" in roadmap and "## 0.27.2" in roadmap and "## 0.27.1" in roadmap and "## 0.27.0" in roadmap and "## 0.26.7" in roadmap and "## 0.26.6" in roadmap and "## 0.26.5" in roadmap and "## 0.26.4" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
assert changelog.startswith("# Changelog\n") assert changelog.startswith("# Changelog\n")
for release in ["0.29.0", "0.28.2", "0.28.1", "0.28.0", "0.27.3", "0.27.2", "0.27.1", "0.27.0", "0.26.7", "0.26.6", "0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]: for release in ["0.29.1", "0.29.0", "0.28.2", "0.28.1", "0.28.0", "0.27.3", "0.27.2", "0.27.1", "0.27.0", "0.26.7", "0.26.6", "0.26.5", "0.26.4", "0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
assert f"## {release}" in changelog, release assert f"## {release}" in changelog, release
assert changelog.index("## 0.29.0") < changelog.index("## 0.28.2") < changelog.index("## 0.28.1") < changelog.index("## 0.28.0") < changelog.index("## 0.27.3") < changelog.index("## 0.27.2") < changelog.index("## 0.27.1") < changelog.index("## 0.27.0") < changelog.index("## 0.26.7") < changelog.index("## 0.26.6") < changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5") assert changelog.index("## 0.29.1") < changelog.index("## 0.29.0") < changelog.index("## 0.28.2") < changelog.index("## 0.28.1") < changelog.index("## 0.28.0") < changelog.index("## 0.27.3") < changelog.index("## 0.27.2") < changelog.index("## 0.27.1") < changelog.index("## 0.27.0") < changelog.index("## 0.26.7") < changelog.index("## 0.26.6") < changelog.index("## 0.26.5") < changelog.index("## 0.26.4") < changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog
for developer_only in ["Clean the remaining compiler warnings", "Update package/source/UI/OpenAPI/release identity", "source regressions and update release identity"]: for developer_only in ["Clean the remaining compiler warnings", "Update package/source/UI/OpenAPI/release identity", "source regressions and update release identity"]:
assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}" assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}"
+3 -3
View File
@@ -73,12 +73,12 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
flat = module.parent.with_suffix(".rs") flat = module.parent.with_suffix(".rs")
if flat.is_file(): if flat.is_file():
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}") raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]: for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]:
if not (root / path).is_file(): if not (root / path).is_file():
raise ValueError(f"Incomplete RustProxy 0.29.0 source tree: missing {path}") raise ValueError(f"Incomplete RustProxy 0.29.2 source tree: missing {path}")
text = (root / "src/lib.rs").read_text(encoding="utf-8") text = (root / "src/lib.rs").read_text(encoding="utf-8")
if '#[path = "auth/mod.rs"]' not in text: if '#[path = "auth/mod.rs"]' not in text:
raise ValueError("src/lib.rs is not the 0.29.0 entry point; extract the complete package, not individual files.") raise ValueError("src/lib.rs is not the 0.29.2 entry point; extract the complete package, not individual files.")
return messages return messages
+6 -6
View File
@@ -135,7 +135,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert backup.is_dir() assert backup.is_dir()
assert (backup / "src/obsolete.rs").is_file() assert (backup / "src/obsolete.rs").is_file()
assert not (target / "src/obsolete.rs").exists() assert not (target / "src/obsolete.rs").exists()
assert 'version = "0.29.0"' in (target / "Cargo.toml").read_text(encoding="utf-8") assert 'version = "0.29.2"' in (target / "Cargo.toml").read_text(encoding="utf-8")
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n" assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n" assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
assert (target / "langs/site.json").is_file() assert (target / "langs/site.json").is_file()
@@ -146,7 +146,7 @@ def upgrade_smoke(tmp: Path) -> None:
assert (target / "docker-agent/certificate.go").is_file() assert (target / "docker-agent/certificate.go").is_file()
assert (target / "docker-agent/server.go").is_file() assert (target / "docker-agent/server.go").is_file()
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8")) manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
assert manifest["to_version"] == "0.29.0" assert manifest["to_version"] == "0.29.2"
assert manifest["removed_obsolete_runtime_state"] is True assert manifest["removed_obsolete_runtime_state"] is True
assert not legacy.exists() assert not legacy.exists()
assert not (backup / "legacy-data").exists() assert not (backup / "legacy-data").exists()
@@ -619,7 +619,7 @@ def source_contract_smoke() -> None:
assert marker in admin, marker assert marker in admin, marker
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0] anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
assert '"/api/status"' not in anonymous_block assert '"/api/status"' not in anonymous_block
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.29.0"' in openapi_0241 assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.29.2"' in openapi_0241
assert 'id="panel-logging"' not in html assert 'id="panel-logging"' not in html
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']: for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
assert marker in html, marker assert marker in html, marker
@@ -799,7 +799,7 @@ def source_contract_smoke() -> None:
assert not re.search(r'(?<![A-Za-z])(?:window\.)?(?:alert|confirm)\s*\(', all_admin_js) assert not re.search(r'(?<![A-Za-z])(?:window\.)?(?:alert|confirm)\s*\(', all_admin_js)
assert 'function appConfirm(message, options = {})' in app_js and 'function apiErrorMessage(path, response, value)' in app_js assert 'function appConfirm(message, options = {})' in app_js and 'function apiErrorMessage(path, response, value)' in app_js
deploy_py = read_deploy_source() deploy_py = read_deploy_source()
assert "SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh')" in deploy_py and 'install_shell_path(prefix)' in deploy_py and 'check_shell_path(prefix)' in deploy_py assert "SHELL_PATH_PROFILE = Path('/etc/profile.d/rustproxy-path.sh')" in deploy_py and 'install_shell_path(prefix)' in deploy_py and 'check_shell_path(prefix)' in deploy_py and 'install_logrotate(' in deploy_py
assert 'grid-column: span 3' in ui_css and '.check>span {' in ui_css and 'transform: translateY(-1px)' in ui_css assert 'grid-column: span 3' in ui_css and '.check>span {' in ui_css and 'transform: translateY(-1px)' in ui_css
assert '.cache-toolbar-status .check' in ui_css and '.docker-toolbar .check' in ui_css and '.docker-alias-field>span:first-child {' in ui_css and 'margin-bottom: 0' in ui_css and '.docker-alias-editor {' in ui_css and 'min-height: 38px' in ui_css and 'height: 30px' in ui_css assert '.cache-toolbar-status .check' in ui_css and '.docker-toolbar .check' in ui_css and '.docker-alias-field>span:first-child {' in ui_css and 'margin-bottom: 0' in ui_css and '.docker-alias-editor {' in ui_css and 'min-height: 38px' in ui_css and 'height: 30px' in ui_css
assert 'readOnly: !!editing' not in vhosts_js and 'vhIdRenameConfirm' in vhosts_js and 'v.id === value.id && v.id !== editing' in vhosts_js assert 'readOnly: !!editing' not in vhosts_js and 'vhIdRenameConfirm' in vhosts_js and 'v.id === value.id && v.id !== editing' in vhosts_js
@@ -1109,7 +1109,7 @@ def source_contract_smoke() -> None:
assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main assert '.managed-update' in main and "flush_hot_to_cold_for_update" in main
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8") openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
assert '"/api/config/history/restore"' in openapi assert '"/api/config/history/restore"' in openapi
for marker in ['"version": "0.29.0"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']: for marker in ['"version": "0.29.2"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
assert marker in openapi, marker assert marker in openapi, marker
assert '"software_update"' in openapi and '"update_available"' in openapi assert '"software_update"' in openapi and '"update_available"' in openapi
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']: for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
@@ -1314,7 +1314,7 @@ def main() -> None:
source_contract_smoke() source_contract_smoke()
container_contract_smoke() container_contract_smoke()
grafana_contract_smoke() grafana_contract_smoke()
print("PASS: 0.29.0 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.") print("PASS: 0.29.2 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
if __name__ == "__main__": if __name__ == "__main__":
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env python3
"""Keep executable release identity derived from the Cargo package version."""
from pathlib import Path
import sys
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from scripts.version import project_version
root = Path(__file__).resolve().parents[2]
version = project_version(root)
for name in ('src/main/bootstrap.rs','src/main/tasks.rs','src/web_server/fastcgi/protocol.rs','src/web_server/response/file.rs','dev.sh','scripts/deploy/upgrade.py','make_zip.py'):
text = (root/name).read_text(encoding='utf-8')
assert f'RustProxy {version}' not in text and f'RustProxy/{version}' not in text, f'hardcoded runtime version: {name}'
assert 'env!("CARGO_PKG_VERSION")' in (root/'src/web_server/fastcgi/protocol.rs').read_text()
assert 'env!("CARGO_PKG_VERSION")' in (root/'src/web_server/response/file.rs').read_text()
assert '__RUSTPROXY_VERSION__' in (root/'src/admin/web/index.html').read_text()
assert 'OnceLock<String>' in (root/'src/admin/public.rs').read_text()
assert 'include_str!("web/index.html")' in (root/'src/admin/public.rs').read_text()
assert 'env!("CARGO_PKG_VERSION")' in (root/'src/admin/public.rs').read_text()
assert 'admin_index.html' not in (root/'build.rs').read_text()
assert 'project_version' in (root/'make_zip.py').read_text()
print(f'PASS: executable version comes from Cargo.toml ({version})')
+1 -1
View File
@@ -96,7 +96,7 @@ need(web,
'HeaderValue::from_static("no-store")', 'HeaderValue::from_static("no-store")',
"builtin_error_response(StatusCode::FORBIDDEN, &method)", "builtin_error_response(StatusCode::FORBIDDEN, &method)",
"empty_index_can_go_directly_to_directory_listing", "empty_index_can_go_directly_to_directory_listing",
"RustProxy <b>0.29.0</b>", "env!(\"CARGO_PKG_VERSION\")",
"traversal_and_symlink_escape_are_not_served", "traversal_and_symlink_escape_are_not_served",
"try_files_rewrites_cache_rules_headers_and_downloads_work", "try_files_rewrites_cache_rules_headers_and_downloads_work",
"precompressed_sidecars_keep_mime_validators_and_range_semantics", "precompressed_sidecars_keep_mime_validators_and_range_semantics",
+5 -2
View File
@@ -43,8 +43,8 @@ const ids = [
"web-service-htaccess-strict","web-service-id","web-service-index","web-service-list","web-service-listing", "web-service-htaccess-strict","web-service-id","web-service-index","web-service-list","web-service-listing",
"web-service-mime-types","web-service-name","web-service-new","web-service-php-concurrency", "web-service-mime-types","web-service-name","web-service-new","web-service-php-concurrency",
"web-service-php-connect-timeout","web-service-php-enabled","web-service-php-endpoint","web-service-php-extensions", "web-service-php-connect-timeout","web-service-php-enabled","web-service-php-endpoint","web-service-php-extensions",
"web-service-php-request-timeout","web-service-php-pass-authorization","web-service-php-body-limit","web-service-php-header-limit", "web-service-php-request-timeout","web-service-php-pass-authorization","web-service-php-path-info","web-service-php-body-limit","web-service-php-header-limit",
"web-service-php-stderr-limit","web-service-php-custom-params","web-service-php-values","web-service-php-help", "web-service-php-stderr-limit","web-service-php-custom-params","web-service-php-values","web-service-php-admin-values","web-service-php-help",
"web-service-compression","web-service-compression-options","web-service-compression-zstd","web-service-compression-br","web-service-compression-gzip","web-service-compression-deflate","web-service-precompressed","web-service-precompressed-br","web-service-precompressed-gzip", "web-service-compression","web-service-compression-options","web-service-compression-zstd","web-service-compression-br","web-service-compression-gzip","web-service-compression-deflate","web-service-precompressed","web-service-precompressed-br","web-service-precompressed-gzip",
"web-service-preset","web-service-preset-apply","web-service-preset-summary","web-service-reset", "web-service-preset","web-service-preset-apply","web-service-preset-summary","web-service-reset",
"web-service-response-headers","web-service-rewrites","web-service-root","web-service-search", "web-service-response-headers","web-service-rewrites","web-service-root","web-service-search",
@@ -60,6 +60,7 @@ elements.get("web-service-php-connect-timeout").value = "1000";
elements.get("web-service-php-request-timeout").value = "30000"; elements.get("web-service-php-request-timeout").value = "30000";
elements.get("web-service-php-concurrency").value = "64"; elements.get("web-service-php-concurrency").value = "64";
elements.get("web-service-php-pass-authorization").checked = true; elements.get("web-service-php-pass-authorization").checked = true;
elements.get("web-service-php-path-info").checked = false;
elements.get("web-service-php-body-limit").value = "0"; elements.get("web-service-php-body-limit").value = "0";
elements.get("web-service-php-header-limit").value = "65536"; elements.get("web-service-php-header-limit").value = "65536";
elements.get("web-service-php-stderr-limit").value = "4096"; elements.get("web-service-php-stderr-limit").value = "4096";
@@ -145,6 +146,7 @@ elements.get("web-service-id").value = "web1";
elements.get("web-service-name").value = "web1"; elements.get("web-service-name").value = "web1";
elements.get("web-service-root").value = "/srv/web1"; elements.get("web-service-root").value = "/srv/web1";
elements.get("web-service-php-pass-authorization").checked = false; elements.get("web-service-php-pass-authorization").checked = false;
elements.get("web-service-php-path-info").checked = true;
elements.get("web-service-php-body-limit").value = "1048576"; elements.get("web-service-php-body-limit").value = "1048576";
elements.get("web-service-php-header-limit").value = "131072"; elements.get("web-service-php-header-limit").value = "131072";
elements.get("web-service-php-stderr-limit").value = "8192"; elements.get("web-service-php-stderr-limit").value = "8192";
@@ -163,6 +165,7 @@ if (!saved.php.enabled) throw new Error("Saved WordPress preset lost PHP-FPM ena
if (!saved.precompressed.enabled) throw new Error("Saved WordPress preset lost precompressed enablement"); if (!saved.precompressed.enabled) throw new Error("Saved WordPress preset lost precompressed enablement");
if (saved.compression?.enabled !== true || saved.compression?.gzip !== false) throw new Error("Saved WordPress preset lost compression/codec override"); if (saved.compression?.enabled !== true || saved.compression?.gzip !== false) throw new Error("Saved WordPress preset lost compression/codec override");
if (saved.php.pass_authorization !== false) throw new Error("Saved WordPress preset lost Authorization forwarding choice"); if (saved.php.pass_authorization !== false) throw new Error("Saved WordPress preset lost Authorization forwarding choice");
if (saved.php.path_info !== true) throw new Error("Saved WordPress preset lost PATH_INFO choice");
if (saved.php.max_request_body_bytes !== 1048576 || saved.php.max_response_header_bytes !== 131072 || saved.php.stderr_log_bytes !== 8192) throw new Error("Saved WordPress preset lost advanced FastCGI limits"); if (saved.php.max_request_body_bytes !== 1048576 || saved.php.max_response_header_bytes !== 131072 || saved.php.stderr_log_bytes !== 8192) throw new Error("Saved WordPress preset lost advanced FastCGI limits");
if (saved.php.custom_params.APP_ENV !== "production") throw new Error("Saved WordPress preset lost custom FastCGI parameters"); if (saved.php.custom_params.APP_ENV !== "production") throw new Error("Saved WordPress preset lost custom FastCGI parameters");
if (saved.php.php_values.memory_limit !== "256M") throw new Error("Saved WordPress preset lost PHP_VALUE settings"); if (saved.php.php_values.memory_limit !== "256M") throw new Error("Saved WordPress preset lost PHP_VALUE settings");