v0.26.0
This commit is contained in:
1 parent
d168ee2afa
commit
ddc5c6593f
70 files changed
+2293
-594
No files matched your search
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
|
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
|
||||||
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
|
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
|
||||||
compatibility. Current source version: 0.25.7.
|
compatibility. Current source version: 0.26.0.
|
||||||
|
|
||||||
## Delivery
|
## Delivery
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,15 @@
|
|||||||
> test-only changes and packaging implementation notes were moved to
|
> test-only changes and packaging implementation notes were moved to
|
||||||
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
|
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||||
|
|
||||||
|
## 0.26.0
|
||||||
|
|
||||||
|
- Add persistent protocol-error traffic records for malformed HTTP, TLS ClientHello/handshake failures and timeouts, HTTP/2/HTTP/3 protocol failures, invalid h2c bootstraps and rejected connection/stream limits. These records use the existing bounded external logging queue and do not enter the Live Logs memory ring.
|
||||||
|
- Add a built-in read-only **Logs** explorer at `/logs`, reachable from the top bar and Settings -> Logging, with source, text, event, status-class, client-IP and vhost filters plus bounded pagination and JSON record inspection.
|
||||||
|
- Keep the stored-log explorer independent from Live Logs: it reads only configured access/error files, including existing files when file logging is currently disabled.
|
||||||
|
- Harden local log-file access with regular-file checks and `O_NOFOLLOW` on Unix; the browser API never accepts an arbitrary filesystem path and enforces bounded bytes, lines, sources, offsets and page sizes.
|
||||||
|
- Extend structured access-log fields with `event`, `protocol_error`, `error_detail`, `local_addr` and `tls_sni` so protocol failures can be distinguished from ordinary request records.
|
||||||
|
- Keep SQLite schema 8 and the existing saved configuration format; no data migration is required from 0.25.7.
|
||||||
|
|
||||||
## 0.25.7
|
## 0.25.7
|
||||||
|
|
||||||
- Add one-click vhost enable/disable actions and make the vhost editor Enabled control clearly stand out with an explicit state indicator.
|
- Add one-click vhost enable/disable actions and make the vhost editor Enabled control clearly stand out with an explicit state indicator.
|
||||||
|
|||||||
Generated
+1
-1
@@ -1688,7 +1688,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-proxy"
|
name = "rust-proxy"
|
||||||
version = "0.25.7"
|
version = "0.26.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rust-proxy"
|
name = "rust-proxy"
|
||||||
version = "0.25.7"
|
version = "0.26.0"
|
||||||
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"
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# RustProxy 0.25.7
|
# RustProxy 0.26.0
|
||||||
|
|
||||||
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
|
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
|
||||||
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
|
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
|
||||||
|
|
||||||
**0.25.7 release source.**
|
**0.26.0 release source.**
|
||||||
|
|
||||||
## Layer-4 streams
|
## Layer-4 streams
|
||||||
|
|
||||||
@@ -11,22 +11,24 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a
|
|||||||
|
|
||||||
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
|
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
|
||||||
|
|
||||||
## What changed in 0.25.7
|
## What changed in 0.26.0
|
||||||
|
|
||||||
- Add a one-click enable/disable action to every vhost card while keeping the existing revision/CSRF protected save path.
|
- Persist malformed HTTP, TLS ClientHello/handshake failures and timeouts, invalid h2c traffic, HTTP/2 protocol failures and HTTP/3/QUIC failures that can happen before a normal routed request exists.
|
||||||
- Make the main **Backends** diagnostics table searchable and sortable, with visible columns and sort order saved in the existing account-scoped administration preferences. These view preferences do not modify proxy configuration.
|
- Keep those low-level `protocol_error` events out of the Live Logs memory ring and send them only through enabled structured-log sinks, preventing scanner noise from displacing ordinary live requests.
|
||||||
- Style dashboard scrollbars with the active dark/light theme.
|
- Add the authenticated **Logs** workspace at `/logs`, reachable from the top bar and Settings -> Logging, for bounded filtering and JSON inspection of configured access/error files.
|
||||||
- Export the previously missing backend retry and upstream-client lifecycle metrics to Prometheus for ordinary and Docker-discovered backends, and add matching Grafana lifecycle/retry panels.
|
- Keep file-backed Logs independent from Live Logs: existing configured files can be viewed even when their writers are currently disabled.
|
||||||
- Make the vhost editor's **Enabled** control visually distinct, with an explicit state badge and traffic-impact hint.
|
- Harden access-log creation and stored-log reads with regular-file checks and Unix `O_NOFOLLOW`; the file browser never accepts an arbitrary filesystem path.
|
||||||
- Keep SQLite schema 8 and the existing configuration format; upgrading from 0.25.6 requires no data migration.
|
- Extend standard structured records with `event`, `protocol_error`, `error_detail`, `local_addr` and `tls_sni`.
|
||||||
|
- Keep SQLite schema 8 and the existing saved configuration format; upgrading from 0.25.7 requires no data migration.
|
||||||
|
|
||||||
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.25.7 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
|
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.26.0 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- `/docs` - authenticated project documentation Reader plus the API/OpenAPI tab.
|
- `/docs` - authenticated project documentation Reader plus the API/OpenAPI tab.
|
||||||
- [docs/README.md](docs/README.md) - canonical documentation index.
|
- [docs/README.md](docs/README.md) - canonical documentation index.
|
||||||
- [docs/WEB_SERVER.md](docs/WEB_SERVER.md) - Web Server configuration, HTTP behavior and filesystem security.
|
- [docs/WEB_SERVER.md](docs/WEB_SERVER.md) - Web Server configuration, HTTP behavior and filesystem security.
|
||||||
|
- [docs/FILE_LOGS.md](docs/FILE_LOGS.md) - stored-log explorer, protocol-error records, bounds and filesystem security.
|
||||||
- [docs/PHP_FPM.md](docs/PHP_FPM.md) - implemented FastCGI/PHP-FPM integration, safety model and runtime requirements.
|
- [docs/PHP_FPM.md](docs/PHP_FPM.md) - implemented FastCGI/PHP-FPM integration, safety model and runtime requirements.
|
||||||
- [docs/V0.25.5_AUDIT.md](docs/V0.25.5_AUDIT.md) - 0.25.5 Cloudflare country analytics and Revision History styling release audit.
|
- [docs/V0.25.5_AUDIT.md](docs/V0.25.5_AUDIT.md) - 0.25.5 Cloudflare country analytics and Revision History styling release audit.
|
||||||
- [docs/V0.25.4_AUDIT.md](docs/V0.25.4_AUDIT.md) - historical 0.25.4 configuration-history restore release audit and compiler gate.
|
- [docs/V0.25.4_AUDIT.md](docs/V0.25.4_AUDIT.md) - historical 0.25.4 configuration-history restore release audit and compiler gate.
|
||||||
@@ -97,7 +99,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \
|
|||||||
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked
|
CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked
|
||||||
```
|
```
|
||||||
|
|
||||||
The 0.25.7 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
|
The 0.26.0 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
|
||||||
updates only this package's own version entry. This packaging environment has no Cargo, so the
|
updates only this package's own version entry. This packaging environment has no Cargo, so the
|
||||||
dependency graph was not re-resolved here. On the compiler-enabled deployment host, run the
|
dependency graph was not re-resolved here. On the compiler-enabled deployment host, run the
|
||||||
locked check/test/release gates below and retain the verified lockfile. `dev.sh` can generate a
|
locked check/test/release gates below and retain the verified lockfile. `dev.sh` can generate a
|
||||||
@@ -336,7 +338,7 @@ python3 scripts/release_smoke.py
|
|||||||
# Runs those checks first, then the Rust suite when Cargo is available:
|
# Runs those checks first, then the Rust suite when Cargo is available:
|
||||||
bash dev.sh test
|
bash dev.sh test
|
||||||
# Source-only package, with no Git checkout required:
|
# Source-only package, with no Git checkout required:
|
||||||
python3 make_zip.py /tmp/RustProxy-0.25.7.zip
|
python3 make_zip.py /tmp/RustProxy-0.26.0.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
## New management and test-origin guide
|
## New management and test-origin guide
|
||||||
|
|||||||
+12
@@ -1,5 +1,17 @@
|
|||||||
# RustProxy roadmap
|
# RustProxy roadmap
|
||||||
|
|
||||||
|
## 0.26.0 - Persistent protocol diagnostics and stored-log explorer
|
||||||
|
|
||||||
|
Current direction: 0.25.7 -> 0.26.0
|
||||||
|
|
||||||
|
- [x] Persist malformed HTTP, TLS/QUIC handshake failures, HTTP/2/HTTP/3 protocol failures, invalid h2c attempts and connection-limit rejects through the existing bounded logging sink.
|
||||||
|
- [x] Keep low-level protocol failures out of the Live Logs memory ring so hostile scans cannot displace ordinary live request history.
|
||||||
|
- [x] Add a local read-only **Logs** explorer with bounded server-side filtering and pagination over configured access/error files.
|
||||||
|
- [x] Prevent arbitrary-path browsing and symlink following; enumerate only paths derived from active RustProxy logging configuration.
|
||||||
|
- [x] Add `/logs` dashboard navigation plus a Logging-settings launcher while retaining the existing Live Logs dialog unchanged.
|
||||||
|
- [x] Keep schema 8 and configuration compatibility with 0.25.7.
|
||||||
|
- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host.
|
||||||
|
|
||||||
## 0.25.7 - Vhost operations and backend observability
|
## 0.25.7 - Vhost operations and backend observability
|
||||||
|
|
||||||
Current direction: 0.25.6 -> 0.25.7
|
Current direction: 0.25.6 -> 0.25.7
|
||||||
|
|||||||
+69
-63
@@ -1,19 +1,19 @@
|
|||||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||||
160a7e43d9d963bfa93d0331e56fe039d39e2a4312199634bf3449611207eeb0 AGENTS.md
|
4a5f6bf0b46f663a37e8759e8e8962e081d154b47e859f1faed7b603726c01de AGENTS.md
|
||||||
bc6f9cf316f77dbd4b4b7f79f02a1a77df86ddd97eb7e9f044403e9e425e75ff CHANGELOG.md
|
d951a8cc95cb902e8d1ec6d702198784458465d36d65a4b90d1df165fb2135d5 CHANGELOG.md
|
||||||
d07ad8d8f03ea00e9902b9b07eca1d91f3c8b683e169496c2fbc3ee7af908260 Cargo.lock
|
1af885bbc3120d440ea6c2025f4d1cb2ee29a59f62230f5202f587158864f171 Cargo.lock
|
||||||
99b597e30f1aa5989dd553680f8de1f69663a04ea4f80663f19200f4d23d80d2 Cargo.toml
|
1845fd0b917d70a6b092176a211d49577b81d5c6a75ddb4b33eee790ef299089 Cargo.toml
|
||||||
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
|
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
|
||||||
45e033e8c9f53d919235463f77e3a3585b00f8bfaf196050e34b160d84def69d README.md
|
a33a2aab100f87a4f3a868ca32276dbf1e122b3294711c8543ea1ad3e15ab869 README.md
|
||||||
0fa8f954b10cc8c4a9452b821ece5a5841c5cf8c1581e27df091b5ce7e73300e ROADMAP.md
|
a49f373ddac5ba6a0ad778fb545a7ec82dec6b0c436c5f00b3f94c2ebfacd994 ROADMAP.md
|
||||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||||
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
|
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
|
||||||
f505c829f5256f5bf14320eed034c049b4e4dd568ae2918fd8ea238051759793 debian/README.md
|
e6787f5b31d63a6c7b997562855ac7349680268d6f9a7d55d550f33a702b7ad1 debian/README.md
|
||||||
33e38a974560d7f832b467a9223664e1fc26627259656927cbbcd7053441303c debian/build-package.sh
|
1fa83ad64f747e27822815de2d85221ac4bb20e5003a72112fe9b11d7b95cad8 debian/build-package.sh
|
||||||
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
|
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
|
||||||
037ac5c953ddbb32bcd4f295f0fd259d0f64e003fa9830c9d4fe50afc552958c debian/changelog
|
228319d6ad21c699dc5cc6104686d02d26f41c5d7d8df73fea021896389b314a debian/changelog
|
||||||
c90457b19b1b2f7998d3db59a656d215eb0fab23769197405960457b66d95cf7 debian/control
|
c90457b19b1b2f7998d3db59a656d215eb0fab23769197405960457b66d95cf7 debian/control
|
||||||
dff2ecb9c14623416981f444bc6df1c4e2bf55d330260f36a312d8d42245400f debian/rules
|
dff2ecb9c14623416981f444bc6df1c4e2bf55d330260f36a312d8d42245400f debian/rules
|
||||||
c537d54fd92a9b97c5f6e6ec63bc1276cda324513e2946b341b2193f63b62e6a debian/rustproxy.environment
|
c537d54fd92a9b97c5f6e6ec63bc1276cda324513e2946b341b2193f63b62e6a debian/rustproxy.environment
|
||||||
@@ -24,7 +24,7 @@ cb0db03a2c1b06fa04a11f7690755a0ca90b17f19ed8fdb228810f505c48c78a debian/rustpro
|
|||||||
e8e18df40bcd69d6aa404282679186d9b8256ac63a5b4e80d7f081e8a0095a2e debian/source/format
|
e8e18df40bcd69d6aa404282679186d9b8256ac63a5b4e80d7f081e8a0095a2e debian/source/format
|
||||||
385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md
|
385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md
|
||||||
7b7fdf383bbaea4f37b5c120e98a24e9a075fba56ae7838a31a5152c7edcde02 deploy/rustproxy.service
|
7b7fdf383bbaea4f37b5c120e98a24e9a075fba56ae7838a31a5152c7edcde02 deploy/rustproxy.service
|
||||||
e7d57772dab81cfcb9546a213d7cfad91a8aa381a08e13d25b2c64f0ae046d94 dev.sh
|
162551960700a3a1c68dba29f37248f6a31d6a0d39144559e9653f6657ca4d94 dev.sh
|
||||||
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
|
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
|
||||||
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
|
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
|
||||||
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
|
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
|
||||||
@@ -33,35 +33,36 @@ 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
|
||||||
9df430e38907d04ec62b47f7ed4d0bc06f43199103737050938b8a79bf86c398 docker-agent/model.go
|
9de427ff54b2fba7a8ed5ff356b1b0069bda81f3953b0252638e84efc79b73fb docker-agent/model.go
|
||||||
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
|
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
|
||||||
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
||||||
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
||||||
802a30b793a6ae6a5046489612a23659f33d25e41d0794551381db202daac904 docs/ACME_PROVIDERS.md
|
802a30b793a6ae6a5046489612a23659f33d25e41d0794551381db202daac904 docs/ACME_PROVIDERS.md
|
||||||
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
|
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
|
||||||
d699f2261a0d7854e49e9b8bae44fa37008a7d2f6340a73cdc37e840722ea211 docs/API.md
|
d699f2261a0d7854e49e9b8bae44fa37008a7d2f6340a73cdc37e840722ea211 docs/API.md
|
||||||
7a30b06c88701d1b689e0d402e716400e9e8c4a0cb350e8f8fe4210e165d29c0 docs/API_ACCESS.md
|
e386c03dba02287a864329b9b1993cbfd131764d0755c8d9f87b8b198940abcf docs/API_ACCESS.md
|
||||||
0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md
|
0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md
|
||||||
cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md
|
cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md
|
||||||
9cb338ed07ac1f92ad491e2099a7e3af63e66262417b1fef24d98c6a791d13fa docs/CACHE_DIAGNOSTICS.md
|
9cb338ed07ac1f92ad491e2099a7e3af63e66262417b1fef24d98c6a791d13fa docs/CACHE_DIAGNOSTICS.md
|
||||||
f2f850b9500e7aa297337b54253f76fc3ceddad940a66c40332029adf32d124c docs/CHANGELOG_TECHNICAL_ARCHIVE.md
|
ab12dfc46c71570ec0fb73732e29aa8258ea6ea889a13eaee2837b7c5223e0aa docs/CHANGELOG_TECHNICAL_ARCHIVE.md
|
||||||
db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER.md
|
db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER.md
|
||||||
9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md
|
9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md
|
||||||
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
|
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
|
||||||
9a882f07945b207d032936feb843f29467e4654f8255fa0104d988a54987f982 docs/ERROR_LOGGING.md
|
86bb62f288f2e999994c302716fc9986dba1ac7a26e598dc97cb56a7bc21cfb5 docs/ERROR_LOGGING.md
|
||||||
|
363e46aa4d6ca133a21e47d5a530154fbd1784eb43593b36022c85c66c0e787b docs/FILE_LOGS.md
|
||||||
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
|
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
|
||||||
8cd4109e62ec7106c64e77bb426933d5517b1e04b52574a9bdb21bd6e1884495 docs/INSTALL.md
|
5299c5f58056fea84e88d6d86857fb593d0a7458af3be559476de2bb57939b4d docs/INSTALL.md
|
||||||
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
|
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
|
||||||
b640a87349e125d1cce1b93caed06aeee09ef5acd0824d0ca1472757b696d477 docs/LIVE_LOGS.md
|
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
|
||||||
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
|
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
|
||||||
397afd77e47be6b29b3acf3ee027e52a5ded5733789a998f2eb14073666984f3 docs/MIGRATION.md
|
ba3cbe1f80271326d6e92e98e035ff280bb8ddbf9d602273684dca147e09d172 docs/MIGRATION.md
|
||||||
5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md
|
5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md
|
||||||
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
|
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
|
||||||
e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md
|
e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md
|
||||||
3da047768f32064c5ef0ad7a4c73c4b14930fe7317c54f5650f426af9f484593 docs/NPM_MIGRATION.md
|
3da047768f32064c5ef0ad7a4c73c4b14930fe7317c54f5650f426af9f484593 docs/NPM_MIGRATION.md
|
||||||
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md
|
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md
|
||||||
d27aebd13c71900df076a53e8ea77e3646565d1872085a59c4158de2df385e63 docs/PROMETHEUS.md
|
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
|
||||||
0832608a28022bec27b82a6c4abfbfeac7c4bdbe84572e8b3a430cee8d76efa2 docs/README.md
|
a31cce4c476ce9d449a5083510660a569d97dcf165f36e24d26b100a29d66944 docs/README.md
|
||||||
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
|
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
|
||||||
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
|
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
|
||||||
ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md
|
ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md
|
||||||
@@ -69,9 +70,9 @@ ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.m
|
|||||||
4bf7b3ea0bfe6c6b0ae0b7884b48b10ba29ff3a71611ec96957a3327d3c6ec88 docs/SECRET_STORE_BACKUP.md
|
4bf7b3ea0bfe6c6b0ae0b7884b48b10ba29ff3a71611ec96957a3327d3c6ec88 docs/SECRET_STORE_BACKUP.md
|
||||||
a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.md
|
a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.md
|
||||||
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
||||||
a2be253670072567459010b79f64df6093ccf9614aa249bdaa98917a83caeb31 docs/SETTINGS.md
|
4245408286d8bdc3bed585c2f0928ecd4c69a3e48dfacbac9b952079ec2075b1 docs/SETTINGS.md
|
||||||
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
|
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
|
||||||
93e822855eea1d4c8d76f8e1fbbb3e3a143e6aab24ccd650b3b641921310e124 docs/TEST_REPORT.md
|
d9153240be9cca05079893e10b1649c3d5c9a3cf8517890afe0e3aff1cdd6fad docs/TEST_REPORT.md
|
||||||
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
||||||
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
|
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
|
||||||
a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md
|
a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md
|
||||||
@@ -113,14 +114,14 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
|
|||||||
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
|
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
|
||||||
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
|
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
|
||||||
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
|
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
|
||||||
93164163bb24981267fb67942c1f3a1b92534fa69c09ff0e5b3fd96c083ddf17 grafana/README.md
|
a25dbbfb5f6ebdf23898cfc50828e965d14bbf9f2a397463010a07840cd84f49 grafana/README.md
|
||||||
2ff0048e598edacfd569f24bdc8b577b5c1cf49c8f20322acea4e981ce1849cb grafana/rustproxy-overview.json
|
8909eeefb8186c6c8a140b460501db96a7891cd062b7ad3d4ca754ea2185387c grafana/rustproxy-overview.json
|
||||||
30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh
|
30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh
|
||||||
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
|
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
|
||||||
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
|
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
|
||||||
b1b2d9b5ba3037989f9e0ddfc75acd5b8aff6f27f1b33dc985425dc96add9057 langs/en_US.json
|
e034c5376b65bf23d49f652d5315e87d0d3f28b02c91955055227235121d78a3 langs/en_US.json
|
||||||
ca41421c7a40f066319b806b52f7004ce1736cfe6ab959b81ab773d3bb50d921 langs/pl_pl.json
|
47680dc4c306623890febef47d847f50ed5e8780550c15391f13c3c15ca1c770 langs/pl_pl.json
|
||||||
268f53cc66c33e93217f4e8e143d9e74b979c095c9a53513b7d0337ca502818f make_zip.py
|
d5df6eade5883206f52d6e015ddfd4cac0c904faaa7616c1c5c49ff5bd05035a 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
|
||||||
@@ -137,13 +138,13 @@ ee50f38c037765fab742e888d4b9fc4d874c31c785c6064d4eca3d2c4d24d944 nagios/check_r
|
|||||||
7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml
|
7458b2d74495e6fb1c1edeea4d5a793705476431dadb60627187111f91441e13 rust-toolchain.toml
|
||||||
edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_provider.py
|
edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_provider.py
|
||||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
|
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
|
||||||
7e524285bfa2e3dbde0391257660635b039f23b0b3807e9510df663f757a5ad8 scripts/admin_audit.py
|
a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py
|
||||||
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
|
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
|
||||||
0c73dbd46f19b519ad6dde9e97a0b11841c84473f9285c6d03b2138e95817f86 scripts/check_source.py
|
6a0425b289abfca1bc67d43ce8a586b20ffcb9d839e020ddbb1205c4a6336eb2 scripts/check_source.py
|
||||||
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
|
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
|
||||||
fa03e74191206c0629024405b75cf0bec25d73477fd54e60571f8f793a63b7b7 scripts/deploy_core.py
|
d2e954c80b229f8b4a37b2b401b0ac7611507cb76c03866cf688453c531b903e scripts/deploy_core.py
|
||||||
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
|
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
|
||||||
33f5ada91e81dd509738145a1256e7282e7c0ead1c937459da70ad5b59d739a7 scripts/docs_check.py
|
948d910904143a98574c7fc256dfa2fac17572c6606876e5b64b0a8baf56eaba scripts/docs_check.py
|
||||||
6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py
|
6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py
|
||||||
2e35b7c78887fd56f83ca9017caa168630963a9f47da71dfc53b1a8f9ad436c2 scripts/memory_contracts.py
|
2e35b7c78887fd56f83ca9017caa168630963a9f47da71dfc53b1a8f9ad436c2 scripts/memory_contracts.py
|
||||||
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py
|
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py
|
||||||
@@ -153,8 +154,8 @@ d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a scripts/mock_a
|
|||||||
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py
|
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py
|
||||||
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md
|
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md
|
||||||
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh
|
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh
|
||||||
93eb03e1d26fd6dae3dc798b7284aa939c0c4a57270af3851a44eb2f0c0a7bf0 scripts/preflight.py
|
f9f8a13e3a8689a45db81c785acaa8c1a78757d3b101e430340f0a881424f9d3 scripts/preflight.py
|
||||||
96a4ed125d21ebc38c280dccd13db437e942d4e3656de9f5decc6a96c469080b scripts/release_smoke.py
|
7fdadf0f973aca3c811deb018f1191eee1bcea06446c1e641a80b7096c452bc2 scripts/release_smoke.py
|
||||||
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example
|
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example
|
||||||
3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros
|
3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros
|
||||||
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore
|
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore
|
||||||
@@ -166,11 +167,11 @@ ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_s
|
|||||||
1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py
|
1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py
|
||||||
295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py
|
295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py
|
||||||
a65010f828194885ffc7f0f733937957f28e6049cd978bec1380a2b8bfbd0113 scripts/source_module_contracts.py
|
a65010f828194885ffc7f0f733937957f28e6049cd978bec1380a2b8bfbd0113 scripts/source_module_contracts.py
|
||||||
a7a66a0f81216ce576e2b2a73e0d1eeeab0491f799b25105236767b40f6de88b scripts/swagger_assets.py
|
41fe5df23e28d4b13d5e998906a8c3a18a312fbeb6cb856237cbae58291fa91e scripts/swagger_assets.py
|
||||||
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
|
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
|
||||||
2e81ff452d94b053f3c242edca26b6a5d04cde2b12982da5b757d9120035fe62 scripts/upgrade.py
|
c4097fecc34a7b545ffba7203b928f5edf4ed932936df903d90673fc23fac766 scripts/upgrade.py
|
||||||
43db35e7264b0345592172f2d1f6a6e4919fc4cbde2a9ff787acd7fff1d031d6 scripts/web_assets.py
|
43db35e7264b0345592172f2d1f6a6e4919fc4cbde2a9ff787acd7fff1d031d6 scripts/web_assets.py
|
||||||
769b6d92bde5457564937e03549b4f479315032540195b784ee19f8245f1ff94 scripts/web_server_contracts.py
|
8af399947a8dac09ecf33aecbfe7a4c1fc38722605d936fba65d9715833fa367 scripts/web_server_contracts.py
|
||||||
b8ce3e8fd08865015ce312fae9c0457612af18d6ef4513a6281b9e3212f8d458 scripts/web_service_ui_test.mjs
|
b8ce3e8fd08865015ce312fae9c0457612af18d6ef4513a6281b9e3212f8d458 scripts/web_service_ui_test.mjs
|
||||||
d8e2b51c39252dbb4305512fa9b1ef508ec610968aed316b10514fd160298762 src/acl.rs
|
d8e2b51c39252dbb4305512fa9b1ef508ec610968aed316b10514fd160298762 src/acl.rs
|
||||||
4e3be1ea1be0a81529c7c9021fb9bb77c24e5044acd54094972745443ad2c26a src/acl/tests.rs
|
4e3be1ea1be0a81529c7c9021fb9bb77c24e5044acd54094972745443ad2c26a src/acl/tests.rs
|
||||||
@@ -183,28 +184,30 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi
|
|||||||
55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs
|
55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs
|
||||||
85f00f8157e83a152cc531f4cfecd0b1472ce269be9c8eb1ac17ad0693748b42 src/admin/certificates.rs
|
85f00f8157e83a152cc531f4cfecd0b1472ce269be9c8eb1ac17ad0693748b42 src/admin/certificates.rs
|
||||||
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
|
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
|
||||||
ea4b58f7e46189e0b16606f197ee89f7587b569764470a6b92d37daef67e3afe src/admin/documentation/documents.rs
|
c9e4237e64a11e7252d0b0b65e12adc2513b8232e9d5fbc81ef22ec81671df7a src/admin/documentation/documents.rs
|
||||||
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
|
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
|
||||||
|
b9eb9fff800ea42da1779ea123d6951f3ea8734f664aed577dbe5961f007443f src/admin/file_logs.rs
|
||||||
beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs
|
beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs
|
||||||
468b6062cb806ef489bc107f69d4725dbf9c08d7e18d15b4482ae0f309c15d2f src/admin/mod.rs
|
aeb8351f4d8455cbc81f363862195ce3fda09a07291df7b1eba55cff64861a68 src/admin/mod.rs
|
||||||
6187fcb99a16c0b457e798d7f285bf687f3c164e8719e3d80a570a2e19d501c7 src/admin/openapi.json
|
ae136f17c60ea724906d170799866ef998e90418d53497696ded7424b8beb1b4 src/admin/openapi.json
|
||||||
a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs
|
a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs
|
||||||
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
|
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
|
||||||
ea95e3d74729d652ce467c626a73e433060a195afa31dba5da1fc7ca935678ea src/admin/router.rs
|
d246389975c87d125ebcb8c24282a3729d509cc70e13e0f3b56b4008d80d76b4 src/admin/router.rs
|
||||||
9e2981f9c96140bd6d9ca54e0d03b51b0783360e534a13e3b5cd1bec8c9440fd src/admin/session.rs
|
9e2981f9c96140bd6d9ca54e0d03b51b0783360e534a13e3b5cd1bec8c9440fd src/admin/session.rs
|
||||||
f7938b202919a49fd2ff86ecdf09efadae12e0ffc0a4246a72a76218db11aa88 src/admin/status.rs
|
f7938b202919a49fd2ff86ecdf09efadae12e0ffc0a4246a72a76218db11aa88 src/admin/status.rs
|
||||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||||
9bed1f93a8ef7bea4478b26bc825aacc8825ca70cf26c993fe78ac784aab2526 src/admin/tests.rs
|
e5f973c6fbddc3897bc0e6d9f8e70cbe9279acea6161ffb46e013792e845e1e4 src/admin/tests.rs
|
||||||
5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs
|
5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs
|
||||||
b8866db46f8be4f3f5bcceb5d84d32fe482f65d534b65cc75b4b3e33dba576c0 src/admin/tests/part_02.rs
|
6f8c1e30babb2cc4cb7accf4bfa28338651a469b16341f6e7eaec881dd9abf2b src/admin/tests/part_02.rs
|
||||||
b31d594499d7f8164c14b66dd77971b9985cefd99bd4d3686f9ea26e11e5a59c src/admin/tests/part_03.rs
|
77608ad33da348dbea41d2a7ceadb75b0bcf05336cb5a20b7f2a47502f1e4842 src/admin/tests/part_03.rs
|
||||||
252456477d13c0652a488731c11173649c4816cf67f151cd881f138f1cac2edc src/admin/tests/part_04.rs
|
bf3cc7a41c2575003515cc8212111395aaa0f2169b7d04b6bbd0908a6bc2af0f src/admin/tests/part_04.rs
|
||||||
5f0062abc42a3b81c79088dc0824ab4c81a22c6ea765e77d2527d07d13bffd58 src/admin/tests/part_05.rs
|
5f0062abc42a3b81c79088dc0824ab4c81a22c6ea765e77d2527d07d13bffd58 src/admin/tests/part_05.rs
|
||||||
|
b00210595ea1871cb0e04b4cf7325ade081cd9938bfabec4730344099ed45e9c src/admin/tests/part_06.rs
|
||||||
23c55f23deeec844a9abc2289440c9e0583aaeb72416fc5f3632a43a864c0efd src/admin/web/acme.js
|
23c55f23deeec844a9abc2289440c9e0583aaeb72416fc5f3632a43a864c0efd src/admin/web/acme.js
|
||||||
78e7d54d6536cd600dcbe66f0fc06ec109e121dfee1e57f3ca6e61cc6ae04a6a src/admin/web/app/config-history.js
|
78e7d54d6536cd600dcbe66f0fc06ec109e121dfee1e57f3ca6e61cc6ae04a6a src/admin/web/app/config-history.js
|
||||||
bf987a7652ebe054d02188fce32de3d797254b4740ea818985d5b76013fa3111 src/admin/web/app/core.js
|
a9953d0ed8ba3d301db51dabafeeac42c0697fc0d11a45abef501e790ac14758 src/admin/web/app/core.js
|
||||||
19abc332a1009bcb087fe4ebb58d458739b27480ab50715b03d07fd408302942 src/admin/web/app/runtime.js
|
19abc332a1009bcb087fe4ebb58d458739b27480ab50715b03d07fd408302942 src/admin/web/app/runtime.js
|
||||||
342b5e6401c2d23d680ff86dcc937b29f5673cd42e578c6a57127a6ab7609f2a src/admin/web/app/settings.js
|
2bfe0dc64d4386133b04bf71502ea9ed8ff346ab0023dc588c68496f6abf3ba3 src/admin/web/app/settings.js
|
||||||
d0be71555de13f732fd6ee44438a0110eb08c91e78bbc1aa36a1a84182780210 src/admin/web/cache-panel.js
|
d0be71555de13f732fd6ee44438a0110eb08c91e78bbc1aa36a1a84182780210 src/admin/web/cache-panel.js
|
||||||
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
|
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
|
||||||
3b84a7a08f9f68e2c78fed4523b594c31a251addc5480a69c5f6949908230539 src/admin/web/docker.js
|
3b84a7a08f9f68e2c78fed4523b594c31a251addc5480a69c5f6949908230539 src/admin/web/docker.js
|
||||||
@@ -218,17 +221,18 @@ f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/
|
|||||||
386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html
|
386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html
|
||||||
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
|
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
|
||||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||||
|
26b458e8e82e6cf844b3cda7b6d228db6f5b0560aeaff8c2e3c7113e7c9c69eb src/admin/web/file-logs.js
|
||||||
dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js
|
dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js
|
||||||
f3a946a33a2006a1e175c9c9a3621c51f46ad055d95c19324e5aa21c9711ef13 src/admin/web/index.html
|
c3d38fbf046e287a77c51a5b0182e377a90d7dce7b3fb23c2e42a712e1347d80 src/admin/web/index.html
|
||||||
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
|
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
|
||||||
cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json
|
cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json
|
||||||
a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html
|
a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html
|
||||||
211ebcd166f023ebfbed4b0805fd9942f193d9e2a916864dd634cae997466f3d src/admin/web/login.js
|
dc092f7120b6502e7d322b5f0953f34a131b7af7d1476207a934605ee65f3968 src/admin/web/login.js
|
||||||
e41619e5cb0ba01a3d287748f28eb38bbe7641f9bedade252d286c18e619c400 src/admin/web/management.js
|
e41619e5cb0ba01a3d287748f28eb38bbe7641f9bedade252d286c18e619c400 src/admin/web/management.js
|
||||||
22318f39f7bb1ae98e65737d3c2ad9e63065b8e94ad59bed560c0e80d3a46949 src/admin/web/redirects.js
|
22318f39f7bb1ae98e65737d3c2ad9e63065b8e94ad59bed560c0e80d3a46949 src/admin/web/redirects.js
|
||||||
668f5162fa9f8bbe2fefce4115644a6469e01aa7defc9c7f760c28afd8e2afc7 src/admin/web/settings.js
|
668f5162fa9f8bbe2fefce4115644a6469e01aa7defc9c7f760c28afd8e2afc7 src/admin/web/settings.js
|
||||||
1a30affd6a717095a1763998a5850a79ae0f86b7b694806f2aba2b880735dd1a src/admin/web/streams.js
|
1a30affd6a717095a1763998a5850a79ae0f86b7b694806f2aba2b880735dd1a src/admin/web/streams.js
|
||||||
59eda800b0eadf4ca452bc9c9b1b669394973d96716afbc92ef8e3122b54fcc5 src/admin/web/style.css
|
a68cbb41d43d699aea8f71ec58af347695dfd91f5a4b13d17b96a231fa942749 src/admin/web/style.css
|
||||||
47e98065281c04b4c185d53df4ed0cb12fabc716e52143e6ae3c3c25db62126a src/admin/web/traffic-panel.js
|
47e98065281c04b4c185d53df4ed0cb12fabc716e52143e6ae3c3c25db62126a src/admin/web/traffic-panel.js
|
||||||
d818580e61bbb70da1f758b9231c0667639cd947d875a55adc21dcf9ed1319c2 src/admin/web/transform-panel.js
|
d818580e61bbb70da1f758b9231c0667639cd947d875a55adc21dcf9ed1319c2 src/admin/web/transform-panel.js
|
||||||
ac79a0f5471f110850bf5ceaaee9a3903bb92fb2edc10e969d8fda81f66571c0 src/admin/web/vhosts/core.js
|
ac79a0f5471f110850bf5ceaaee9a3903bb92fb2edc10e969d8fda81f66571c0 src/admin/web/vhosts/core.js
|
||||||
@@ -326,21 +330,23 @@ fe571c85a924c827097f4be01b3a827a16bc5dd17a55fbf991ee3ad016641305 src/docker/tes
|
|||||||
e40171e72327bc4350ba95cedacde2f3c9148c26a5a232d9f790f4449f20a685 src/geoip/tests.rs
|
e40171e72327bc4350ba95cedacde2f3c9148c26a5a232d9f790f4449f20a685 src/geoip/tests.rs
|
||||||
650818b60f76e1ae1a26c6fbf71d7ba63bc85dcb88754a592e8da87b5938e5af src/headers.rs
|
650818b60f76e1ae1a26c6fbf71d7ba63bc85dcb88754a592e8da87b5938e5af src/headers.rs
|
||||||
01cb98570ec388170420883e4ba18b2bf85ad1af97854dbc3a12b135db511c5c src/headers/tests.rs
|
01cb98570ec388170420883e4ba18b2bf85ad1af97854dbc3a12b135db511c5c src/headers/tests.rs
|
||||||
63c421fe3ae93aea00b63c599eb5eb74539a1dcaed42afa9d6275f568e23a377 src/http3.rs
|
1988f116779d725d324fe7b8af0eed071916efaffb38636b55e6276966a36056 src/http3.rs
|
||||||
|
2165a0a4b27d621985e2030326ad52b8bbd59d7b9e4f092f72f7e92b4e58b497 src/http3/body.rs
|
||||||
68bc11160bc5275a904e48af8698b37433096fb6d592364d8281d59bb9e2b3f4 src/http3/tests.rs
|
68bc11160bc5275a904e48af8698b37433096fb6d592364d8281d59bb9e2b3f4 src/http3/tests.rs
|
||||||
3dab8fac75263120e48ee869ad55a3e2ccc2a19fc9f6d6a6fe055657ec55b1e3 src/lib.rs
|
3dab8fac75263120e48ee869ad55a3e2ccc2a19fc9f6d6a6fe055657ec55b1e3 src/lib.rs
|
||||||
6946917d57fb589bdba639fef2cec1d256fac0c8bfde92adffef2b31c3b3e08c src/logging.rs
|
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
|
||||||
1bff8b45d825ec29b44cda4b98b4c9d2e3ad14ff69252e5eb469ecce6e8514a9 src/logging/format.rs
|
d8abc31f130e85b38c760b96e3369639464bca513991532e025485489a0af70e src/logging/error_runtime.rs
|
||||||
147825d3c6dbb4c794c18897703741aa6b60bbfa3dbae0e30b6f770e2a8672ba src/logging/model.rs
|
2ee000906a56219cda4124b5bafad4af326a70ff0baa160d8f920ad90c5ebef9 src/logging/format.rs
|
||||||
348db1112a6067b06f4620ae75d2fc57d275852fef995b52649a6112b8c6589c src/logging/runtime.rs
|
15f88b956b2a4804ba82f493ab20e8bd1ca57920a9fd218e9e3d85867c796d81 src/logging/model.rs
|
||||||
6a07c1eeb6914b863ba7581444486029ca0c7465d2427f251bd1143cbba49448 src/logging/tests.rs
|
c4c2f77e4e64943ac7e5d26688743a19e1349d2c984fec20dcff70373af1c254 src/logging/runtime.rs
|
||||||
|
a978bb38c87336145395d598213718ee9ede41e82e3ab89883fdb8e3b024d944 src/logging/tests.rs
|
||||||
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
||||||
46732ed6d9cf2a33031624f941b8318d2676fb1b6bb5142e96820301f9fa1ac4 src/main/admin_runtime.rs
|
46732ed6d9cf2a33031624f941b8318d2676fb1b6bb5142e96820301f9fa1ac4 src/main/admin_runtime.rs
|
||||||
b3d9ff6e92d3e6c8d54c7476200cacbc69d378e873e10d53ce306ac7132187ea src/main/bootstrap.rs
|
86d62387fb73c221aeed3d0136b4b4954e5feac8533f8b3490b585d17fb743ac 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
|
||||||
c601a96213abeff45dc879ee5a1e107ff60c1f1d657c505f770a81e7e7e4fb25 src/main/tasks.rs
|
7110171b265945d44a45ca09a02c9860e7bbae4e9aec2bc9583c7a65a3fea907 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
|
||||||
@@ -356,7 +362,7 @@ d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus
|
|||||||
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
|
||||||
6fc307286ab24b259237d34c6207dd325b375e1e896f824c66c9805a97bb981f src/proxy/body.rs
|
b4ab7010056ff095e4c4d2f878d2301b8ce626bd2059ef4fd54c3647c16d4f61 src/proxy/body.rs
|
||||||
16b9d4db3b20b2ab14c47ac14fcd18d3d8ca77c3ad9a1f37866ebfa60f379862 src/proxy/body/tests.rs
|
16b9d4db3b20b2ab14c47ac14fcd18d3d8ca77c3ad9a1f37866ebfa60f379862 src/proxy/body/tests.rs
|
||||||
00e50a914511754266b4eae2626244587174bfa71fff91509c530bcde7f0d397 src/proxy/cache_helpers.rs
|
00e50a914511754266b4eae2626244587174bfa71fff91509c530bcde7f0d397 src/proxy/cache_helpers.rs
|
||||||
cfb0910af8cd868bde3f6c9992319e46d5328d6d8f69c50701df0ea12bef2108 src/proxy/forward/cache_lookup.rs
|
cfb0910af8cd868bde3f6c9992319e46d5328d6d8f69c50701df0ea12bef2108 src/proxy/forward/cache_lookup.rs
|
||||||
@@ -393,11 +399,11 @@ b4c17b304166592e63c81884076589fbaf725c3f57043f8924c46167b061ac5b src/secret_sto
|
|||||||
6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs
|
6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs
|
||||||
0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs
|
0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs
|
||||||
53a4bed4d1233aa434712b946857ea2050acaaf5de011c6dadca5b5c22155a83 src/server.rs
|
53a4bed4d1233aa434712b946857ea2050acaaf5de011c6dadca5b5c22155a83 src/server.rs
|
||||||
2a48d22cdba034f1ba015b657ba7966458b89e411796f286997b728c7101a2d6 src/server/http.rs
|
a5495ac40a75d8c94b6f1c47b7e9c0946f836d40735464d4d18ce6705541d063 src/server/http.rs
|
||||||
8c6ca5eaffb9acab57199d0349722703b903cfff3752baad2cf2596bd51188de src/server/listeners.rs
|
8c6ca5eaffb9acab57199d0349722703b903cfff3752baad2cf2596bd51188de src/server/listeners.rs
|
||||||
59f087070279171d7d23a84ba4f5befb617d8bfc34b7fd6493abb037c46b1768 src/server/serve.rs
|
c362593f2feb7cb3b22b2a4fb22f8afa128e72ca8185111ceb9bd0f8125abe55 src/server/serve.rs
|
||||||
b209c1135aa30c5a2d1edb3f8a80b9b95398b2c2e4c03139b59b8a4955f1eb99 src/server/serve/tests.rs
|
b209c1135aa30c5a2d1edb3f8a80b9b95398b2c2e4c03139b59b8a4955f1eb99 src/server/serve/tests.rs
|
||||||
dfee3f5a4a79dbac5edfd81f4f1a63b4cf26757f604b8b41924ecabcbf37e370 src/server/tls_runtime.rs
|
c123c2e1bfc127621a5c8915ad3e0a32f57d557b2fd8760f1cfc537d703e1a02 src/server/tls_runtime.rs
|
||||||
1be88858215d393d4f62f04f36a77ee64ce476be1b642c8f37e059c33423fea7 src/settings.rs
|
1be88858215d393d4f62f04f36a77ee64ce476be1b642c8f37e059c33423fea7 src/settings.rs
|
||||||
b8b3150abe1c6f61d3f593e169795aae397b578607710ea6fd887b90f37b872e src/settings/tests.rs
|
b8b3150abe1c6f61d3f593e169795aae397b578607710ea6fd887b90f37b872e src/settings/tests.rs
|
||||||
566daf9c0e1a30da2dc18e51090541a85ced3320a0fc00ed1d1de3e2b4da5072 src/stream.rs
|
566daf9c0e1a30da2dc18e51090541a85ced3320a0fc00ed1d1de3e2b4da5072 src/stream.rs
|
||||||
@@ -447,7 +453,7 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
|
|||||||
9c1c8d6eedecb95eadbfb23c3caea562f6f7a995606ef988f25b58c2d03bcdd5 src/web_server/config/model.rs
|
9c1c8d6eedecb95eadbfb23c3caea562f6f7a995606ef988f25b58c2d03bcdd5 src/web_server/config/model.rs
|
||||||
6191f4d541476c4a3ae296e7dd28bdb9f8a123fc509b47619f6f44519c44d278 src/web_server/config/validation.rs
|
6191f4d541476c4a3ae296e7dd28bdb9f8a123fc509b47619f6f44519c44d278 src/web_server/config/validation.rs
|
||||||
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
|
86c744216d7132d8294a6e90ba9be5fd871646006313d1c62506d40c81db9b12 src/web_server/fastcgi.rs
|
||||||
a490ec8fc283efe9c91e8e8ffd00926133444f51a9d2390aab63c5db234e85be src/web_server/fastcgi/protocol.rs
|
7fd2d89aa016f03e602c140c49ea24c95a66a90abfa6de186e6806b224e68252 src/web_server/fastcgi/protocol.rs
|
||||||
134c4998f6a0aa27e121af1ae3355b7e335488b2ae867ac0a934c58b4eb77191 src/web_server/fastcgi/response.rs
|
134c4998f6a0aa27e121af1ae3355b7e335488b2ae867ac0a934c58b4eb77191 src/web_server/fastcgi/response.rs
|
||||||
8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs
|
8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs
|
||||||
0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs
|
0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs
|
||||||
@@ -457,14 +463,14 @@ a490ec8fc283efe9c91e8e8ffd00926133444f51a9d2390aab63c5db234e85be src/web_server
|
|||||||
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
|
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
|
||||||
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
|
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
|
||||||
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
|
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
|
||||||
9988a784cb025c63c230389da138deab40fae2716aaa85c4dfeec909bdc1adf1 src/web_server/response/file.rs
|
6c1b6b61fadb978620bb6906e535e1b96ac9cb3f062b6178931a8fc267bd8191 src/web_server/response/file.rs
|
||||||
9aa68251a5f549f4ce3b1bda88d4abb9efc629caf444989381b4117c9488e37b src/web_server/response/headers.rs
|
9aa68251a5f549f4ce3b1bda88d4abb9efc629caf444989381b4117c9488e37b src/web_server/response/headers.rs
|
||||||
2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs
|
2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs
|
||||||
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
|
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
|
||||||
3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs
|
3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs
|
||||||
cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs
|
cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs
|
||||||
c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs
|
c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs
|
||||||
5ac754a25cc0009015d5937522224195a695abda9e1f4e121d824b22f729dbff src/web_server/tests/part_02.rs
|
8c15c626e879016d5d3e3f0c30af42aa9acd18ce55b6965976ca6895e0af9028 src/web_server/tests/part_02.rs
|
||||||
e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs
|
e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs
|
||||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||||
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
|
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# Debian packaging
|
# Debian packaging
|
||||||
|
|
||||||
This directory builds the `rustproxy` binary package for RustProxy 0.25.7.
|
This directory builds the `rustproxy` binary package for RustProxy 0.26.0.
|
||||||
The package installs:
|
The package installs:
|
||||||
|
|
||||||
- `/usr/bin/rust-proxy`
|
- `/usr/bin/rust-proxy`
|
||||||
|
|||||||
Vendored
+1
-1
@@ -21,6 +21,6 @@ cargo fetch --locked
|
|||||||
dpkg-buildpackage -us -uc -b
|
dpkg-buildpackage -us -uc -b
|
||||||
|
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.25.7_*.deb' -o -name 'rustproxy_0.25.7_*.changes' -o -name 'rustproxy_0.25.7_*.buildinfo' \) \
|
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.26.0_*.deb' -o -name 'rustproxy_0.26.0_*.changes' -o -name 'rustproxy_0.26.0_*.buildinfo' \) \
|
||||||
-exec cp -f {} dist/ \;
|
-exec cp -f {} dist/ \;
|
||||||
echo "Packages copied to $ROOT/dist"
|
echo "Packages copied to $ROOT/dist"
|
||||||
Vendored
+8
@@ -1,3 +1,11 @@
|
|||||||
|
rustproxy (0.26.0) stable; urgency=medium
|
||||||
|
|
||||||
|
* Persist malformed HTTP, TLS/QUIC and HTTP/2/HTTP/3 protocol failures through bounded logging sinks.
|
||||||
|
* Add the authenticated read-only Logs explorer for configured access/error files.
|
||||||
|
* Keep Live Logs independent from file-backed protocol diagnostics and retain schema 8.
|
||||||
|
|
||||||
|
-- RustProxy Maintainers <noreply@example.invalid> Mon, 05 Oct 2026 15:38:00 +0200
|
||||||
|
|
||||||
rustproxy (0.25.7) stable; urgency=medium
|
rustproxy (0.25.7) stable; urgency=medium
|
||||||
|
|
||||||
* Add fast vhost enable/disable controls and improve the vhost Enabled editor control.
|
* Add fast vhost enable/disable controls and improve the vhost Enabled editor control.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi
|
|||||||
case "$mode" in
|
case "$mode" in
|
||||||
help|-h|--help)
|
help|-h|--help)
|
||||||
cat <<'HELP'
|
cat <<'HELP'
|
||||||
RustProxy 0.25.7
|
RustProxy 0.26.0
|
||||||
./dev.sh Build and start proxy + local panel
|
./dev.sh Build and start proxy + local panel
|
||||||
./dev.sh run [serve flags] Same, with extra server options
|
./dev.sh run [serve flags] Same, with extra server options
|
||||||
./dev.sh admin setup-token Generate/print a one-time setup capability before first login
|
./dev.sh admin setup-token Generate/print a one-time setup capability before first login
|
||||||
@@ -139,7 +139,7 @@ if [[ "$mode" == "test" ]]; then
|
|||||||
printf 'Checking all Rust targets before tests...\n' >&2
|
printf 'Checking all Rust targets before tests...\n' >&2
|
||||||
cargo check --locked --all-targets
|
cargo check --locked --all-targets
|
||||||
fi
|
fi
|
||||||
printf 'Building RustProxy 0.25.7...\n' >&2
|
printf 'Building RustProxy 0.26.0...\n' >&2
|
||||||
cargo build --locked
|
cargo build --locked
|
||||||
BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
|
BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
|
||||||
case "$mode" in
|
case "$mode" in
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
const version = "0.25.7"
|
const version = "0.26.0"
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
Listen string
|
Listen string
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# Administration API access - 0.25.7
|
# Administration API access - 0.26.0
|
||||||
|
|
||||||
## Sign-in boundary
|
## Sign-in boundary
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,14 @@
|
|||||||
|
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.26.0
|
||||||
|
|
||||||
|
### Compiler follow-up
|
||||||
|
|
||||||
|
- Corrected the external `cargo` build failure in `server::tls_runtime`: the HTTP/1 service closure now captures its own cloned `frontend_state`, leaving the original state available for connection-level protocol-error logging after `serve_connection` returns.
|
||||||
|
- Corrected `serde_json::Map::entry` type inference in the stored-log normalizer by passing the literal `"event"` directly instead of the ambiguous `"event".into()`.
|
||||||
|
- Rust-independent language, security, source-module, documentation, UI, source and release-smoke gates pass after both corrections. A fresh compiler-enabled `cargo check/test` remains required because this packaging workspace has no Rust toolchain.
|
||||||
|
|
||||||
## 0.25.1
|
## 0.25.1
|
||||||
|
|
||||||
### 0.25.1 source-split compiler follow-up
|
### 0.25.1 source-split compiler follow-up
|
||||||
|
|||||||
@@ -37,3 +37,7 @@ Error messages from dependencies should still be treated as untrusted diagnostic
|
|||||||
## Rotation
|
## Rotation
|
||||||
|
|
||||||
RustProxy does not implement file rollover, retention or compression. Use the system `logrotate`. The Debian package configuration in `debian/rustproxy.logrotate` already covers `/var/log/rustproxy/*.log` and uses `copytruncate`, so the default error-log path is included without a RustProxy restart.
|
RustProxy does not implement file rollover, retention or compression. Use the system `logrotate`. The Debian package configuration in `debian/rustproxy.logrotate` already covers `/var/log/rustproxy/*.log` and uses `copytruncate`, so the default error-log path is included without a RustProxy restart.
|
||||||
|
|
||||||
|
## Stored-log explorer
|
||||||
|
|
||||||
|
The authenticated `/logs` workspace can read the configured error file alongside structured access/protocol files. It remains independent from Live Logs and can inspect an existing error file even while `error_log.enabled` is false. The browser API does not accept arbitrary paths; it resolves only the configured error-log path and applies the same bounded, read-only regular-file/symlink protections documented in [FILE_LOGS](FILE_LOGS.md).
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# Stored log explorer
|
||||||
|
|
||||||
|
RustProxy 0.26.0 adds an authenticated, read-only **Logs** workspace at `/logs`. It is a local file-backed explorer for RustProxy logging outputs and is intentionally separate from **Live Logs**.
|
||||||
|
|
||||||
|
## Separation from Live Logs
|
||||||
|
|
||||||
|
Live Logs uses the bounded in-memory request history and Tokio broadcast channel. It remains available even when file logging is disabled and is optimized for current request activity.
|
||||||
|
|
||||||
|
The stored-log explorer does not read that memory ring. It reads only files derived from the active RustProxy logging configuration:
|
||||||
|
|
||||||
|
- the combined structured access-log path, or JSONL files directly inside the configured per-vhost directory;
|
||||||
|
- the configured optional error-log file.
|
||||||
|
|
||||||
|
Existing files remain browsable after their writer is disabled. Disabling file logging therefore stops new persistent access/protocol records but does not remove old files.
|
||||||
|
|
||||||
|
## Protocol and malformed-traffic records
|
||||||
|
|
||||||
|
Ordinary completed requests use `event: "request"`. RustProxy can also persist low-level records that fail before a normal routed HTTP exchange exists. Those records use:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"event": "protocol_error",
|
||||||
|
"protocol_error": "tls_handshake_error",
|
||||||
|
"error_detail": "...",
|
||||||
|
"client_ip": "192.0.2.10",
|
||||||
|
"local_addr": "192.0.2.20:443",
|
||||||
|
"tls_sni": "app.example",
|
||||||
|
"transport": "tls",
|
||||||
|
"http_version": "",
|
||||||
|
"status": 0,
|
||||||
|
"outcome": "protocol_error"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Covered classes include connection-limit rejection, malformed/incomplete HTTP/1 connections, invalid/disabled h2c upgrades, HTTP/2 protocol termination, TLS ClientHello and handshake errors/timeouts, and HTTP/3/QUIC handshake, header, protocol and stream-limit failures.
|
||||||
|
|
||||||
|
These low-level records are sent only through the configured persistent structured-log sinks (file, Logstash and/or Loki). They are deliberately **not** inserted into Live Logs, so scanner noise cannot displace the ordinary bounded live request history. For `protocol_error` events, the diagnostic envelope fields shown above are always emitted even when a custom JSON field mapping is active; custom/static fields are retained, but they cannot hide or overwrite the required protocol-diagnostic fields.
|
||||||
|
|
||||||
|
Dependency error text is secret-redacted and capped before it enters the persistent record. Raw packets, request bodies and raw malformed request bytes are not captured.
|
||||||
|
|
||||||
|
## Browser security model
|
||||||
|
|
||||||
|
The browser never supplies a filesystem path. `/api/file-logs` returns opaque source IDs derived from the current configuration, and `/api/file-logs/query` accepts only those IDs or `all`.
|
||||||
|
|
||||||
|
The reader is bounded:
|
||||||
|
|
||||||
|
- up to 2 MiB scanned from the tail of one source;
|
||||||
|
- up to 8 MiB across one query;
|
||||||
|
- up to 20,000 lines across the complete query, shared fairly across selected sources;
|
||||||
|
- at most 32 sources in an `all` query;
|
||||||
|
- page size at most 500 and offset at most 10,000.
|
||||||
|
|
||||||
|
File reads run in a blocking worker rather than the async proxy runtime. Only regular files are accepted. On Unix the reader opens with `O_NOFOLLOW`; a symlink is not followed. Per-vhost enumeration also rejects a configured directory that is itself a symlink and only lists direct regular `.jsonl` children.
|
||||||
|
|
||||||
|
The endpoints are private administration routes and inherit the existing panel session/Host/Origin access controls. There is no public-proxy route to the file reader.
|
||||||
|
|
||||||
|
## Filters
|
||||||
|
|
||||||
|
The workspace supports source, case-insensitive text, event class, HTTP status class, client IP and vhost filters. Results are paged from the bounded scanned tail. Selecting a row shows the complete parsed JSON object. RAW error-log lines are wrapped as a record with `event: "error"` and a `raw` field.
|
||||||
|
|
||||||
|
The explorer is diagnostic rather than a retention system. RustProxy does not add rollover, indexing or compression; use the existing operating-system log rotation policy for retention.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# Installing and updating RustProxy 0.25.7
|
# Installing and updating RustProxy 0.26.0
|
||||||
|
|
||||||
The scripts prepare a Linux/systemd installation. This source archive is **not a
|
The scripts prepare a Linux/systemd installation. This source archive is **not a
|
||||||
verified production build**: Rust/Cargo, real CA operations, live systemd startup
|
verified production build**: Rust/Cargo, real CA operations, live systemd startup
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ The CLI first probes the loopback administration `/healthz` endpoint over HTTP.
|
|||||||
|
|
||||||
This fixes the previous reconnect loop where `--livelogs` treated the TLS listener's expected HTTP-to-HTTPS `308` response as a stream failure. The same local transport selection is used by CLI config and certificate API operations.
|
This fixes the previous reconnect loop where `--livelogs` treated the TLS listener's expected HTTP-to-HTTPS `308` response as a stream failure. The same local transport selection is used by CLI config and certificate API operations.
|
||||||
|
|
||||||
The live path is separate from configured file/syslog sinks. Request processing publishes into a bounded Tokio broadcast channel without waiting for viewers. A slow viewer receives an explicit `gap` event with the number of entries lost from that viewer's live queue. The CLI also consults the retained history after connecting/reconnecting and de-duplicates entries by request id. Each request now carries a fresh 64-hex-character SHA-256 `request_id`; the same protected value is forwarded to the backend as `X-Request-ID` for correlation.
|
The live path is separate from configured file/syslog sinks and from the `/logs` stored-log explorer. Request processing publishes ordinary routed request records into a bounded Tokio broadcast channel without waiting for viewers. Low-level malformed HTTP/TLS/HTTP2/HTTP3 protocol failures introduced in 0.26.0 are intentionally persistent-sink-only and do not enter this Live Logs ring, so scanner noise cannot evict normal live request history. A slow viewer receives an explicit `gap` event with the number of entries lost from that viewer's live queue. The CLI also consults the retained history after connecting/reconnecting and de-duplicates entries by request id. Each request now carries a fresh 64-hex-character SHA-256 `request_id`; the same protected value is forwarded to the backend as `X-Request-ID` for correlation.
|
||||||
|
|
||||||
## User-Agent, country and Traffic analytics
|
## User-Agent, country and Traffic analytics
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# Deployment and configuration migration history
|
# Deployment and configuration migration history
|
||||||
|
|
||||||
|
## 0.25.7 -> 0.26.0
|
||||||
|
|
||||||
|
There is no SQLite schema or saved-configuration migration. Version 0.26.0 extends the existing structured access-log record with optional protocol-diagnostic fields and adds a read-only stored-log explorer. Existing logging settings remain valid and file logging remains disabled by default. Low-level malformed/TLS/protocol events are written only when at least one external structured-log sink is enabled; they do not enter the Live Logs memory ring. Existing JSONL access files and the configured error file can be browsed after upgrade without conversion.
|
||||||
|
|
||||||
|
The `/logs` explorer is authenticated administration UI. Its API derives source IDs from the active RustProxy logging configuration and does not accept filesystem paths. On Unix it refuses symlink targets when opening files for browsing.
|
||||||
|
|
||||||
## 0.25.6 -> 0.25.7
|
## 0.25.6 -> 0.25.7
|
||||||
|
|
||||||
There is no SQLite schema or saved-configuration migration. Version 0.25.7 reuses the existing `admin_preferences` document to persist backend-table visible columns and sort order per administrator account; search text remains transient. It also adds vhost enable/disable UI actions, themed scrollbars and additional Prometheus/Grafana observability. Existing vhost `enabled` values and backend configuration remain unchanged until an administrator explicitly uses the new action.
|
There is no SQLite schema or saved-configuration migration. Version 0.25.7 reuses the existing `admin_preferences` document to persist backend-table visible columns and sort order per administrator account; search text remains transient. It also adds vhost enable/disable UI actions, themed scrollbars and additional Prometheus/Grafana observability. Existing vhost `enabled` values and backend configuration remain unchanged until an administrator explicitly uses the new action.
|
||||||
|
|||||||
+1
-1
@@ -56,7 +56,7 @@ Only `GET` on the exact configured path without a query string is accepted. A
|
|||||||
wrong path is `404`, wrong method `405`, disallowed peer `403`, invalid/missing
|
wrong path is `404`, wrong method `405`, disallowed peer `403`, invalid/missing
|
||||||
required bearer `401`, and exporter concurrency exhaustion `429`.
|
required bearer `401`, and exporter concurrency exhaustion `429`.
|
||||||
|
|
||||||
## Coverage audit in 0.25.7
|
## Coverage audit in 0.26.0
|
||||||
|
|
||||||
The exporter represents all scalar fields currently sampled by `diagnostics::View` when available. The per-interface `network_interfaces` list added in 0.18.6 is intentionally dashboard-only so kernel-created interface names cannot create an uncontrolled Prometheus label set. RustProxy also exports the bounded operational data already available from traffic metrics, cache, vhosts, backend pools, streams, logging, GeoIP and ACME renewal observations.
|
The exporter represents all scalar fields currently sampled by `diagnostics::View` when available. The per-interface `network_interfaces` list added in 0.18.6 is intentionally dashboard-only so kernel-created interface names cannot create an uncontrolled Prometheus label set. RustProxy also exports the bounded operational data already available from traffic metrics, cache, vhosts, backend pools, streams, logging, GeoIP and ACME renewal observations.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -31,7 +31,7 @@ This is the canonical index for the current project documentation. The authentic
|
|||||||
- [NOT_FOUND_HOSTS](NOT_FOUND_HOSTS.md) - local 404 host handling.
|
- [NOT_FOUND_HOSTS](NOT_FOUND_HOSTS.md) - local 404 host handling.
|
||||||
- [DOCKER_DISCOVERY](DOCKER_DISCOVERY.md) - Docker discovery and generated vhosts.
|
- [DOCKER_DISCOVERY](DOCKER_DISCOVERY.md) - Docker discovery and generated vhosts.
|
||||||
- [ACME_PROVIDERS](ACME_PROVIDERS.md) and [RENEWAL](RENEWAL.md) - certificate automation.
|
- [ACME_PROVIDERS](ACME_PROVIDERS.md) and [RENEWAL](RENEWAL.md) - certificate automation.
|
||||||
- [LIVE_LOGS](LIVE_LOGS.md) and [ERROR_LOGGING](ERROR_LOGGING.md) - request/error observability, User-Agent/optional-country capture and bounded per-vhost Traffic analytics.
|
- [LIVE_LOGS](LIVE_LOGS.md), [FILE_LOGS](FILE_LOGS.md) and [ERROR_LOGGING](ERROR_LOGGING.md) - independent in-memory live request viewing, bounded on-disk log exploration, protocol-error persistence and optional error-file logging.
|
||||||
|
|
||||||
## Administration and operations
|
## Administration and operations
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@ The **Settings -> Logging** page is split into **Outputs**, **JSON format** and
|
|||||||
`logging.format.fields` maps output field names to standard event fields; an empty map
|
`logging.format.fields` maps output field names to standard event fields; an empty map
|
||||||
emits the complete standard event. The panel provides Standard, Nginx-style, ECS-style
|
emits the complete standard event. The panel provides Standard, Nginx-style, ECS-style
|
||||||
and Minimal presets plus a visual field selector/renamer with JSON preview. Advanced raw
|
and Minimal presets plus a visual field selector/renamer with JSON preview. Advanced raw
|
||||||
mapping remains available for aliases not representable by the simple builder. Supported mapped sources include `vhost`, `route`, `cache_effective_enabled`, `transform_effective_enabled`, `backend`, request/response metadata and cache outcome fields. `static_fields` adds constant string fields. Loki labels are separately configurable.
|
mapping remains available for aliases not representable by the simple builder. Supported mapped sources include `vhost`, `route`, `cache_effective_enabled`, `transform_effective_enabled`, `backend`, request/response metadata and cache outcome fields. `static_fields` adds constant string fields. For low-level `protocol_error` events, RustProxy always preserves the required diagnostic envelope (`event`, error kind/detail, time, client/local/TLS/protocol fields, status and outcome) even with a custom mapping, so malformed-traffic records remain searchable in the built-in Logs explorer. Loki labels are separately configurable.
|
||||||
`/api/status.logging.sinks` reports queue capacity, dropped events, successfully delivered
|
`/api/status.logging.sinks` reports queue capacity, dropped events, successfully delivered
|
||||||
events and sink errors.
|
events and sink errors.
|
||||||
|
|
||||||
|
|||||||
+10
-6
@@ -1,18 +1,22 @@
|
|||||||
# Verification report - RustProxy 0.25.7
|
# Verification report - RustProxy 0.26.0
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
RustProxy 0.25.7 is a patch release focused on vhost operations and backend observability. Vhost cards now expose a direct revision-protected enable/disable action, the main Backends diagnostics table supports account-scoped persisted sort/column visibility preferences plus transient search, and the vhost editor gives the Enabled setting a distinct state treatment. Dashboard scrollbars follow the active theme. Prometheus now exports backend retry attempt/success counters and upstream-client generation, generation-request, generation-age and rotation metrics for both ordinary and Docker-discovered backends; the bundled Grafana dashboard adds matching lifecycle/retry panels. SQLite remains schema 8 and no saved-configuration migration is introduced.
|
RustProxy 0.26.0 is a minor release focused on persistent malformed-traffic diagnostics and a built-in stored-log explorer. Public listeners now enqueue bounded `protocol_error` records for malformed/incomplete HTTP/1 traffic, TLS ClientHello/handshake failures and timeouts, invalid/disabled h2c attempts, HTTP/2 connection/drain protocol failures, HTTP/3/QUIC handshake/request/header/stream failures and connection-limit rejects. These records use the existing asynchronous structured-log sinks and deliberately bypass the Live Logs memory ring so hostile scanner traffic cannot evict ordinary live request history.
|
||||||
|
|
||||||
Backend-table visible columns and sort order are presentation-only fields in the existing account-scoped `admin_preferences` document; search text is not persisted and none of these view settings alter RustProxy configuration. The quick vhost action changes only the selected vhost `enabled` flag through the same `saveSection("vhosts", ...)` revision/CSRF path as other vhost edits. Existing backend client recycling defaults remain unchanged.
|
The authenticated `/logs` workspace is independent from Live Logs and reads only configured RustProxy access/per-vhost/error files. It uses opaque source IDs rather than browser-supplied paths, regular-file checks plus `O_NOFOLLOW` on Unix, bounded blocking reads, a global 20,000-line / 8 MiB query budget, at most 32 sources, bounded pagination and server-side filters. Existing configured files remain browsable while their writer is disabled. Required protocol-diagnostic envelope fields are retained even when an administrator uses a custom structured JSON mapping. SQLite remains schema 8 and the saved configuration format remains compatible with 0.25.7.
|
||||||
|
|
||||||
## Implemented automated coverage
|
## Implemented automated coverage
|
||||||
|
|
||||||
|
0.26.0-specific Rust tests cover protocol-error separation from the Live Logs ring, required diagnostic fields under custom JSON mappings, secret redaction and UTF-8-safe detail bounding. Admin tests cover visibility of existing log files while writers are disabled, bounded tail reads, oversized source-ID rejection and Unix symlink refusal. The HTTP/1, TLS/H2/h2c and HTTP/3 adapters contain explicit persistent diagnostic emission points for the failure classes listed above. These Rust tests are present in the source tree but cannot be compiled/executed in this workspace because Cargo is unavailable.
|
||||||
|
|
||||||
Rust unit tests in `src/web_server/tests.rs` cover GET/HEAD/index/slash redirect, MIME/length, validators, valid/invalid range handling, If-Range, escaped/repeated directory listings, per-directory index-before-listing fallback, generic 403/HEAD behavior, MIME overrides/default MIME, cache rules, source headers, `try_files`, rewrite redirect/internal behavior, `.htaccess` WordPress-style front-controller routing, nested DirectoryIndex/MIME/Header/Expires behavior, strict-mode rejection and PHP-source protection, hardening probe/method blocking and fingerprint-header stripping, precompressed representations/ranges, download headers, PHP FastCGI fixture execution/failure-source-disclosure behavior, request-body 413 enforcement, custom FastCGI/PHP_VALUE validation and reserved-parameter rejection, direct traversal rejection and Unix/Linux symlink policy. Upstream tests cover request-budget client generation recycling and disabled recycling; balancing tests cover the new keepalive defaults and validation bounds. `src/traffic/tests.rs` includes bounded-table, effective-client-IP view and fixed Prometheus User-Agent/method family coverage. Configuration tests cover pure static vhosts without a pool, mixed Web Server/Reverse Proxy routing, reuse of one Web Service by multiple vhosts, unknown service references and backward-compatible default source behavior. Vhost tests cover method-policy normalization/allow/block behavior, database tests cover retained configuration revision documents, actor metadata and previous-revision lookup, and admin tests cover account-scoped/CSRF-protected view preferences including legacy preference defaults, backend-table columns/sort persistence and invalid-column rejection, plus restoring revision 1 as revision 3 while preserving the previous document and emitting `config.restore`. ACL tests include encoded traversal/separator/malformed-encoding cases.
|
Rust unit tests in `src/web_server/tests.rs` cover GET/HEAD/index/slash redirect, MIME/length, validators, valid/invalid range handling, If-Range, escaped/repeated directory listings, per-directory index-before-listing fallback, generic 403/HEAD behavior, MIME overrides/default MIME, cache rules, source headers, `try_files`, rewrite redirect/internal behavior, `.htaccess` WordPress-style front-controller routing, nested DirectoryIndex/MIME/Header/Expires behavior, strict-mode rejection and PHP-source protection, hardening probe/method blocking and fingerprint-header stripping, precompressed representations/ranges, download headers, PHP FastCGI fixture execution/failure-source-disclosure behavior, request-body 413 enforcement, custom FastCGI/PHP_VALUE validation and reserved-parameter rejection, direct traversal rejection and Unix/Linux symlink policy. Upstream tests cover request-budget client generation recycling and disabled recycling; balancing tests cover the new keepalive defaults and validation bounds. `src/traffic/tests.rs` includes bounded-table, effective-client-IP view and fixed Prometheus User-Agent/method family coverage. Configuration tests cover pure static vhosts without a pool, mixed Web Server/Reverse Proxy routing, reuse of one Web Service by multiple vhosts, unknown service references and backward-compatible default source behavior. Vhost tests cover method-policy normalization/allow/block behavior, database tests cover retained configuration revision documents, actor metadata and previous-revision lookup, and admin tests cover account-scoped/CSRF-protected view preferences including legacy preference defaults, backend-table columns/sort persistence and invalid-column rejection, plus restoring revision 1 as revision 3 while preserving the previous document and emitting `config.restore`. ACL tests include encoded traversal/separator/malformed-encoding cases.
|
||||||
|
|
||||||
Rust-independent release checks additionally validate UI/JSON/language/source contracts and reproducible packaging.
|
Rust-independent release checks additionally validate UI/JSON/language/source contracts and reproducible packaging.
|
||||||
|
|
||||||
During the prior 0.25.4 compiler gate, an external build exposed one module-privacy error in the history split (`CONFIG_HISTORY_CAPACITY` was private to `db::history`) and one unused `country_resolved` assignment in the location GeoIP branch. Both were corrected before the 0.25.5 baseline: the capacity is exposed to its parent module with `pub(super)`, referenced through `history::CONFIG_HISTORY_CAPACITY`, and the dead assignment is removed. This workspace still has no Rust/Cargo toolchain, so compiler confirmation of the 0.25.7 tree remains a release-host gate.
|
An external 0.26.0 `cargo` build exposed two compiler regressions that the Rust-independent gates could not detect: `frontend_state` was moved into the HTTP/1 service closure and then reused for connection-error logging, and `serde_json::Map::entry("event".into())` was ambiguous with the pinned dependency graph. Both are corrected in this source tree by giving the service closure its own cloned frontend state and by passing the `"event"` literal directly to `Map::entry`. The Rust-independent release gates pass after these corrections. A fresh compiler-enabled `cargo check/test` is still required to confirm there are no further Rust errors because this workspace has no Rust/Cargo toolchain.
|
||||||
|
|
||||||
|
During the prior 0.25.4 compiler gate, an external build exposed one module-privacy error in the history split (`CONFIG_HISTORY_CAPACITY` was private to `db::history`) and one unused `country_resolved` assignment in the location GeoIP branch. Both were corrected before the 0.25.5 baseline: the capacity is exposed to its parent module with `pub(super)`, referenced through `history::CONFIG_HISTORY_CAPACITY`, and the dead assignment is removed.
|
||||||
|
|
||||||
### Modular source-tree verification
|
### Modular source-tree verification
|
||||||
|
|
||||||
@@ -30,8 +34,8 @@ During the prior 0.25.4 compiler gate, an external build exposed one module-priv
|
|||||||
| `python3 scripts/langs_check.py` | PASS |
|
| `python3 scripts/langs_check.py` | PASS |
|
||||||
| `node --check src/admin/web/vhosts/render.js` | PASS |
|
| `node --check src/admin/web/vhosts/render.js` | PASS |
|
||||||
| `node --check src/admin/web/vhosts/editor.js` | PASS |
|
| `node --check src/admin/web/vhosts/editor.js` | PASS |
|
||||||
| 0.25.7 vhost/dashboard source contracts | PASS: quick enable/disable, backend search/sort/column controls, account-scoped backend table preferences, themed scrollbars and distinct Enabled editor control are present |
|
| 0.26.0 stored-log/protocol diagnostics source contracts | PASS: persistent protocol-error emitters, Live Logs separation, authenticated `/logs` explorer, bounded source-ID file access and required diagnostic fields are present |
|
||||||
| 0.25.7 Prometheus/Grafana source contracts | PASS: retry/client-lifecycle families exist for ordinary and Docker backends and matching dashboard queries are present |
|
| retained 0.25.7 vhost/backend observability contracts | PASS: quick enable/disable, backend search/sort/column controls, retry/client-lifecycle metrics and matching dashboard coverage remain present |
|
||||||
| `python3 scripts/memory_contracts.py` | PASS |
|
| `python3 scripts/memory_contracts.py` | PASS |
|
||||||
| `python3 scripts/web_server_contracts.py` | PASS |
|
| `python3 scripts/web_server_contracts.py` | PASS |
|
||||||
| `python3 scripts/security_contracts.py` | PASS |
|
| `python3 scripts/security_contracts.py` | PASS |
|
||||||
|
|||||||
+1
-1
@@ -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.25.7 metric names and a scrape interval around 15s.
|
dashboard expects RustProxy 0.26.0 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.
|
||||||
|
|
||||||
|
|||||||
@@ -4529,9 +4529,9 @@
|
|||||||
},
|
},
|
||||||
"timepicker": {},
|
"timepicker": {},
|
||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"title": "RustProxy 0.25.7 - Operations",
|
"title": "RustProxy 0.26.0 - Operations",
|
||||||
"uid": "rustproxy-0170",
|
"uid": "rustproxy-0170",
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"weekStart": "",
|
"weekStart": "",
|
||||||
"description": "RustProxy 0.25.7 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
|
"description": "RustProxy 0.26.0 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
|
||||||
}
|
}
|
||||||
+30
-1
@@ -1973,6 +1973,35 @@
|
|||||||
"vhQuickEnable": "Enable",
|
"vhQuickEnable": "Enable",
|
||||||
"vhQuickDisable": "Disable",
|
"vhQuickDisable": "Disable",
|
||||||
"vhEnableHint": "Controls whether this vhost accepts traffic.",
|
"vhEnableHint": "Controls whether this vhost accepts traffic.",
|
||||||
"backendColumnsRequired": "At least one backend table column must remain visible."
|
"backendColumnsRequired": "At least one backend table column must remain visible.",
|
||||||
|
"logs": "Logs",
|
||||||
|
"filelogs": "Logs",
|
||||||
|
"filelogsTitle": "Stored log explorer",
|
||||||
|
"filelogsDescription": "Search configured on-disk RustProxy logs without coupling to the Live Logs memory buffer.",
|
||||||
|
"fileLogsTitle": "Stored log explorer",
|
||||||
|
"fileLogsMode": "LOCAL / READ-ONLY / BOUNDED",
|
||||||
|
"fileLogsHelp": "Reads only configured RustProxy log files. It is independent from Live Logs and never accepts an arbitrary filesystem path.",
|
||||||
|
"fileLogsSettingsHelp": "Browse configured access and error files with server-side filters. File logging can be off while Live Logs continues to use its memory buffer.",
|
||||||
|
"openLogs": "Open Logs",
|
||||||
|
"fileLogsSource": "Source",
|
||||||
|
"fileLogsAllSources": "All configured files",
|
||||||
|
"fileLogsNoSources": "No configured log files are available.",
|
||||||
|
"fileLogsEvent": "Event",
|
||||||
|
"fileLogsPageSize": "Page size",
|
||||||
|
"fileLogsRecord": "Request / reason",
|
||||||
|
"fileLogsEmpty": "No records match the current filters.",
|
||||||
|
"fileLogsDetails": "Record details",
|
||||||
|
"fileLogsSelectRecord": "Select a record to inspect its JSON.",
|
||||||
|
"fileLogsSearchPlaceholder": "IP, path, error, request id...",
|
||||||
|
"fileLogsTruncated": "scan limit reached",
|
||||||
|
"fileLogsSummary": "{from}–{to} of {total} matches · scanned {bytes} / {lines} lines",
|
||||||
|
"search": "Search",
|
||||||
|
"all": "All",
|
||||||
|
"clientIp": "Client IP",
|
||||||
|
"vhost": "Vhost",
|
||||||
|
"previous": "Previous",
|
||||||
|
"next": "Next",
|
||||||
|
"refresh": "Refresh",
|
||||||
|
"applyFilters": "Apply filters"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+30
-1
@@ -1973,6 +1973,35 @@
|
|||||||
"vhQuickEnable": "WÅÄ
cz",
|
"vhQuickEnable": "WÅÄ
cz",
|
||||||
"vhQuickDisable": "WyÅÄ
cz",
|
"vhQuickDisable": "WyÅÄ
cz",
|
||||||
"vhEnableHint": "OkreÅla, czy ten vhost przyjmuje ruch.",
|
"vhEnableHint": "OkreÅla, czy ten vhost przyjmuje ruch.",
|
||||||
"backendColumnsRequired": "Co najmniej jedna kolumna tabeli backendów musi pozostać widoczna."
|
"backendColumnsRequired": "Co najmniej jedna kolumna tabeli backendów musi pozostać widoczna.",
|
||||||
|
"logs": "Logs",
|
||||||
|
"filelogs": "Logs",
|
||||||
|
"filelogsTitle": "Przeglądarka zapisanych logów",
|
||||||
|
"filelogsDescription": "Przeszukuj skonfigurowane logi RustProxy na dysku niezależnie od bufora Live Logs.",
|
||||||
|
"fileLogsTitle": "Przeglądarka zapisanych logów",
|
||||||
|
"fileLogsMode": "LOKALNIE / TYLKO ODCZYT / LIMITY",
|
||||||
|
"fileLogsHelp": "Czyta wyłącznie skonfigurowane pliki logów RustProxy. Działa niezależnie od Live Logs i nie przyjmuje dowolnej ścieżki systemu plików.",
|
||||||
|
"fileLogsSettingsHelp": "Przeglądaj skonfigurowane pliki access/error z filtrami po stronie serwera. Logowanie do plików może być wyłączone, a Live Logs nadal korzysta z bufora pamięci.",
|
||||||
|
"openLogs": "Otwórz Logs",
|
||||||
|
"fileLogsSource": "Źródło",
|
||||||
|
"fileLogsAllSources": "Wszystkie skonfigurowane pliki",
|
||||||
|
"fileLogsNoSources": "Brak dostępnych skonfigurowanych plików logów.",
|
||||||
|
"fileLogsEvent": "Zdarzenie",
|
||||||
|
"fileLogsPageSize": "Rozmiar strony",
|
||||||
|
"fileLogsRecord": "Żądanie / przyczyna",
|
||||||
|
"fileLogsEmpty": "Brak rekordów pasujących do filtrów.",
|
||||||
|
"fileLogsDetails": "Szczegóły rekordu",
|
||||||
|
"fileLogsSelectRecord": "Wybierz rekord, aby zobaczyć jego JSON.",
|
||||||
|
"fileLogsSearchPlaceholder": "IP, ścieżka, błąd, request id...",
|
||||||
|
"fileLogsTruncated": "osiągnięto limit skanowania",
|
||||||
|
"fileLogsSummary": "{from}–{to} z {total} wyników · przeskanowano {bytes} / {lines} linii",
|
||||||
|
"search": "Szukaj",
|
||||||
|
"all": "Wszystkie",
|
||||||
|
"clientIp": "IP klienta",
|
||||||
|
"vhost": "Vhost",
|
||||||
|
"previous": "Poprzednia",
|
||||||
|
"next": "Następna",
|
||||||
|
"refresh": "Odśwież",
|
||||||
|
"applyFilters": "Zastosuj filtry"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -88,7 +88,7 @@ def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[
|
|||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser(description=__doc__)
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.25.7.zip')
|
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.0.zip')
|
||||||
output = parser.parse_args().output.resolve()
|
output = parser.parse_args().output.resolve()
|
||||||
paths = files()
|
paths = files()
|
||||||
checksum = ROOT / 'SHA256SUMS'
|
checksum = ROOT / 'SHA256SUMS'
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ from urllib.parse import urlsplit
|
|||||||
from urllib.request import Request, build_opener, ProxyHandler, HTTPRedirectHandler
|
from urllib.request import Request, build_opener, ProxyHandler, HTTPRedirectHandler
|
||||||
from urllib.error import HTTPError, URLError
|
from urllib.error import HTTPError, URLError
|
||||||
|
|
||||||
PRIVATE = ('/api/status','/api/config','/api/backends','/api/traffic','/api/cache','/api/transform','/api/logs','/api/audit',
|
PRIVATE = ('/api/status','/api/config','/api/backends','/api/traffic','/api/cache','/api/transform','/api/logs','/api/file-logs','/api/file-logs/query','/api/audit',
|
||||||
'/api/events','/api/certificates','/api/acme','/api/auth/session','/metrics','/api/metrics',
|
'/api/events','/api/certificates','/api/acme','/api/auth/session','/metrics','/api/metrics',
|
||||||
'/docs/openapi.json','/docs/init.js','/docs/style.css','/docs/swagger-ui-bundle.js',
|
'/docs/openapi.json','/docs/init.js','/docs/style.css','/docs/swagger-ui-bundle.js',
|
||||||
'/docs/swagger-ui.css','/docs/licenses.txt','/assets/app.js','/assets/langs.json','/assets/vhosts.js',
|
'/docs/swagger-ui.css','/docs/licenses.txt','/assets/app.js','/assets/langs.json','/assets/vhosts.js',
|
||||||
'/assets/management.js','/assets/integrations.js','/assets/settings.js','/assets/acme.js',
|
'/assets/management.js','/assets/integrations.js','/assets/settings.js','/assets/file-logs.js','/assets/acme.js',
|
||||||
'/assets/traffic-panel.js','/assets/cache-panel.js','/assets/transform-panel.js','/api/does-not-exist')
|
'/assets/traffic-panel.js','/assets/cache-panel.js','/assets/transform-panel.js','/api/does-not-exist')
|
||||||
|
|
||||||
class NoRedirect(HTTPRedirectHandler):
|
class NoRedirect(HTTPRedirectHandler):
|
||||||
|
|||||||
+10
-10
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
|
|||||||
|
|
||||||
preflight(root)
|
preflight(root)
|
||||||
manifest = tomllib.loads((root / "Cargo.toml").read_text())
|
manifest = tomllib.loads((root / "Cargo.toml").read_text())
|
||||||
assert manifest["package"]["version"] == "0.25.7"
|
assert manifest["package"]["version"] == "0.26.0"
|
||||||
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
||||||
h3dep = manifest["dependencies"]["h3"]
|
h3dep = manifest["dependencies"]["h3"]
|
||||||
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
|
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
|
||||||
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
|
|||||||
lock_path = root / "Cargo.lock"
|
lock_path = root / "Cargo.lock"
|
||||||
if lock_path.exists():
|
if lock_path.exists():
|
||||||
lock = lock_path.read_text()
|
lock = lock_path.read_text()
|
||||||
assert 'name = "rust-proxy"\nversion = "0.25.7"' in lock
|
assert 'name = "rust-proxy"\nversion = "0.26.0"' in lock
|
||||||
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
|
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
|
||||||
for release_doc, marker in {
|
for release_doc, marker in {
|
||||||
"README.md": "# RustProxy 0.25.7",
|
"README.md": "# RustProxy 0.26.0",
|
||||||
"CHANGELOG.md": "## 0.25.7",
|
"CHANGELOG.md": "## 0.26.0",
|
||||||
"ROADMAP.md": "## 0.25.7",
|
"ROADMAP.md": "## 0.26.0",
|
||||||
"AGENTS.md": "Current source version: 0.25.7.",
|
"AGENTS.md": "Current source version: 0.26.0.",
|
||||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.25.7",
|
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.0",
|
||||||
}.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"
|
||||||
@@ -240,7 +240,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
|
|||||||
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
|
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
|
||||||
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
|
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
|
||||||
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
|
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
|
||||||
for marker in ['"version": "0.25.7"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']:
|
for marker in ['"version": "0.26.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']:
|
||||||
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
|
||||||
@@ -1203,7 +1203,7 @@ assert '"error_log" => {' not in admin_0241 and 'request("/api/config/error_log"
|
|||||||
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
|
assert '"error_log"' in openapi and 'logging, error_log, forwarding' not in openapi
|
||||||
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
|
assert 'logging_section_persists_error_log_settings_atomically' in admin_0241 and 'error_log_is_not_a_standalone_configuration_section' in admin_0241
|
||||||
assert 'location.pathname === "/logging"' in settings_js_0241
|
assert 'location.pathname === "/logging"' in settings_js_0241
|
||||||
assert '"RuntimeInformation"' in openapi and '"version": "0.25.7"' in openapi
|
assert '"RuntimeInformation"' in openapi and '"version": "0.26.0"' 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")
|
||||||
@@ -1248,5 +1248,5 @@ for schema_name in ["RedirectHost", "NotFoundHost"]:
|
|||||||
node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"]
|
node = openapi_0241["components"]["schemas"][schema_name]["properties"]["frontend"]["properties"]["hosts"]
|
||||||
assert "maxItems" not in node, schema_name
|
assert "maxItems" not in node, schema_name
|
||||||
|
|
||||||
print("PASS: 0.25.7 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
|
print("PASS: 0.26.0 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, Debian/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v8 migrations.")
|
||||||
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
|
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
|
||||||
@@ -97,7 +97,7 @@ def digest(path: Path) -> str:
|
|||||||
def fetch_https(url: str, max_bytes: int) -> bytes:
|
def fetch_https(url: str, max_bytes: int) -> bytes:
|
||||||
if urllib.parse.urlparse(url).scheme != 'https':
|
if urllib.parse.urlparse(url).scheme != 'https':
|
||||||
raise ValueError('GeoIP downloads require HTTPS.')
|
raise ValueError('GeoIP downloads require HTTPS.')
|
||||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.25.7'})
|
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.0'})
|
||||||
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.')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Rust-independent documentation/Reader consistency checks for the 0.25.7 release."""
|
"""Rust-independent documentation/Reader consistency checks for the 0.26.0 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
|
||||||
@@ -55,16 +55,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.25.7")
|
assert readme.startswith("# RustProxy 0.26.0")
|
||||||
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
|
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
|
||||||
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
|
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
|
||||||
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
|
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
|
||||||
assert roadmap.startswith("# RustProxy roadmap") and "## 0.25.7" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
|
assert roadmap.startswith("# RustProxy roadmap") and "## 0.26.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.25.7", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
|
for release in ["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.25.7") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
|
assert 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}"
|
||||||
|
|||||||
@@ -75,10 +75,10 @@ def check(root: Path, repair_known: bool = False) -> list[str]:
|
|||||||
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
|
raise ValueError(f"Conflicting Rust modules: {flat.relative_to(root)} and {module.relative_to(root)}")
|
||||||
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]:
|
for path in ["src/auth/mod.rs", "src/auth/store.rs", "src/backup/snapshot.rs", "src/upstream.rs", "src/tls.rs", "src/proxy/websocket.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/editor.js", "src/admin/web/vhosts/render.js", "src/admin/web/web-server.js", "src/admin/web/streams.js", "src/admin/web/host404.js", "src/admin/web/app/core.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/pages.rs", "scripts/mock_server.py", "scripts/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "scripts/docs_check.py", "scripts/web_server_contracts.py", "scripts/rust_source.py", "scripts/rust_module_contracts.py", "scripts/source_module_contracts.py", "scripts/acme_provider.py", "src/basic_auth.rs", "scripts/deploy.py", "scripts/deploy_core.py", "scripts/deploy_runtime.py", "scripts/migrate_npm.py", "scripts/migrate_npm_core.py", "scripts/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "debian/control", "debian/rules", "debian/rustproxy.service", "debian/rustproxy.logrotate", "grafana/rustproxy-overview.json"]:
|
||||||
if not (root / path).is_file():
|
if not (root / path).is_file():
|
||||||
raise ValueError(f"Incomplete RustProxy 0.25.7 source tree: missing {path}")
|
raise ValueError(f"Incomplete RustProxy 0.26.0 source tree: missing {path}")
|
||||||
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
||||||
if '#[path = "auth/mod.rs"]' not in text:
|
if '#[path = "auth/mod.rs"]' not in text:
|
||||||
raise ValueError("src/lib.rs is not the 0.25.7 entry point; extract the complete package, not individual files.")
|
raise ValueError("src/lib.rs is not the 0.26.0 entry point; extract the complete package, not individual files.")
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
|||||||
assert backup.is_dir()
|
assert backup.is_dir()
|
||||||
assert (backup / "src/obsolete.rs").is_file()
|
assert (backup / "src/obsolete.rs").is_file()
|
||||||
assert not (target / "src/obsolete.rs").exists()
|
assert not (target / "src/obsolete.rs").exists()
|
||||||
assert 'version = "0.25.7"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
assert 'version = "0.26.0"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||||
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
||||||
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
||||||
assert (target / "langs/site.json").is_file()
|
assert (target / "langs/site.json").is_file()
|
||||||
@@ -134,7 +134,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
|||||||
assert (target / "docker-agent/certificate.go").is_file()
|
assert (target / "docker-agent/certificate.go").is_file()
|
||||||
assert (target / "docker-agent/server.go").is_file()
|
assert (target / "docker-agent/server.go").is_file()
|
||||||
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
|
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
|
||||||
assert manifest["to_version"] == "0.25.7"
|
assert manifest["to_version"] == "0.26.0"
|
||||||
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()
|
||||||
@@ -554,7 +554,7 @@ def source_contract_smoke() -> None:
|
|||||||
assert marker in admin, marker
|
assert marker in admin, marker
|
||||||
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
|
anonymous_block = admin.split("fn anonymous_allowed", 1)[1].split("const CERTIFICATE_API_KEY_HEADER", 1)[0]
|
||||||
assert '"/api/status"' not in anonymous_block
|
assert '"/api/status"' not in anonymous_block
|
||||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.25.7"' in openapi_0241
|
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.26.0"' 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
|
||||||
@@ -1011,7 +1011,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.25.7"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']:
|
for marker in ['"version": "0.26.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"']:
|
||||||
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"']:
|
||||||
@@ -1186,7 +1186,7 @@ def main() -> None:
|
|||||||
source_contract_smoke()
|
source_contract_smoke()
|
||||||
container_contract_smoke()
|
container_contract_smoke()
|
||||||
debian_grafana_contract_smoke()
|
debian_grafana_contract_smoke()
|
||||||
print("PASS: 0.25.7 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
print("PASS: 0.26.0 SQLite/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, Debian packaging, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
|
|||||||
"""TLS verification uses Python's default trust store; never executes a shell."""
|
"""TLS verification uses Python's default trust store; never executes a shell."""
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
opener = build_opener(HttpsRedirects())
|
opener = build_opener(HttpsRedirects())
|
||||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.25.7"})
|
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.0"})
|
||||||
with opener.open(request, timeout=30) as response, destination.open("wb") as output:
|
with opener.open(request, timeout=30) as response, destination.open("wb") as output:
|
||||||
length = response.headers.get("Content-Length")
|
length = response.headers.get("Content-Length")
|
||||||
if length and int(length) > MAX_ARCHIVE:
|
if length and int(length) > MAX_ARCHIVE:
|
||||||
|
|||||||
+3
-3
@@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
|||||||
for name in MANAGED:
|
for name in MANAGED:
|
||||||
if (target / name).is_symlink():
|
if (target / name).is_symlink():
|
||||||
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
|
raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.")
|
||||||
backup = target / ".upgrade-backup" / ("before-0.25.7-" + uuid.uuid4().hex[:12])
|
backup = target / ".upgrade-backup" / ("before-0.26.0-" + uuid.uuid4().hex[:12])
|
||||||
backup.mkdir(parents=True, mode=0o700)
|
backup.mkdir(parents=True, mode=0o700)
|
||||||
installed: list[str] = []
|
installed: list[str] = []
|
||||||
moved: list[str] = []
|
moved: list[str] = []
|
||||||
@@ -84,7 +84,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
|||||||
remove_legacy = legacy.exists()
|
remove_legacy = legacy.exists()
|
||||||
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
|
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
|
||||||
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
|
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
|
||||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.25.7", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.26.0", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||||
if remove_legacy:
|
if remove_legacy:
|
||||||
legacy.unlink()
|
legacy.unlink()
|
||||||
except BaseException:
|
except BaseException:
|
||||||
@@ -115,7 +115,7 @@ def main() -> None:
|
|||||||
backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir)
|
backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir)
|
||||||
except (OSError, ValueError) as error:
|
except (OSError, ValueError) as error:
|
||||||
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
|
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
|
||||||
print(f"Installed RustProxy 0.25.7. Previous managed source files: {backup}")
|
print(f"Installed RustProxy 0.26.0. 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")
|
||||||
|
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ need(web,
|
|||||||
'HeaderValue::from_static("no-store")',
|
'HeaderValue::from_static("no-store")',
|
||||||
"builtin_error_response(StatusCode::FORBIDDEN, &method)",
|
"builtin_error_response(StatusCode::FORBIDDEN, &method)",
|
||||||
"empty_index_can_go_directly_to_directory_listing",
|
"empty_index_can_go_directly_to_directory_listing",
|
||||||
"RustProxy <b>0.25.7</b>",
|
"RustProxy <b>0.26.0</b>",
|
||||||
"traversal_and_symlink_escape_are_not_served",
|
"traversal_and_symlink_escape_are_not_served",
|
||||||
"try_files_rewrites_cache_rules_headers_and_downloads_work",
|
"try_files_rewrites_cache_rules_headers_and_downloads_work",
|
||||||
"precompressed_sidecars_keep_mime_validators_and_range_semantics",
|
"precompressed_sidecars_keep_mime_validators_and_range_semantics",
|
||||||
|
|||||||
@@ -94,6 +94,11 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
|||||||
"/docs/ERROR_LOGGING.md"
|
"/docs/ERROR_LOGGING.md"
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"docs/FILE_LOGS.md",
|
||||||
|
"guides",
|
||||||
|
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/FILE_LOGS.md")),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"docs/HTTP3.md",
|
"docs/HTTP3.md",
|
||||||
"technical",
|
"technical",
|
||||||
|
|||||||
@@ -0,0 +1,426 @@
|
|||||||
|
const FILE_LOG_SCAN_BYTES_PER_SOURCE: u64 = 2 * 1024 * 1024;
|
||||||
|
const FILE_LOG_SCAN_BYTES_TOTAL: u64 = 8 * 1024 * 1024;
|
||||||
|
const FILE_LOG_SCAN_LINES: usize = 20_000;
|
||||||
|
const FILE_LOG_MAX_SOURCES: usize = 32;
|
||||||
|
const FILE_LOG_MAX_LIMIT: usize = 500;
|
||||||
|
const FILE_LOG_MAX_OFFSET: usize = 10_000;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FileLogSource {
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
kind: &'static str,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
enabled: bool,
|
||||||
|
format: &'static str,
|
||||||
|
exists: bool,
|
||||||
|
size: u64,
|
||||||
|
modified_ms: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize)]
|
||||||
|
#[serde(default, deny_unknown_fields)]
|
||||||
|
struct FileLogQuery {
|
||||||
|
source: Option<String>,
|
||||||
|
q: Option<String>,
|
||||||
|
event: Option<String>,
|
||||||
|
status_class: Option<String>,
|
||||||
|
client_ip: Option<String>,
|
||||||
|
vhost: Option<String>,
|
||||||
|
offset: Option<usize>,
|
||||||
|
limit: Option<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_modified_ms(metadata: &std::fs::Metadata) -> u64 {
|
||||||
|
metadata
|
||||||
|
.modified()
|
||||||
|
.ok()
|
||||||
|
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
|
||||||
|
.map(|duration| duration.as_millis() as u64)
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_from_path(
|
||||||
|
id: String,
|
||||||
|
label: String,
|
||||||
|
kind: &'static str,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
enabled: bool,
|
||||||
|
format: &'static str,
|
||||||
|
) -> FileLogSource {
|
||||||
|
let metadata = std::fs::symlink_metadata(&path).ok();
|
||||||
|
let safe_regular = metadata
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|meta| meta.file_type().is_file() && !meta.file_type().is_symlink());
|
||||||
|
FileLogSource {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
kind,
|
||||||
|
path,
|
||||||
|
enabled,
|
||||||
|
format,
|
||||||
|
exists: safe_regular,
|
||||||
|
size: metadata
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| safe_regular)
|
||||||
|
.map_or(0, |m| m.len()),
|
||||||
|
modified_ms: metadata
|
||||||
|
.as_ref()
|
||||||
|
.filter(|_| safe_regular)
|
||||||
|
.map_or(0, file_modified_ms),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configured_file_log_sources(config: &crate::config::Config) -> Vec<FileLogSource> {
|
||||||
|
let mut sources = Vec::new();
|
||||||
|
if config.logging.file.per_vhost {
|
||||||
|
let directory = std::path::PathBuf::from(&config.logging.file.directory);
|
||||||
|
let directory_safe = std::fs::symlink_metadata(&directory)
|
||||||
|
.ok()
|
||||||
|
.is_some_and(|meta| meta.file_type().is_dir() && !meta.file_type().is_symlink());
|
||||||
|
if directory_safe {
|
||||||
|
let mut entries = std::fs::read_dir(&directory)
|
||||||
|
.ok()
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|iter| iter.filter_map(|entry| entry.ok()))
|
||||||
|
.filter_map(|entry| {
|
||||||
|
let path = entry.path();
|
||||||
|
let name = entry.file_name().to_string_lossy().into_owned();
|
||||||
|
if !name.ends_with(".jsonl") || name.len() > 160 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let file_type = entry.file_type().ok()?;
|
||||||
|
if !file_type.is_file() || file_type.is_symlink() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let meta = entry.metadata().ok()?;
|
||||||
|
Some((name, path, file_modified_ms(&meta)))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
entries.sort_by(|a, b| b.2.cmp(&a.2).then_with(|| a.0.cmp(&b.0)));
|
||||||
|
entries.truncate(256);
|
||||||
|
for (name, path, _) in entries {
|
||||||
|
let id = format!("vhost:{}", name.trim_end_matches(".jsonl"));
|
||||||
|
sources.push(source_from_path(
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
"access",
|
||||||
|
path,
|
||||||
|
config.logging.file.enabled,
|
||||||
|
"JSONL",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sources.push(source_from_path(
|
||||||
|
"access".into(),
|
||||||
|
"Access log".into(),
|
||||||
|
"access",
|
||||||
|
std::path::PathBuf::from(&config.logging.file.path),
|
||||||
|
config.logging.file.enabled,
|
||||||
|
"JSONL",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
sources.push(source_from_path(
|
||||||
|
"error".into(),
|
||||||
|
"Error log".into(),
|
||||||
|
"error",
|
||||||
|
std::path::PathBuf::from(&config.error_log.path),
|
||||||
|
config.error_log.enabled,
|
||||||
|
match config.error_log.format {
|
||||||
|
crate::logging::ErrorLogFormat::Json => "JSONL",
|
||||||
|
crate::logging::ErrorLogFormat::Raw => "RAW",
|
||||||
|
},
|
||||||
|
));
|
||||||
|
sources
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_log_sources_json(sources: &[FileLogSource]) -> Value {
|
||||||
|
Value::Array(
|
||||||
|
sources
|
||||||
|
.iter()
|
||||||
|
.map(|source| {
|
||||||
|
json!({
|
||||||
|
"id": source.id,
|
||||||
|
"label": source.label,
|
||||||
|
"kind": source.kind,
|
||||||
|
"path": source.path.to_string_lossy(),
|
||||||
|
"enabled": source.enabled,
|
||||||
|
"format": source.format,
|
||||||
|
"exists": source.exists,
|
||||||
|
"size": source.size,
|
||||||
|
"modified_ms": source.modified_ms,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn file_logs(State(state): State<Arc<App>>) -> Result<Json<Value>, ApiError> {
|
||||||
|
let config = state.config.load().envelope.config.clone();
|
||||||
|
let sources = tokio::task::spawn_blocking(move || configured_file_log_sources(&config))
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
ApiError(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"file log inventory worker failed".into(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(Json(json!({
|
||||||
|
"sources": file_log_sources_json(&sources),
|
||||||
|
"limits": {
|
||||||
|
"max_limit": FILE_LOG_MAX_LIMIT,
|
||||||
|
"max_offset": FILE_LOG_MAX_OFFSET,
|
||||||
|
"scan_bytes_per_source": FILE_LOG_SCAN_BYTES_PER_SOURCE,
|
||||||
|
"scan_bytes_total": FILE_LOG_SCAN_BYTES_TOTAL,
|
||||||
|
"scan_lines": FILE_LOG_SCAN_LINES,
|
||||||
|
"max_sources_per_query": FILE_LOG_MAX_SOURCES,
|
||||||
|
}
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_file_log(path: &std::path::Path) -> std::io::Result<std::fs::File> {
|
||||||
|
let mut options = std::fs::OpenOptions::new();
|
||||||
|
options.read(true);
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
options.custom_flags(libc::O_NOFOLLOW);
|
||||||
|
}
|
||||||
|
let file = options.open(path)?;
|
||||||
|
let metadata = file.metadata()?;
|
||||||
|
if !metadata.file_type().is_file() {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::InvalidInput,
|
||||||
|
"configured log path is not a regular file",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_tail(
|
||||||
|
source: &FileLogSource,
|
||||||
|
budget: u64,
|
||||||
|
line_budget: usize,
|
||||||
|
) -> std::io::Result<(Vec<String>, u64, bool)> {
|
||||||
|
use std::io::{Read, Seek, SeekFrom};
|
||||||
|
let mut file = open_file_log(&source.path)?;
|
||||||
|
let length = file.metadata()?.len();
|
||||||
|
let bytes = length.min(budget).min(FILE_LOG_SCAN_BYTES_PER_SOURCE);
|
||||||
|
let start = length.saturating_sub(bytes);
|
||||||
|
file.seek(SeekFrom::Start(start))?;
|
||||||
|
let mut content = Vec::with_capacity(bytes as usize);
|
||||||
|
file.take(bytes).read_to_end(&mut content)?;
|
||||||
|
let truncated = start > 0;
|
||||||
|
if truncated {
|
||||||
|
if let Some(index) = content.iter().position(|byte| *byte == b'\n') {
|
||||||
|
content.drain(..=index);
|
||||||
|
} else {
|
||||||
|
content.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let text = String::from_utf8_lossy(&content);
|
||||||
|
let mut iter = text.lines().rev();
|
||||||
|
let lines = iter
|
||||||
|
.by_ref()
|
||||||
|
.take(line_budget)
|
||||||
|
.map(str::to_owned)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok((lines, bytes, truncated || iter.next().is_some()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalized_record(source: &FileLogSource, line: &str) -> Value {
|
||||||
|
let mut value = serde_json::from_str::<Value>(line).unwrap_or_else(|_| json!({"raw": line}));
|
||||||
|
if !value.is_object() {
|
||||||
|
value = json!({"value": value});
|
||||||
|
}
|
||||||
|
let object = value.as_object_mut().expect("record normalized to object");
|
||||||
|
object.entry("event").or_insert_with(|| {
|
||||||
|
Value::String(
|
||||||
|
if source.kind == "error" {
|
||||||
|
"error"
|
||||||
|
} else {
|
||||||
|
"request"
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
object.insert("_source".into(), Value::String(source.id.clone()));
|
||||||
|
object.insert("_source_kind".into(), Value::String(source.kind.into()));
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains_ci(value: Option<&Value>, needle: &str) -> bool {
|
||||||
|
value
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.is_some_and(|text| text.to_ascii_lowercase().contains(needle))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_matches(value: &Value, query: &FileLogQuery, q: &str) -> bool {
|
||||||
|
let Some(object) = value.as_object() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if !q.is_empty() && !value.to_string().to_ascii_lowercase().contains(q) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(event) = query
|
||||||
|
.event
|
||||||
|
.as_deref()
|
||||||
|
.filter(|v| !v.is_empty() && *v != "all")
|
||||||
|
{
|
||||||
|
if object.get("event").and_then(Value::as_str) != Some(event) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(class) = query
|
||||||
|
.status_class
|
||||||
|
.as_deref()
|
||||||
|
.filter(|v| !v.is_empty() && *v != "all")
|
||||||
|
{
|
||||||
|
let Some(status) = object.get("status").and_then(Value::as_u64) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let expected = class
|
||||||
|
.as_bytes()
|
||||||
|
.first()
|
||||||
|
.and_then(|v| v.checked_sub(b'0'))
|
||||||
|
.map(u64::from);
|
||||||
|
if expected != Some(status / 100) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(client) = query.client_ip.as_deref().filter(|v| !v.is_empty()) {
|
||||||
|
if !contains_ci(object.get("client_ip"), &client.to_ascii_lowercase()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(vhost) = query.vhost.as_deref().filter(|v| !v.is_empty()) {
|
||||||
|
if !contains_ci(object.get("vhost"), &vhost.to_ascii_lowercase()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_file_log_query(query: &FileLogQuery) -> Result<(), ApiError> {
|
||||||
|
let text_ok = |value: &Option<String>, max: usize| {
|
||||||
|
value
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|v| v.len() <= max && !v.chars().any(char::is_control))
|
||||||
|
};
|
||||||
|
if !text_ok(&query.source, 192)
|
||||||
|
|| !text_ok(&query.q, 256)
|
||||||
|
|| !text_ok(&query.client_ip, 128)
|
||||||
|
|| !text_ok(&query.vhost, 256)
|
||||||
|
|| query.offset.unwrap_or(0) > FILE_LOG_MAX_OFFSET
|
||||||
|
|| query
|
||||||
|
.limit
|
||||||
|
.is_some_and(|v| v == 0 || v > FILE_LOG_MAX_LIMIT)
|
||||||
|
|| query.event.as_ref().is_some_and(|v| {
|
||||||
|
!matches!(
|
||||||
|
v.as_str(),
|
||||||
|
"" | "all" | "request" | "protocol_error" | "error"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|| query.status_class.as_ref().is_some_and(|v| {
|
||||||
|
!matches!(
|
||||||
|
v.as_str(),
|
||||||
|
"" | "all" | "1xx" | "2xx" | "3xx" | "4xx" | "5xx"
|
||||||
|
)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"invalid file log filter".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn file_logs_query(
|
||||||
|
State(state): State<Arc<App>>,
|
||||||
|
Query(query): Query<FileLogQuery>,
|
||||||
|
) -> Result<Json<Value>, ApiError> {
|
||||||
|
validate_file_log_query(&query)?;
|
||||||
|
let config = state.config.load().envelope.config.clone();
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let mut sources = configured_file_log_sources(&config);
|
||||||
|
let requested = query.source.as_deref().unwrap_or("all");
|
||||||
|
if requested != "all" {
|
||||||
|
sources.retain(|source| source.id == requested);
|
||||||
|
if sources.is_empty() {
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::BAD_REQUEST,
|
||||||
|
"unknown file log source".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
sources.sort_by(|a, b| b.modified_ms.cmp(&a.modified_ms));
|
||||||
|
sources.truncate(FILE_LOG_MAX_SOURCES);
|
||||||
|
}
|
||||||
|
let q = query.q.clone().unwrap_or_default().to_ascii_lowercase();
|
||||||
|
let offset = query.offset.unwrap_or(0);
|
||||||
|
let limit = query.limit.unwrap_or(100).clamp(1, FILE_LOG_MAX_LIMIT);
|
||||||
|
let mut remaining_bytes = FILE_LOG_SCAN_BYTES_TOTAL;
|
||||||
|
let mut scanned_bytes = 0u64;
|
||||||
|
let mut remaining_lines = FILE_LOG_SCAN_LINES;
|
||||||
|
let mut scanned_lines = 0usize;
|
||||||
|
let mut truncated = false;
|
||||||
|
let mut records = Vec::new();
|
||||||
|
for (index, source) in sources.iter().enumerate() {
|
||||||
|
if remaining_bytes == 0 || remaining_lines == 0 {
|
||||||
|
truncated = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let sources_left = sources.len().saturating_sub(index).max(1);
|
||||||
|
let line_budget = (remaining_lines / sources_left).max(1);
|
||||||
|
match read_tail(source, remaining_bytes, line_budget) {
|
||||||
|
Ok((lines, bytes, source_truncated)) => {
|
||||||
|
remaining_bytes = remaining_bytes.saturating_sub(bytes);
|
||||||
|
remaining_lines = remaining_lines.saturating_sub(lines.len());
|
||||||
|
scanned_bytes += bytes;
|
||||||
|
scanned_lines += lines.len();
|
||||||
|
truncated |= source_truncated;
|
||||||
|
for line in lines {
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let value = normalized_record(source, &line);
|
||||||
|
if record_matches(&value, &query, &q) {
|
||||||
|
records.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
|
||||||
|
Err(_) => truncated = true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
records.sort_by(|a, b| {
|
||||||
|
let time = |value: &Value| value.get("time_ms").and_then(Value::as_u64).unwrap_or(0);
|
||||||
|
time(b).cmp(&time(a))
|
||||||
|
});
|
||||||
|
let total = records.len();
|
||||||
|
let page = records
|
||||||
|
.into_iter()
|
||||||
|
.skip(offset)
|
||||||
|
.take(limit)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(Json(json!({
|
||||||
|
"entries": page,
|
||||||
|
"total": total,
|
||||||
|
"offset": offset,
|
||||||
|
"limit": limit,
|
||||||
|
"truncated": truncated,
|
||||||
|
"scanned_bytes": scanned_bytes,
|
||||||
|
"scanned_lines": scanned_lines,
|
||||||
|
"sources": file_log_sources_json(&sources),
|
||||||
|
})))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
ApiError(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
"file log query worker failed".into(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ include!("access.rs");
|
|||||||
include!("session.rs");
|
include!("session.rs");
|
||||||
include!("status.rs");
|
include!("status.rs");
|
||||||
include!("logging_api.rs");
|
include!("logging_api.rs");
|
||||||
|
include!("file_logs.rs");
|
||||||
include!("operations.rs");
|
include!("operations.rs");
|
||||||
include!("certificates.rs");
|
include!("certificates.rs");
|
||||||
include!("tail.rs");
|
include!("tail.rs");
|
||||||
|
|||||||
+339
-3
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "RustProxy Administration API",
|
"title": "RustProxy Administration API",
|
||||||
"version": "0.25.7",
|
"version": "0.26.0",
|
||||||
"description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust."
|
"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": [
|
||||||
@@ -4799,6 +4799,111 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"FileLogSource": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"label": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"access",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Configured server-side path. Clients cannot supply filesystem paths."
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"JSONL",
|
||||||
|
"RAW"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"exists": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"modified_ms": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"FileLogsInventory": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"sources": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/FileLogSource"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"FileLogsQuery": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entries": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"offset": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"truncated": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"scanned_bytes": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"scanned_lines": {
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64"
|
||||||
|
},
|
||||||
|
"sources": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/components/schemas/FileLogSource"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -5161,7 +5266,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"description": "Sets an HttpOnly SameSite=Strict session cookie. Browser sessions use the configured 7–30 day absolute lifetime. Use the panel sign-in page. No bearer token or URL token is accepted.",
|
"description": "Sets an HttpOnly SameSite=Strict session cookie. Browser sessions use the configured 7\u201330 day absolute lifetime. Use the panel sign-in page. No bearer token or URL token is accepted.",
|
||||||
"security": [],
|
"security": [],
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -9463,6 +9568,237 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"/api/file-logs": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Configured on-disk log sources",
|
||||||
|
"description": "Lists only RustProxy log paths from the active configuration. This endpoint is independent from the Live Logs memory buffer and does not accept a filesystem path.",
|
||||||
|
"tags": [
|
||||||
|
"Monitoring"
|
||||||
|
],
|
||||||
|
"operationId": "get_api_file_logs",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Configured file-log inventory",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/FileLogsInventory"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authentication required or revoked",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Host, origin or CSRF validation failed",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"description": "Bounded worker/session/rate limit",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"503": {
|
||||||
|
"description": "Control-plane storage/worker unavailable",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/file-logs/query": {
|
||||||
|
"get": {
|
||||||
|
"summary": "Query configured on-disk logs",
|
||||||
|
"description": "Reads bounded tails of configured RustProxy access/error log files using server-side source IDs only. The query never accepts arbitrary paths, follows no symlink on Unix, and is separate from /api/logs and /api/live-logs.",
|
||||||
|
"tags": [
|
||||||
|
"Monitoring"
|
||||||
|
],
|
||||||
|
"operationId": "get_api_file_logs_query",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "source",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Source ID returned by /api/file-logs, or all. Never a filesystem path.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "q",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Case-insensitive text search in the bounded scanned records.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 256
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "event",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Event class.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"all",
|
||||||
|
"request",
|
||||||
|
"protocol_error",
|
||||||
|
"error"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "status_class",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "HTTP status class.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"all",
|
||||||
|
"1xx",
|
||||||
|
"2xx",
|
||||||
|
"3xx",
|
||||||
|
"4xx",
|
||||||
|
"5xx"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "client_ip",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Client IP substring filter.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 128
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "vhost",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Vhost substring filter.",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 256
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "offset",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Result offset within the bounded scan.",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 10000,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query",
|
||||||
|
"required": false,
|
||||||
|
"description": "Page size.",
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 500,
|
||||||
|
"default": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Bounded filtered page",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/FileLogsQuery"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Invalid filter or unknown source",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "Authentication required or revoked",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"description": "Host, origin or CSRF validation failed",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"429": {
|
||||||
|
"description": "Bounded worker/session/rate limit",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"503": {
|
||||||
|
"description": "Control-plane storage/worker unavailable",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +83,7 @@ pub fn router(state: Arc<App>) -> Router {
|
|||||||
.route("/cache", get(public::index))
|
.route("/cache", get(public::index))
|
||||||
.route("/transform", get(public::index))
|
.route("/transform", get(public::index))
|
||||||
.route("/logging", get(public::index))
|
.route("/logging", get(public::index))
|
||||||
|
.route("/logs", get(public::index))
|
||||||
.route("/settings", get(public::index))
|
.route("/settings", get(public::index))
|
||||||
.route(
|
.route(
|
||||||
"/assets/login.js",
|
"/assets/login.js",
|
||||||
@@ -183,6 +184,15 @@ pub fn router(state: Arc<App>) -> Router {
|
|||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/assets/file-logs.js",
|
||||||
|
get(|| async {
|
||||||
|
(
|
||||||
|
[("content-type", "text/javascript; charset=utf-8")],
|
||||||
|
include_str!("web/file-logs.js"),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/assets/settings.js",
|
"/assets/settings.js",
|
||||||
get(|| async {
|
get(|| async {
|
||||||
@@ -348,6 +358,8 @@ pub fn router(state: Arc<App>) -> Router {
|
|||||||
.route("/api/config/{section}", put(update_section))
|
.route("/api/config/{section}", put(update_section))
|
||||||
.route("/api/logs", get(logs))
|
.route("/api/logs", get(logs))
|
||||||
.route("/api/live-logs", get(live_logs_stream))
|
.route("/api/live-logs", get(live_logs_stream))
|
||||||
|
.route("/api/file-logs", get(file_logs))
|
||||||
|
.route("/api/file-logs/query", get(file_logs_query))
|
||||||
.route("/api/preferences", get(preferences).put(update_preferences))
|
.route("/api/preferences", get(preferences).put(update_preferences))
|
||||||
.route("/api/audit", get(audit))
|
.route("/api/audit", get(audit))
|
||||||
.route("/api/events", get(events))
|
.route("/api/events", get(events))
|
||||||
|
|||||||
@@ -4,4 +4,5 @@ mod tests {
|
|||||||
include!("tests/part_03.rs");
|
include!("tests/part_03.rs");
|
||||||
include!("tests/part_04.rs");
|
include!("tests/part_04.rs");
|
||||||
include!("tests/part_05.rs");
|
include!("tests/part_05.rs");
|
||||||
|
include!("tests/part_06.rs");
|
||||||
}
|
}
|
||||||
@@ -383,6 +383,7 @@
|
|||||||
"/cache",
|
"/cache",
|
||||||
"/transform",
|
"/transform",
|
||||||
"/logging",
|
"/logging",
|
||||||
|
"/logs",
|
||||||
"/settings",
|
"/settings",
|
||||||
] {
|
] {
|
||||||
let public = router
|
let public = router
|
||||||
|
|||||||
@@ -78,6 +78,9 @@
|
|||||||
for (path, expected) in [
|
for (path, expected) in [
|
||||||
("/api/logs?vhost=demo&limit=8", 200),
|
("/api/logs?vhost=demo&limit=8", 200),
|
||||||
("/api/logs?cache=BAD", 400),
|
("/api/logs?cache=BAD", 400),
|
||||||
|
("/api/file-logs", 200),
|
||||||
|
("/api/file-logs/query?source=all&limit=8", 200),
|
||||||
|
("/api/file-logs/query?limit=0", 400),
|
||||||
("/api/traffic", 200),
|
("/api/traffic", 200),
|
||||||
("/api/cache", 200),
|
("/api/cache", 200),
|
||||||
("/api/transform", 200),
|
("/api/transform", 200),
|
||||||
|
|||||||
@@ -293,6 +293,8 @@
|
|||||||
"/api/cache?limit=invalid",
|
"/api/cache?limit=invalid",
|
||||||
"/api/logs?limit=invalid",
|
"/api/logs?limit=invalid",
|
||||||
"/api/live-logs",
|
"/api/live-logs",
|
||||||
|
"/api/file-logs",
|
||||||
|
"/api/file-logs/query?limit=invalid",
|
||||||
"/api/certificate-api",
|
"/api/certificate-api",
|
||||||
"/api/preferences",
|
"/api/preferences",
|
||||||
"/api/audit",
|
"/api/audit",
|
||||||
@@ -322,6 +324,7 @@
|
|||||||
"/assets/redirects.js",
|
"/assets/redirects.js",
|
||||||
"/assets/host404.js",
|
"/assets/host404.js",
|
||||||
"/assets/settings.js",
|
"/assets/settings.js",
|
||||||
|
"/assets/file-logs.js",
|
||||||
"/assets/acme.js",
|
"/assets/acme.js",
|
||||||
"/api/not-a-route",
|
"/api/not-a-route",
|
||||||
"/not-a-route",
|
"/not-a-route",
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#[test]
|
||||||
|
fn stored_log_inventory_keeps_existing_files_visible_when_writer_is_disabled() {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("access.jsonl");
|
||||||
|
let mut file = std::fs::File::create(&path).unwrap();
|
||||||
|
writeln!(file, r#"{{"event":"request","status":200}}"#).unwrap();
|
||||||
|
|
||||||
|
let mut config = Config::default();
|
||||||
|
config.logging.file.enabled = false;
|
||||||
|
config.logging.file.per_vhost = false;
|
||||||
|
config.logging.file.path = path.to_string_lossy().into_owned();
|
||||||
|
let sources = configured_file_log_sources(&config);
|
||||||
|
let access = sources.iter().find(|source| source.id == "access").unwrap();
|
||||||
|
assert!(!access.enabled);
|
||||||
|
assert!(access.exists);
|
||||||
|
assert!(access.size > 0);
|
||||||
|
assert_eq!(access.path, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stored_log_tail_enforces_line_budget() {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("access.jsonl");
|
||||||
|
let mut file = std::fs::File::create(&path).unwrap();
|
||||||
|
for id in 0..10 {
|
||||||
|
writeln!(file, r#"{{"time_ms":{id},"event":"request"}}"#).unwrap();
|
||||||
|
}
|
||||||
|
drop(file);
|
||||||
|
|
||||||
|
let source = source_from_path(
|
||||||
|
"access".into(),
|
||||||
|
"Access log".into(),
|
||||||
|
"access",
|
||||||
|
path,
|
||||||
|
false,
|
||||||
|
"JSONL",
|
||||||
|
);
|
||||||
|
let (lines, _bytes, truncated) = read_tail(&source, 1024 * 1024, 3).unwrap();
|
||||||
|
assert_eq!(lines.len(), 3);
|
||||||
|
assert!(lines[0].contains(r#""time_ms":9"#));
|
||||||
|
assert!(lines[2].contains(r#""time_ms":7"#));
|
||||||
|
assert!(truncated);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stored_log_query_rejects_oversized_source_identifier() {
|
||||||
|
let query = FileLogQuery {
|
||||||
|
source: Some("x".repeat(193)),
|
||||||
|
..FileLogQuery::default()
|
||||||
|
};
|
||||||
|
assert!(validate_file_log_query(&query).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn stored_log_reader_refuses_symlink_targets() {
|
||||||
|
use std::os::unix::fs::symlink;
|
||||||
|
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let real = dir.path().join("real.jsonl");
|
||||||
|
let link = dir.path().join("link.jsonl");
|
||||||
|
std::fs::write(&real, b"{}\n").unwrap();
|
||||||
|
symlink(&real, &link).unwrap();
|
||||||
|
assert!(open_file_log(&link).is_err());
|
||||||
|
}
|
||||||
@@ -61,7 +61,7 @@ function updateAdminPreferences(patch) {
|
|||||||
}
|
}
|
||||||
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesWrite = Promise.resolve(); }
|
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesWrite = Promise.resolve(); }
|
||||||
window.rustProxyPreferences = { load: loadAdminPreferences, update: updateAdminPreferences, current: () => adminPreferences, applyTheme: applyAdminTheme };
|
window.rustProxyPreferences = { load: loadAdminPreferences, update: updateAdminPreferences, current: () => adminPreferences, applyTheme: applyAdminTheme };
|
||||||
const PANEL_PATHS = { overview: "/overview", vhosts: "/vhosts", "web-server": "/web-server", "docker-vhosts": "/docker-vhosts", redirects: "/redirects", notfound: "/404-hosts", streams: "/streams", "acl-profiles": "/acl-profiles", tls: "/tls", traffic: "/traffic", cache: "/cache", transform: "/transform", settings: "/settings" };
|
const PANEL_PATHS = { overview: "/overview", vhosts: "/vhosts", "web-server": "/web-server", "docker-vhosts": "/docker-vhosts", redirects: "/redirects", notfound: "/404-hosts", streams: "/streams", "acl-profiles": "/acl-profiles", tls: "/tls", traffic: "/traffic", cache: "/cache", transform: "/transform", filelogs: "/logs", settings: "/settings" };
|
||||||
function panelFromLocation() { if (typeof location === "undefined") return "overview"; if (location.pathname === "/configuration" || location.pathname === "/logging") return "settings"; const found = Object.entries(PANEL_PATHS).find(([, path]) => path === location.pathname); return found?.[0] || "overview"; }
|
function panelFromLocation() { if (typeof location === "undefined") return "overview"; if (location.pathname === "/configuration" || location.pathname === "/logging") return "settings"; const found = Object.entries(PANEL_PATHS).find(([, path]) => path === location.pathname); return found?.[0] || "overview"; }
|
||||||
let envelope = null, busy = false, connected = false, currentPanel = panelFromLocation(), previous = null;
|
let envelope = null, busy = false, connected = false, currentPanel = panelFromLocation(), previous = null;
|
||||||
let samples = [], protocolSamples = { h1: [], h2: [], h3: [] }, websocketProtocolSamples = { h1: [], h2: [], h3: [] }, transformSamples = [], polling = false, toastTimer, editingBackend = null, editingRoute = null;
|
let samples = [], protocolSamples = { h1: [], h2: [], h3: [] }, websocketProtocolSamples = { h1: [], h2: [], h3: [] }, transformSamples = [], polling = false, toastTimer, editingBackend = null, editingRoute = null;
|
||||||
@@ -162,6 +162,7 @@ function apiArea(path) {
|
|||||||
if (path.startsWith("/api/certificates") || path.startsWith("/api/acme")) return "tls";
|
if (path.startsWith("/api/certificates") || path.startsWith("/api/acme")) return "tls";
|
||||||
if (path.startsWith("/api/certificate-api")) return "settings";
|
if (path.startsWith("/api/certificate-api")) return "settings";
|
||||||
if (path.startsWith("/api/docker")) return "dockerVhosts";
|
if (path.startsWith("/api/docker")) return "dockerVhosts";
|
||||||
|
if (path.startsWith("/api/file-logs")) return "filelogs";
|
||||||
if (path.startsWith("/api/logs")) return "logging";
|
if (path.startsWith("/api/logs")) return "logging";
|
||||||
if (path.startsWith("/api/audit") || path.startsWith("/api/preferences") || path.startsWith("/api/auth/password") || path.startsWith("/api/auth/logout")) return "security";
|
if (path.startsWith("/api/audit") || path.startsWith("/api/preferences") || path.startsWith("/api/auth/password") || path.startsWith("/api/auth/logout")) return "security";
|
||||||
return null;
|
return null;
|
||||||
@@ -194,6 +195,7 @@ function setPanel(name, navigate = true) {
|
|||||||
if (navigate && typeof location !== "undefined" && ["http:", "https:"].includes(location.protocol) && location.pathname !== PANEL_PATHS[name]) history.pushState({ panel: name }, "", PANEL_PATHS[name]);
|
if (navigate && typeof location !== "undefined" && ["http:", "https:"].includes(location.protocol) && location.pathname !== PANEL_PATHS[name]) history.pushState({ panel: name }, "", PANEL_PATHS[name]);
|
||||||
if (name === "settings") window.rustProxySettings?.syncLocation?.();
|
if (name === "settings") window.rustProxySettings?.syncLocation?.();
|
||||||
if (name === "tls") window.rustProxyAcme?.syncLocation?.();
|
if (name === "tls") window.rustProxyAcme?.syncLocation?.();
|
||||||
|
if (name === "filelogs") void window.rustProxyFileLogs?.refresh();
|
||||||
if (name === "traffic") void window.rustProxyTrafficPanel?.refresh(); if (name === "cache") void window.rustProxyCachePanel?.refresh(); if (name === "transform") void window.rustProxyTransformPanel?.refresh(); if (name === "docker-vhosts") void window.rustProxyDocker?.refresh(false); if (name === "tls") { void window.rustProxyAcme?.refresh(); void refreshCertificateInventory().then(() => { if (authenticated && currentPanel === "tls") renderCertificates(); }); }
|
if (name === "traffic") void window.rustProxyTrafficPanel?.refresh(); if (name === "cache") void window.rustProxyCachePanel?.refresh(); if (name === "transform") void window.rustProxyTransformPanel?.refresh(); if (name === "docker-vhosts") void window.rustProxyDocker?.refresh(false); if (name === "tls") { void window.rustProxyAcme?.refresh(); void refreshCertificateInventory().then(() => { if (authenticated && currentPanel === "tls") renderCertificates(); }); }
|
||||||
}
|
}
|
||||||
function bytes(value) { if (value < 1024) return `${value} B`; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { value /= 1024; if (value < 1024 || unit === "TiB") return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`; } }
|
function bytes(value) { if (value < 1024) return `${value} B`; for (const unit of ["KiB", "MiB", "GiB", "TiB"]) { value /= 1024; if (value < 1024 || unit === "TiB") return `${value.toFixed(value < 10 ? 1 : 0)} ${unit}`; } }
|
||||||
@@ -342,7 +344,7 @@ function authReturnDestination() {
|
|||||||
}
|
}
|
||||||
function lock(notify = true) {
|
function lock(notify = true) {
|
||||||
const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:");
|
const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:");
|
||||||
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); $("logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage("");
|
||||||
for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); }
|
for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); }
|
||||||
$("workspace").querySelectorAll("form").forEach(form => form.reset());
|
$("workspace").querySelectorAll("form").forEach(form => form.reset());
|
||||||
$("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; });
|
$("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; });
|
||||||
@@ -353,7 +355,7 @@ function lock(notify = true) {
|
|||||||
if (navigating) { document.documentElement.classList.add("app-loading"); location.replace(destination); }
|
if (navigating) { document.documentElement.classList.add("app-loading"); location.replace(destination); }
|
||||||
}
|
}
|
||||||
|
|
||||||
async function activate(info) { pendingAuthDestination = null; authMessage(""); $("api-docs").hidden = false; $("logs-open").hidden = false; authEpoch++; sessionInfo = info; csrf = info.csrf_token; authenticated = true; const preferences = await loadAdminPreferences(); window.rustProxyLogs?.applyPreferences(preferences); window.rustProxyVhosts?.applyPreferences(preferences); await loadConfig(); $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false; renderSession(); await refresh(true); startLive(); document.documentElement.classList.remove("app-loading"); }
|
async function activate(info) { pendingAuthDestination = null; authMessage(""); $("api-docs").hidden = false; $("logs-open").hidden = false; $("file-logs-open").hidden = false; authEpoch++; sessionInfo = info; csrf = info.csrf_token; authenticated = true; const preferences = await loadAdminPreferences(); window.rustProxyLogs?.applyPreferences(preferences); window.rustProxyVhosts?.applyPreferences(preferences); await loadConfig(); $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("login").hidden = true; $("workspace").hidden = false; $("logout").hidden = false; renderSession(); await refresh(true); startLive(); document.documentElement.classList.remove("app-loading"); }
|
||||||
async function loadAudit() { if (!authenticated) return; const epoch = authEpoch; try { const data = await api("/api/audit"); if (!authenticated || epoch !== authEpoch) return; const rows = data.entries.map(item => { const tr = el("tr"); for (const value of [new Date(item.time * 1000).toLocaleString(locale()), item.actor, item.action, item.detail]) { const td = el("td", value); td.title = value; tr.append(td); } return tr; }); $("audit-rows").replaceChildren(...rows); $("empty-audit").hidden = rows.length > 0; } catch (e) { toast(e.message, true); } }
|
async function loadAudit() { if (!authenticated) return; const epoch = authEpoch; try { const data = await api("/api/audit"); if (!authenticated || epoch !== authEpoch) return; const rows = data.entries.map(item => { const tr = el("tr"); for (const value of [new Date(item.time * 1000).toLocaleString(locale()), item.actor, item.action, item.detail]) { const td = el("td", value); td.title = value; tr.append(td); } return tr; }); $("audit-rows").replaceChildren(...rows); $("empty-audit").hidden = rows.length > 0; } catch (e) { toast(e.message, true); } }
|
||||||
let tunnelRefreshTimer = null;
|
let tunnelRefreshTimer = null;
|
||||||
function tunnelMessage(value = "", error = false) { const node = $("tunnels-status"); if (!node) return; node.textContent = value; node.classList.toggle("error", !!error); }
|
function tunnelMessage(value = "", error = false) { const node = $("tunnels-status"); if (!node) return; node.textContent = value; node.classList.toggle("error", !!error); }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ function renderSettings() {
|
|||||||
if (!settingsDirty.has("response-form")) {
|
if (!settingsDirty.has("response-form")) {
|
||||||
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
|
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
|
||||||
$("expose-request-id").checked = !!diagnostics.request_id;
|
$("expose-request-id").checked = !!diagnostics.request_id;
|
||||||
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.25.7";
|
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.0";
|
||||||
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
|
||||||
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
||||||
}
|
}
|
||||||
@@ -296,14 +296,14 @@ bindSettingsForm("transform-form", next => {
|
|||||||
max_inflight: Number($("transform-max-inflight").value)
|
max_inflight: Number($("transform-max-inflight").value)
|
||||||
};
|
};
|
||||||
}, "transform");
|
}, "transform");
|
||||||
const LOGGING_SOURCE_FIELDS = ["vhost", "route", "cache_effective_enabled", "transform_effective_enabled", "backend", "id", "time_ms", "method", "host", "path", "http_version", "transport", "websocket_bootstrap", "websocket_phase", "websocket_error", "h3_reset_code", "upstream_status", "upstream_error", "client_ip", "user_agent", "country", "status", "duration_ms", "request_bytes", "response_bytes", "outcome", "cache", "cache_reason"];
|
const LOGGING_SOURCE_FIELDS = ["event", "protocol_error", "error_detail", "local_addr", "tls_sni", "vhost", "route", "cache_effective_enabled", "transform_effective_enabled", "backend", "id", "time_ms", "method", "host", "path", "http_version", "transport", "websocket_bootstrap", "websocket_phase", "websocket_error", "h3_reset_code", "upstream_status", "upstream_error", "client_ip", "user_agent", "country", "status", "duration_ms", "request_bytes", "response_bytes", "outcome", "cache", "cache_reason"];
|
||||||
const LOGGING_PRESETS = {
|
const LOGGING_PRESETS = {
|
||||||
standard: {},
|
standard: {},
|
||||||
minimal: { time_ms: "time_ms", method: "method", host: "host", path: "path", status: "status", duration_ms: "duration_ms" },
|
minimal: { time_ms: "time_ms", method: "method", host: "host", path: "path", status: "status", duration_ms: "duration_ms" },
|
||||||
nginx: { time_ms: "time_ms", remote_addr: "client_ip", http_user_agent: "user_agent", country: "country", request_method: "method", host: "host", request_uri: "path", status: "status", request_time_ms: "duration_ms", request_length: "request_bytes", bytes_sent: "response_bytes", upstream: "backend", cache: "cache", cache_reason: "cache_reason" },
|
nginx: { time_ms: "time_ms", remote_addr: "client_ip", http_user_agent: "user_agent", country: "country", request_method: "method", host: "host", request_uri: "path", status: "status", request_time_ms: "duration_ms", request_length: "request_bytes", bytes_sent: "response_bytes", upstream: "backend", cache: "cache", cache_reason: "cache_reason" },
|
||||||
ecs: { "event.created_ms": "time_ms", "client.address": "client_ip", "user_agent.original": "user_agent", "client.geo.country_iso_code": "country", "http.request.method": "method", "server.address": "host", "url.path": "path", "http.response.status_code": "status", "event.duration_ms": "duration_ms", "http.request.body.bytes": "request_bytes", "http.response.body.bytes": "response_bytes", "service.target.name": "backend", "event.outcome": "outcome", "cache.result": "cache", "cache.reason": "cache_reason" }
|
ecs: { "event.created_ms": "time_ms", "client.address": "client_ip", "user_agent.original": "user_agent", "client.geo.country_iso_code": "country", "http.request.method": "method", "server.address": "host", "url.path": "path", "http.response.status_code": "status", "event.duration_ms": "duration_ms", "http.request.body.bytes": "request_bytes", "http.response.body.bytes": "response_bytes", "service.target.name": "backend", "event.outcome": "outcome", "cache.result": "cache", "cache.reason": "cache_reason" }
|
||||||
};
|
};
|
||||||
const LOGGING_PREVIEW_VALUES = { vhost: "app", route: "root", cache_effective_enabled: true, transform_effective_enabled: false, backend: "origin-1", id: 1842, time_ms: 1769328123456, method: "CONNECT", host: "app.example", path: "/socket.io/", http_version: "3", transport: "h3", websocket_bootstrap: "h3_extended_connect", websocket_phase: "tunnel_closed", websocket_error: null, h3_reset_code: null, upstream_status: 101, client_ip: "203.0.113.24", user_agent: "Mozilla/5.0 RustProxy-Test", country: "PL", status: 200, duration_ms: 12, request_bytes: 0, response_bytes: 4821, outcome: "websocket_closed", cache: "BYPASS", cache_reason: "request_upgrade" };
|
const LOGGING_PREVIEW_VALUES = { event: "request", protocol_error: null, error_detail: null, local_addr: "192.0.2.1:443", tls_sni: "app.example", vhost: "app", route: "root", cache_effective_enabled: true, transform_effective_enabled: false, backend: "origin-1", id: 1842, time_ms: 1769328123456, method: "CONNECT", host: "app.example", path: "/socket.io/", http_version: "3", transport: "h3", websocket_bootstrap: "h3_extended_connect", websocket_phase: "tunnel_closed", websocket_error: null, h3_reset_code: null, upstream_status: 101, client_ip: "203.0.113.24", user_agent: "Mozilla/5.0 RustProxy-Test", country: "PL", status: 200, duration_ms: 12, request_bytes: 0, response_bytes: 4821, outcome: "websocket_closed", cache: "BYPASS", cache_reason: "request_upgrade" };
|
||||||
function parseJsonObjectValue(value, target = null) { let parsed; try { parsed = JSON.parse(value || "{}"); } catch { throw fieldError(target, t("loggingJsonInvalid")); } if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw fieldError(target, t("loggingJsonInvalid")); return parsed; }
|
function parseJsonObjectValue(value, target = null) { let parsed; try { parsed = JSON.parse(value || "{}"); } catch { throw fieldError(target, t("loggingJsonInvalid")); } if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") throw fieldError(target, t("loggingJsonInvalid")); return parsed; }
|
||||||
function loggingMappingKey(mapping) { return JSON.stringify(Object.entries(mapping || {}).sort(([a], [b]) => a.localeCompare(b))); }
|
function loggingMappingKey(mapping) { return JSON.stringify(Object.entries(mapping || {}).sort(([a], [b]) => a.localeCompare(b))); }
|
||||||
function detectLoggingPreset(mapping) { const key = loggingMappingKey(mapping); for (const [name, preset] of Object.entries(LOGGING_PRESETS)) if (loggingMappingKey(preset) === key) return name; return "custom"; }
|
function detectLoggingPreset(mapping) { const key = loggingMappingKey(mapping); for (const [name, preset] of Object.entries(LOGGING_PRESETS)) if (loggingMappingKey(preset) === key) return name; return "custom"; }
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
(() => {
|
||||||
|
let inventory = null, offset = 0, lastTotal = 0, busy = false;
|
||||||
|
const sourceSelect = () => $("file-log-source");
|
||||||
|
const currentLimit = () => Math.max(1, Math.min(500, Number($("file-log-limit").value) || 100));
|
||||||
|
function sourceOption(source) {
|
||||||
|
const label = `${source.label} · ${source.format} · ${bytes(source.size || 0)}${source.enabled ? ` · ${t("enabled")}` : ""}`;
|
||||||
|
const option = el("option", label); option.value = source.id; return option;
|
||||||
|
}
|
||||||
|
function renderInventory(value) {
|
||||||
|
inventory = value;
|
||||||
|
const selected = sourceSelect().value || "all";
|
||||||
|
const sources = Array.isArray(value?.sources) ? value.sources : [];
|
||||||
|
const options = [Object.assign(el("option", t("fileLogsAllSources")), { value: "all" }), ...sources.map(sourceOption)];
|
||||||
|
sourceSelect().replaceChildren(...options);
|
||||||
|
sourceSelect().value = ["all", ...sources.map(source => source.id)].includes(selected) ? selected : "all";
|
||||||
|
$("file-log-inventory").textContent = sources.length
|
||||||
|
? sources.map(source => `${source.label}: ${source.path} (${bytes(source.size || 0)})`).join(" · ")
|
||||||
|
: t("fileLogsNoSources");
|
||||||
|
}
|
||||||
|
async function loadInventory() {
|
||||||
|
const value = await api("/api/file-logs"); renderInventory(value); return value;
|
||||||
|
}
|
||||||
|
function entryTime(entry) {
|
||||||
|
if (entry.time_ms) return new Date(Number(entry.time_ms)).toLocaleString(locale());
|
||||||
|
if (entry.timestamp) return String(entry.timestamp);
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
function entrySummary(entry) {
|
||||||
|
if (entry.protocol_error) return entry.protocol_error;
|
||||||
|
if (entry.method || entry.path) return `${entry.method || ""} ${entry.host || ""}${entry.path || ""}`.trim();
|
||||||
|
if (entry.message) return entry.message;
|
||||||
|
if (entry.raw) return entry.raw;
|
||||||
|
return entry.outcome || "—";
|
||||||
|
}
|
||||||
|
function entryStatus(entry) {
|
||||||
|
const status = Number(entry.status) || 0;
|
||||||
|
return status ? String(status) : "—";
|
||||||
|
}
|
||||||
|
function renderRows(entries) {
|
||||||
|
const rows = entries.map(entry => {
|
||||||
|
const row = el("tr"); row.tabIndex = 0;
|
||||||
|
for (const value of [entryTime(entry), entry.event || "—", entryStatus(entry), entry.client_ip || "—", entry.vhost || "—", entrySummary(entry), entry._source || "—"]) {
|
||||||
|
const cell = el("td", String(value)); cell.title = String(value); row.append(cell);
|
||||||
|
}
|
||||||
|
const show = () => { $("file-log-detail").textContent = JSON.stringify(entry, null, 2); };
|
||||||
|
row.addEventListener("click", show); row.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); show(); } });
|
||||||
|
return row;
|
||||||
|
});
|
||||||
|
$("file-log-rows").replaceChildren(...rows);
|
||||||
|
$("file-logs-empty").hidden = rows.length !== 0;
|
||||||
|
if (!rows.length) $("file-log-detail").textContent = t("fileLogsSelectRecord");
|
||||||
|
}
|
||||||
|
function queryString() {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.set("source", sourceSelect().value || "all");
|
||||||
|
for (const [id, name] of [["file-log-q", "q"], ["file-log-event", "event"], ["file-log-status", "status_class"], ["file-log-client", "client_ip"], ["file-log-vhost", "vhost"]]) {
|
||||||
|
const value = $(id).value.trim(); if (value && value !== "all") params.set(name, value);
|
||||||
|
}
|
||||||
|
params.set("offset", String(offset)); params.set("limit", String(currentLimit()));
|
||||||
|
return params.toString();
|
||||||
|
}
|
||||||
|
async function refresh(reset = false) {
|
||||||
|
if (!authenticated || busy) return;
|
||||||
|
if (reset) offset = 0;
|
||||||
|
busy = true; $("file-logs-refresh").disabled = true;
|
||||||
|
try {
|
||||||
|
if (!inventory) await loadInventory();
|
||||||
|
const value = await api(`/api/file-logs/query?${queryString()}`);
|
||||||
|
lastTotal = Number(value.total) || 0; renderRows(Array.isArray(value.entries) ? value.entries : []);
|
||||||
|
const from = lastTotal ? offset + 1 : 0, to = Math.min(offset + currentLimit(), lastTotal);
|
||||||
|
$("file-log-summary").textContent = t("fileLogsSummary")
|
||||||
|
.replace("{from}", fmt(from)).replace("{to}", fmt(to)).replace("{total}", fmt(lastTotal))
|
||||||
|
.replace("{bytes}", bytes(Number(value.scanned_bytes) || 0)).replace("{lines}", fmt(Number(value.scanned_lines) || 0))
|
||||||
|
+ (value.truncated ? ` · ${t("fileLogsTruncated")}` : "");
|
||||||
|
$("file-logs-prev").disabled = offset === 0;
|
||||||
|
$("file-logs-next").disabled = offset + currentLimit() >= lastTotal;
|
||||||
|
} catch (error) { toast(error.message, true); }
|
||||||
|
finally { busy = false; $("file-logs-refresh").disabled = false; }
|
||||||
|
}
|
||||||
|
function reset() {
|
||||||
|
inventory = null; offset = 0; lastTotal = 0; $("file-log-rows").replaceChildren(); $("file-log-detail").textContent = "";
|
||||||
|
}
|
||||||
|
$("file-logs-open").addEventListener("click", () => setPanel("filelogs"));
|
||||||
|
$("settings-file-logs-open").addEventListener("click", () => setPanel("filelogs"));
|
||||||
|
$("file-logs-refresh").addEventListener("click", () => refresh(true));
|
||||||
|
$("file-logs-filter").addEventListener("submit", event => { event.preventDefault(); void refresh(true); });
|
||||||
|
$("file-logs-prev").addEventListener("click", () => { offset = Math.max(0, offset - currentLimit()); void refresh(); });
|
||||||
|
$("file-logs-next").addEventListener("click", () => { if (offset + currentLimit() < lastTotal) { offset += currentLimit(); void refresh(); } });
|
||||||
|
sourceSelect().addEventListener("change", () => refresh(true));
|
||||||
|
window.rustProxyFileLogs = { refresh: () => refresh(false), reset, inventory: loadInventory };
|
||||||
|
})();
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
<script defer="" src="/assets/transform-panel.js"></script>
|
<script defer="" src="/assets/transform-panel.js"></script>
|
||||||
<script defer="" src="/assets/acme.js"></script>
|
<script defer="" src="/assets/acme.js"></script>
|
||||||
<script defer="" src="/assets/settings.js"></script>
|
<script defer="" src="/assets/settings.js"></script>
|
||||||
|
<script defer="" src="/assets/file-logs.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -63,7 +64,8 @@
|
|||||||
<header class="topbar"><span class="breadcrumb">CONTROL ROOM <span>/</span> <b
|
<header class="topbar"><span class="breadcrumb">CONTROL ROOM <span>/</span> <b
|
||||||
id="page-label">Overview</b></span>
|
id="page-label">Overview</b></span>
|
||||||
<div class="top-actions"><a class="quiet" href="/docs" data-i18n="documentation" id="api-docs"
|
<div class="top-actions"><a class="quiet" href="/docs" data-i18n="documentation" id="api-docs"
|
||||||
hidden="">Documentation</a><button class="quiet" data-i18n="liveLogs" hidden="" id="logs-open"
|
hidden="">Documentation</a><button class="quiet" data-i18n="logs" hidden="" id="file-logs-open"
|
||||||
|
type="button">Logs</button><button class="quiet" data-i18n="liveLogs" hidden="" id="logs-open"
|
||||||
type="button">Live
|
type="button">Live
|
||||||
access logs</button><select aria-label="Language" class="language-select"
|
access logs</button><select aria-label="Language" class="language-select"
|
||||||
data-i18n-aria="language" id="language"></select><button class="quiet" data-i18n="about"
|
data-i18n-aria="language" id="language"></select><button class="quiet" data-i18n="about"
|
||||||
@@ -1494,6 +1496,79 @@
|
|||||||
yet.</p>
|
yet.</p>
|
||||||
</article>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
|
<section class="panel" hidden="" id="panel-filelogs">
|
||||||
|
<article class="card file-logs-workspace">
|
||||||
|
<div class="card-heading">
|
||||||
|
<div><span class="small-label" data-i18n="fileLogsMode">LOCAL / READ-ONLY / BOUNDED</span>
|
||||||
|
<h2 data-i18n="fileLogsTitle">Stored log explorer</h2>
|
||||||
|
</div>
|
||||||
|
<button class="outline" data-i18n="refresh" id="file-logs-refresh"
|
||||||
|
type="button">Refresh</button>
|
||||||
|
</div>
|
||||||
|
<p class="fine" data-i18n="fileLogsHelp">Reads only configured RustProxy log files. It is
|
||||||
|
independent from Live Logs and never accepts an arbitrary filesystem path.</p>
|
||||||
|
<form class="file-logs-filter" id="file-logs-filter">
|
||||||
|
<label><span data-i18n="fileLogsSource">Source</span><select id="file-log-source">
|
||||||
|
<option value="all">All configured files</option>
|
||||||
|
</select></label>
|
||||||
|
<label class="file-log-search"><span data-i18n="search">Search</span><input id="file-log-q"
|
||||||
|
maxlength="256" data-i18n-placeholder="fileLogsSearchPlaceholder"
|
||||||
|
placeholder="IP, path, error, request id..." /></label>
|
||||||
|
<label><span data-i18n="fileLogsEvent">Event</span><select id="file-log-event">
|
||||||
|
<option data-i18n="all" value="all">All</option>
|
||||||
|
<option value="request">request</option>
|
||||||
|
<option value="protocol_error">protocol_error</option>
|
||||||
|
<option value="error">error</option>
|
||||||
|
</select></label>
|
||||||
|
<label><span data-i18n="status">Status</span><select id="file-log-status">
|
||||||
|
<option data-i18n="all" value="all">All</option>
|
||||||
|
<option value="1xx">1xx</option>
|
||||||
|
<option value="2xx">2xx</option>
|
||||||
|
<option value="3xx">3xx</option>
|
||||||
|
<option value="4xx">4xx</option>
|
||||||
|
<option value="5xx">5xx</option>
|
||||||
|
</select></label>
|
||||||
|
<label><span data-i18n="clientIp">Client IP</span><input id="file-log-client"
|
||||||
|
maxlength="128" placeholder="192.0.2.10" /></label>
|
||||||
|
<label><span data-i18n="vhost">Vhost</span><input id="file-log-vhost"
|
||||||
|
maxlength="256" /></label>
|
||||||
|
<label><span data-i18n="fileLogsPageSize">Page size</span><select id="file-log-limit">
|
||||||
|
<option>50</option>
|
||||||
|
<option selected>100</option>
|
||||||
|
<option>250</option>
|
||||||
|
<option>500</option>
|
||||||
|
</select></label>
|
||||||
|
<button class="primary" data-i18n="applyFilters" type="submit">Apply filters</button>
|
||||||
|
</form>
|
||||||
|
<p class="fine file-log-inventory" id="file-log-inventory"></p>
|
||||||
|
<p class="fine" id="file-log-summary" role="status"></p>
|
||||||
|
<div class="table-scroll file-logs-table">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th data-i18n="time">Time</th>
|
||||||
|
<th data-i18n="fileLogsEvent">Event</th>
|
||||||
|
<th data-i18n="status">Status</th>
|
||||||
|
<th data-i18n="clientIp">Client IP</th>
|
||||||
|
<th data-i18n="vhost">Vhost</th>
|
||||||
|
<th data-i18n="fileLogsRecord">Request / reason</th>
|
||||||
|
<th data-i18n="fileLogsSource">Source</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="file-log-rows"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="empty" data-i18n="fileLogsEmpty" id="file-logs-empty">No records match the current
|
||||||
|
filters.</p>
|
||||||
|
<div class="file-logs-pager"><button class="quiet" data-i18n="previous" disabled
|
||||||
|
id="file-logs-prev" type="button">Previous</button><button class="quiet"
|
||||||
|
data-i18n="next" disabled id="file-logs-next" type="button">Next</button></div>
|
||||||
|
<details open class="file-log-detail-card">
|
||||||
|
<summary data-i18n="fileLogsDetails">Record details</summary>
|
||||||
|
<pre class="metrics-example" id="file-log-detail"></pre>
|
||||||
|
</details>
|
||||||
|
</article>
|
||||||
|
</section>
|
||||||
<section class="panel" hidden="" id="panel-settings">
|
<section class="panel" hidden="" id="panel-settings">
|
||||||
<div class="settings-shortcuts">
|
<div class="settings-shortcuts">
|
||||||
<div><b data-i18n="security">Security</b>
|
<div><b data-i18n="security">Security</b>
|
||||||
@@ -1667,7 +1742,7 @@
|
|||||||
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
|
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
|
||||||
</select>
|
</select>
|
||||||
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
|
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
|
||||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.25.7"
|
id="server-value" maxlength="256" placeholder="RustProxy/v0.26.0"
|
||||||
spellcheck="false" />
|
spellcheck="false" />
|
||||||
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
|
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
|
||||||
<div class="switch-grid">
|
<div class="switch-grid">
|
||||||
@@ -2065,6 +2140,17 @@
|
|||||||
<p class="fine" data-i18n="loggingHelp">Live log viewing stays in memory.
|
<p class="fine" data-i18n="loggingHelp">Live log viewing stays in memory.
|
||||||
External sinks
|
External sinks
|
||||||
run on a bounded background queue and never block proxy request I/O.</p>
|
run on a bounded background queue and never block proxy request I/O.</p>
|
||||||
|
<article class="card soft-card file-logs-launcher">
|
||||||
|
<div>
|
||||||
|
<h3 class="subheading" data-i18n="fileLogsTitle">Stored log explorer
|
||||||
|
</h3>
|
||||||
|
<p class="fine" data-i18n="fileLogsSettingsHelp">Browse configured
|
||||||
|
access/error files with server-side filters. File logs can be off
|
||||||
|
while Live Logs continues to use its memory buffer.</p>
|
||||||
|
</div>
|
||||||
|
<button class="outline" data-i18n="openLogs" id="settings-file-logs-open"
|
||||||
|
type="button">Open Logs</button>
|
||||||
|
</article>
|
||||||
<article class="card soft-card">
|
<article class="card soft-card">
|
||||||
<h3 class="subheading" data-i18n="loggingCaptureTitle">Captured request
|
<h3 class="subheading" data-i18n="loggingCaptureTitle">Captured request
|
||||||
dimensions</h3>
|
dimensions</h3>
|
||||||
@@ -2455,7 +2541,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer class="page-footer"><span>RustProxy <b>0.25.7</b></span><span id="footer-status">API - /
|
<footer class="page-footer"><span>RustProxy <b>0.26.0</b></span><span id="footer-status">API - /
|
||||||
dropped: 0</span></footer>
|
dropped: 0</span></footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -3166,7 +3252,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.25.7</strong></div>
|
<div><span class="small-label">RustProxy</span><strong>0.26.0</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<dl class="about-meta">
|
<dl class="about-meta">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
const node = id => document.getElementById(id);
|
const node = id => document.getElementById(id);
|
||||||
const themes = new Set(["dark", "light"]);
|
const themes = new Set(["dark", "light"]);
|
||||||
try { const cached = localStorage.getItem("rp-theme"); document.documentElement.dataset.theme = themes.has(cached) ? cached : "dark"; } catch { document.documentElement.dataset.theme = "dark"; }
|
try { const cached = localStorage.getItem("rp-theme"); document.documentElement.dataset.theme = themes.has(cached) ? cached : "dark"; } catch { document.documentElement.dataset.theme = "dark"; }
|
||||||
const panelPaths = new Set(["/overview", "/vhosts", "/docker-vhosts", "/redirects", "/404-hosts", "/streams", "/acl-profiles", "/configuration", "/tls", "/traffic", "/cache", "/transform", "/logging", "/settings"]);
|
const panelPaths = new Set(["/overview", "/vhosts", "/docker-vhosts", "/redirects", "/404-hosts", "/streams", "/acl-profiles", "/configuration", "/tls", "/traffic", "/cache", "/transform", "/logging", "/logs", "/settings"]);
|
||||||
const settingsTabs = new Set(["listeners", "connections", "docker-agents", "response", "cache", "transform", "headers", "forwarding", "pages", "logging", "prometheus", "administration", "tls-policy", "manual-config", "security"]);
|
const settingsTabs = new Set(["listeners", "connections", "docker-agents", "response", "cache", "transform", "headers", "forwarding", "pages", "logging", "prometheus", "administration", "tls-policy", "manual-config", "security"]);
|
||||||
let packs = new Map(), fallback = null, language = null, setupRequired = false, ready = false;
|
let packs = new Map(), fallback = null, language = null, setupRequired = false, ready = false;
|
||||||
try { language = localStorage.getItem("rp-language"); } catch { }
|
try { language = localStorage.getItem("rp-language"); } catch { }
|
||||||
|
|||||||
@@ -8198,4 +8198,84 @@ button.config-history-day.is-selected {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-workspace {
|
||||||
|
display: grid;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-filter {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, minmax(120px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-filter label {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-filter .file-log-search {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-table {
|
||||||
|
max-height: 52vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-table tbody tr {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-table td {
|
||||||
|
max-width: 340px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-pager {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-log-detail-card pre {
|
||||||
|
max-height: 32vh;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-launcher {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-log-inventory {
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.file-logs-filter {
|
||||||
|
grid-template-columns: repeat(3, minmax(140px, 1fr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.file-logs-filter {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-filter .file-log-search {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-launcher {
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+63
-122
@@ -203,10 +203,15 @@ pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duratio
|
|||||||
_=tasks.join_next(),if !tasks.is_empty()=>{},
|
_=tasks.join_next(),if !tasks.is_empty()=>{},
|
||||||
incoming=bound.endpoint.accept()=>{
|
incoming=bound.endpoint.accept()=>{
|
||||||
let Some(incoming)=incoming else {break;};
|
let Some(incoming)=incoming else {break;};
|
||||||
|
let peer=incoming.remote_address();
|
||||||
if !incoming.remote_address_validated() {let _=incoming.retry();continue;}
|
if !incoming.remote_address_validated() {let _=incoming.retry();continue;}
|
||||||
let Ok(permit)=slots.clone().try_acquire_owned() else {
|
let Ok(permit)=slots.clone().try_acquire_owned() else {
|
||||||
state.metrics.http3_rejected_connections.fetch_add(1,Ordering::Relaxed);
|
state.metrics.http3_rejected_connections.fetch_add(1,Ordering::Relaxed);
|
||||||
state.metrics.rejected_connections.fetch_add(1,Ordering::Relaxed);
|
state.metrics.rejected_connections.fetch_add(1,Ordering::Relaxed);
|
||||||
|
state.logs.push_protocol_error(
|
||||||
|
"http3_connection_limit_rejected", "HTTP/3 connection concurrency limit reached",
|
||||||
|
peer, bound.address, "quic", "3", 0, None,
|
||||||
|
);
|
||||||
incoming.refuse();continue;
|
incoming.refuse();continue;
|
||||||
};
|
};
|
||||||
let mut config=bound.server_config.clone();
|
let mut config=bound.server_config.clone();
|
||||||
@@ -214,7 +219,15 @@ pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duratio
|
|||||||
// Certificate changes apply to new handshakes without changing
|
// Certificate changes apply to new handshakes without changing
|
||||||
// active transport limits or regenerating the Retry token key.
|
// active transport limits or regenerating the Retry token key.
|
||||||
config.crypto=crypto;
|
config.crypto=crypto;
|
||||||
let connecting=match incoming.accept_with(Arc::new(config)) {Ok(c)=>c,Err(_)=>continue};
|
let connecting=match incoming.accept_with(Arc::new(config)) {
|
||||||
|
Ok(c)=>c,
|
||||||
|
Err(error)=>{
|
||||||
|
state.logs.push_protocol_error(
|
||||||
|
"http3_accept_error", error, peer, bound.address, "quic", "3", 0, None,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
let router=router.clone();let state=state.clone();let address=bound.address;
|
let router=router.clone();let state=state.clone();let address=bound.address;
|
||||||
let limit=bound.settings.max_concurrent_bidistream as usize;
|
let limit=bound.settings.max_concurrent_bidistream as usize;
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
@@ -222,7 +235,21 @@ pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duratio
|
|||||||
_=state.shutdown.cancelled()=>return,
|
_=state.shutdown.cancelled()=>return,
|
||||||
result=tokio::time::timeout(Duration::from_secs(10),connecting)=>match result {
|
result=tokio::time::timeout(Duration::from_secs(10),connecting)=>match result {
|
||||||
Ok(Ok(connection))=>connection,
|
Ok(Ok(connection))=>connection,
|
||||||
_=>{state.metrics.tls_handshake_errors.fetch_add(1,Ordering::Relaxed);return;},
|
Ok(Err(error))=>{
|
||||||
|
state.metrics.tls_handshake_errors.fetch_add(1,Ordering::Relaxed);
|
||||||
|
state.logs.push_protocol_error(
|
||||||
|
"http3_handshake_error", error, peer, address, "quic", "3", 0, None,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
Err(_)=>{
|
||||||
|
state.metrics.tls_handshake_errors.fetch_add(1,Ordering::Relaxed);
|
||||||
|
state.logs.push_protocol_error(
|
||||||
|
"http3_handshake_timeout", "HTTP/3 QUIC/TLS handshake timeout",
|
||||||
|
peer, address, "quic", "3", 0, None,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// A wildcard without destination metadata must fail closed.
|
// A wildcard without destination metadata must fail closed.
|
||||||
@@ -237,7 +264,10 @@ pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duratio
|
|||||||
state.metrics.http3_active_connections.fetch_add(1,Ordering::Relaxed);
|
state.metrics.http3_active_connections.fetch_add(1,Ordering::Relaxed);
|
||||||
state.metrics.tls_connections.fetch_add(1,Ordering::Relaxed);
|
state.metrics.tls_connections.fetch_add(1,Ordering::Relaxed);
|
||||||
let _lease=Lease {_permit:permit,metrics:state.metrics.clone()};
|
let _lease=Lease {_permit:permit,metrics:state.metrics.clone()};
|
||||||
if let Err(error)=connection_loop(connection,local,router,state,limit,grace).await {
|
if let Err(error)=connection_loop(connection,local,router,state.clone(),limit,grace).await {
|
||||||
|
state.logs.push_protocol_error(
|
||||||
|
"http3_connection_error", &error, peer, local, "quic", "3", 0, None,
|
||||||
|
);
|
||||||
tracing::debug!(%error,"HTTP/3 connection ended");
|
tracing::debug!(%error,"HTTP/3 connection ended");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -285,16 +315,42 @@ async fn connection_loop(
|
|||||||
next=h3.accept()=>match next {
|
next=h3.accept()=>match next {
|
||||||
Ok(Some(resolver))=>{
|
Ok(Some(resolver))=>{
|
||||||
// Completed JoinSet entries are drained before accepting more.
|
// Completed JoinSet entries are drained before accepting more.
|
||||||
if requests.len()>=limit {drop(resolver);continue;}
|
if requests.len()>=limit {
|
||||||
|
state.logs.push_protocol_error(
|
||||||
|
"http3_stream_limit_rejected", "HTTP/3 concurrent request stream limit reached",
|
||||||
|
connection.remote_address(), local, "quic", "3", 0, None,
|
||||||
|
);
|
||||||
|
drop(resolver);continue;
|
||||||
|
}
|
||||||
let router=router.clone();let state=state.clone();let peer=connection.remote_address();
|
let router=router.clone();let state=state.clone();let peer=connection.remote_address();
|
||||||
requests.spawn(async move {
|
requests.spawn(async move {
|
||||||
let Ok(Ok((mut request,stream)))=tokio::time::timeout(state.header_timeout,resolver.resolve_request()).await else {return;};
|
let resolved=tokio::time::timeout(state.header_timeout,resolver.resolve_request()).await;
|
||||||
|
let (mut request,stream)=match resolved {
|
||||||
|
Ok(Ok(request))=>request,
|
||||||
|
Ok(Err(error))=>{
|
||||||
|
state.logs.push_protocol_error(
|
||||||
|
"http3_request_error", error, peer, local, "quic", "3", 400, None,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
Err(_)=>{
|
||||||
|
state.logs.push_protocol_error(
|
||||||
|
"http3_header_timeout", "HTTP/3 request header timeout",
|
||||||
|
peer, local, "quic", "3", 408, None,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
};
|
||||||
*request.version_mut()=Version::HTTP_3;
|
*request.version_mut()=Version::HTTP_3;
|
||||||
let protocol=request.extensions().get::<h3::ext::Protocol>().map(|protocol|protocol.as_str().to_owned());
|
let protocol=request.extensions().get::<h3::ext::Protocol>().map(|protocol|protocol.as_str().to_owned());
|
||||||
let extended_connect=request.method()==Method::CONNECT && protocol.is_some();
|
let extended_connect=request.method()==Method::CONNECT && protocol.is_some();
|
||||||
let websocket=extended_connect && protocol.as_deref()==Some("websocket");
|
let websocket=extended_connect && protocol.as_deref()==Some("websocket");
|
||||||
let (mut send,receive)=stream.split();
|
let (mut send,receive)=stream.split();
|
||||||
if invalid_headers(request.headers()) {
|
if invalid_headers(request.headers()) {
|
||||||
|
state.logs.push_protocol_error(
|
||||||
|
"http3_invalid_headers", "HTTP/3 request contains forbidden connection-specific headers",
|
||||||
|
peer, local, "quic", "3", 400, None,
|
||||||
|
);
|
||||||
send.stop_stream(Code::H3_MESSAGE_ERROR);return;
|
send.stop_stream(Code::H3_MESSAGE_ERROR);return;
|
||||||
}
|
}
|
||||||
let expected=request.headers().get("content-length").and_then(|v|v.to_str().ok()).and_then(|v|v.parse::<u64>().ok());
|
let expected=request.headers().get("content-length").and_then(|v|v.to_str().ok()).and_then(|v|v.parse::<u64>().ok());
|
||||||
@@ -377,122 +433,7 @@ async fn connection_loop(
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn invalid_headers(headers: &HeaderMap) -> bool {
|
include!("http3/body.rs");
|
||||||
[
|
|
||||||
"connection",
|
|
||||||
"keep-alive",
|
|
||||||
"proxy-connection",
|
|
||||||
"transfer-encoding",
|
|
||||||
"upgrade",
|
|
||||||
]
|
|
||||||
.iter()
|
|
||||||
.any(|name| headers.contains_key(*name))
|
|
||||||
|| headers
|
|
||||||
.get_all("te")
|
|
||||||
.iter()
|
|
||||||
.any(|value| value.as_bytes() != b"trailers")
|
|
||||||
|| headers.get_all("content-length").iter().count() > 1
|
|
||||||
|| headers.get("content-length").is_some_and(|value| {
|
|
||||||
value
|
|
||||||
.to_str()
|
|
||||||
.ok()
|
|
||||||
.and_then(|s| s.parse::<u64>().ok())
|
|
||||||
.is_none()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
struct ReceiveBody {
|
|
||||||
stream: h3::server::RequestStream<h3::quinn::RecvStream, Bytes>,
|
|
||||||
expected: Option<u64>,
|
|
||||||
received: u64,
|
|
||||||
data_done: bool,
|
|
||||||
done: bool,
|
|
||||||
}
|
|
||||||
impl HttpBody for ReceiveBody {
|
|
||||||
type Data = Bytes;
|
|
||||||
type Error = io::Error;
|
|
||||||
fn poll_frame(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<std::result::Result<Frame<Bytes>, io::Error>>> {
|
|
||||||
let this = self.get_mut();
|
|
||||||
if this.done {
|
|
||||||
return Poll::Ready(None);
|
|
||||||
}
|
|
||||||
if !this.data_done {
|
|
||||||
match this.stream.poll_recv_data(cx) {
|
|
||||||
Poll::Pending => return Poll::Pending,
|
|
||||||
Poll::Ready(Err(error)) => {
|
|
||||||
this.done = true;
|
|
||||||
return Poll::Ready(Some(Err(io::Error::other(error.to_string()))));
|
|
||||||
}
|
|
||||||
Poll::Ready(Ok(Some(mut data))) => {
|
|
||||||
let length = data.remaining();
|
|
||||||
this.received = this.received.saturating_add(length as u64);
|
|
||||||
if this
|
|
||||||
.expected
|
|
||||||
.is_some_and(|expected| this.received > expected)
|
|
||||||
{
|
|
||||||
drop(data);
|
|
||||||
this.done = true;
|
|
||||||
this.stream.stop_sending(Code::H3_MESSAGE_ERROR);
|
|
||||||
return Poll::Ready(Some(Err(io::Error::other(
|
|
||||||
"HTTP/3 Content-Length exceeded",
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
return Poll::Ready(Some(Ok(Frame::data(data.copy_to_bytes(length)))));
|
|
||||||
}
|
|
||||||
Poll::Ready(Ok(None)) => this.data_done = true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match this.stream.poll_recv_trailers(cx) {
|
|
||||||
Poll::Pending => Poll::Pending,
|
|
||||||
Poll::Ready(Err(error)) => {
|
|
||||||
this.done = true;
|
|
||||||
Poll::Ready(Some(Err(io::Error::other(error.to_string()))))
|
|
||||||
}
|
|
||||||
Poll::Ready(Ok(trailers)) => {
|
|
||||||
this.done = true;
|
|
||||||
if this
|
|
||||||
.expected
|
|
||||||
.is_some_and(|expected| expected != this.received)
|
|
||||||
{
|
|
||||||
this.stream.stop_sending(Code::H3_MESSAGE_ERROR);
|
|
||||||
return Poll::Ready(Some(Err(io::Error::other(
|
|
||||||
"HTTP/3 Content-Length mismatch",
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
Poll::Ready(trailers.map(|mut headers| {
|
|
||||||
crate::proxy::headers::strip_hop_by_hop(&mut headers);
|
|
||||||
Ok(Frame::trailers(headers))
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn is_end_stream(&self) -> bool {
|
|
||||||
self.done
|
|
||||||
}
|
|
||||||
fn size_hint(&self) -> SizeHint {
|
|
||||||
// A declared zero is not proof that the peer ended its stream. Unknown
|
|
||||||
// lengths are verified by the cache's bounded empty-body probe.
|
|
||||||
let mut hint = SizeHint::new();
|
|
||||||
if self.done {
|
|
||||||
hint.set_exact(0);
|
|
||||||
} else if let Some(remaining) = self
|
|
||||||
.expected
|
|
||||||
.map(|n| n.saturating_sub(self.received))
|
|
||||||
.filter(|n| *n > 0)
|
|
||||||
{
|
|
||||||
hint.set_upper(remaining);
|
|
||||||
}
|
|
||||||
hint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Drop for ReceiveBody {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// Dropping the request stream cancels the receive side at QUIC level.
|
|
||||||
// Explicit STOP_SENDING is used only from poll_frame paths where the
|
|
||||||
// stream handle is available and a protocol error was detected.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
include!("http3/tests.rs");
|
include!("http3/tests.rs");
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
fn invalid_headers(headers: &HeaderMap) -> bool {
|
||||||
|
[
|
||||||
|
"connection",
|
||||||
|
"keep-alive",
|
||||||
|
"proxy-connection",
|
||||||
|
"transfer-encoding",
|
||||||
|
"upgrade",
|
||||||
|
]
|
||||||
|
.iter()
|
||||||
|
.any(|name| headers.contains_key(*name))
|
||||||
|
|| headers
|
||||||
|
.get_all("te")
|
||||||
|
.iter()
|
||||||
|
.any(|value| value.as_bytes() != b"trailers")
|
||||||
|
|| headers.get_all("content-length").iter().count() > 1
|
||||||
|
|| headers.get("content-length").is_some_and(|value| {
|
||||||
|
value
|
||||||
|
.to_str()
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse::<u64>().ok())
|
||||||
|
.is_none()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
struct ReceiveBody {
|
||||||
|
stream: h3::server::RequestStream<h3::quinn::RecvStream, Bytes>,
|
||||||
|
expected: Option<u64>,
|
||||||
|
received: u64,
|
||||||
|
data_done: bool,
|
||||||
|
done: bool,
|
||||||
|
}
|
||||||
|
impl HttpBody for ReceiveBody {
|
||||||
|
type Data = Bytes;
|
||||||
|
type Error = io::Error;
|
||||||
|
fn poll_frame(
|
||||||
|
self: Pin<&mut Self>,
|
||||||
|
cx: &mut Context<'_>,
|
||||||
|
) -> Poll<Option<std::result::Result<Frame<Bytes>, io::Error>>> {
|
||||||
|
let this = self.get_mut();
|
||||||
|
if this.done {
|
||||||
|
return Poll::Ready(None);
|
||||||
|
}
|
||||||
|
if !this.data_done {
|
||||||
|
match this.stream.poll_recv_data(cx) {
|
||||||
|
Poll::Pending => return Poll::Pending,
|
||||||
|
Poll::Ready(Err(error)) => {
|
||||||
|
this.done = true;
|
||||||
|
return Poll::Ready(Some(Err(io::Error::other(error.to_string()))));
|
||||||
|
}
|
||||||
|
Poll::Ready(Ok(Some(mut data))) => {
|
||||||
|
let length = data.remaining();
|
||||||
|
this.received = this.received.saturating_add(length as u64);
|
||||||
|
if this
|
||||||
|
.expected
|
||||||
|
.is_some_and(|expected| this.received > expected)
|
||||||
|
{
|
||||||
|
drop(data);
|
||||||
|
this.done = true;
|
||||||
|
this.stream.stop_sending(Code::H3_MESSAGE_ERROR);
|
||||||
|
return Poll::Ready(Some(Err(io::Error::other(
|
||||||
|
"HTTP/3 Content-Length exceeded",
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
return Poll::Ready(Some(Ok(Frame::data(data.copy_to_bytes(length)))));
|
||||||
|
}
|
||||||
|
Poll::Ready(Ok(None)) => this.data_done = true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match this.stream.poll_recv_trailers(cx) {
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
|
Poll::Ready(Err(error)) => {
|
||||||
|
this.done = true;
|
||||||
|
Poll::Ready(Some(Err(io::Error::other(error.to_string()))))
|
||||||
|
}
|
||||||
|
Poll::Ready(Ok(trailers)) => {
|
||||||
|
this.done = true;
|
||||||
|
if this
|
||||||
|
.expected
|
||||||
|
.is_some_and(|expected| expected != this.received)
|
||||||
|
{
|
||||||
|
this.stream.stop_sending(Code::H3_MESSAGE_ERROR);
|
||||||
|
return Poll::Ready(Some(Err(io::Error::other(
|
||||||
|
"HTTP/3 Content-Length mismatch",
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
Poll::Ready(trailers.map(|mut headers| {
|
||||||
|
crate::proxy::headers::strip_hop_by_hop(&mut headers);
|
||||||
|
Ok(Frame::trailers(headers))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn is_end_stream(&self) -> bool {
|
||||||
|
self.done
|
||||||
|
}
|
||||||
|
fn size_hint(&self) -> SizeHint {
|
||||||
|
// A declared zero is not proof that the peer ended its stream. Unknown
|
||||||
|
// lengths are verified by the cache's bounded empty-body probe.
|
||||||
|
let mut hint = SizeHint::new();
|
||||||
|
if self.done {
|
||||||
|
hint.set_exact(0);
|
||||||
|
} else if let Some(remaining) = self
|
||||||
|
.expected
|
||||||
|
.map(|n| n.saturating_sub(self.received))
|
||||||
|
.filter(|n| *n > 0)
|
||||||
|
{
|
||||||
|
hint.set_upper(remaining);
|
||||||
|
}
|
||||||
|
hint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for ReceiveBody {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Dropping the request stream cancels the receive side at QUIC level.
|
||||||
|
// Explicit STOP_SENDING is used only from poll_frame paths where the
|
||||||
|
// stream handle is available and a protocol error was detected.
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
//! the background worker and is bounded by a fixed queue.
|
//! the background worker and is bounded by a fixed queue.
|
||||||
|
|
||||||
include!("logging/model.rs");
|
include!("logging/model.rs");
|
||||||
|
include!("logging/error_runtime.rs");
|
||||||
include!("logging/format.rs");
|
include!("logging/format.rs");
|
||||||
include!("logging/runtime.rs");
|
include!("logging/runtime.rs");
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
impl IoWrite for RedactingWriter {
|
||||||
|
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
||||||
|
self.buffer.extend_from_slice(bytes);
|
||||||
|
Ok(bytes.len())
|
||||||
|
}
|
||||||
|
fn flush(&mut self) -> io::Result<()> {
|
||||||
|
if self.buffer.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let text = String::from_utf8_lossy(&self.buffer);
|
||||||
|
let redacted = redact_text(&text);
|
||||||
|
let mut stderr = io::stderr().lock();
|
||||||
|
stderr.write_all(redacted.as_bytes())?;
|
||||||
|
stderr.flush()?;
|
||||||
|
self.buffer.clear();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for RedactingWriter {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for RedactingStderr {
|
||||||
|
type Writer = RedactingWriter;
|
||||||
|
fn make_writer(&'a self) -> Self::Writer {
|
||||||
|
RedactingWriter {
|
||||||
|
buffer: Vec::with_capacity(512),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn redact_regex(pattern: &'static str) -> &'static regex::Regex {
|
||||||
|
static AUTH: OnceLock<regex::Regex> = OnceLock::new();
|
||||||
|
static QUERY: OnceLock<regex::Regex> = OnceLock::new();
|
||||||
|
static JSON: OnceLock<regex::Regex> = OnceLock::new();
|
||||||
|
static KV: OnceLock<regex::Regex> = OnceLock::new();
|
||||||
|
static PEM: OnceLock<regex::Regex> = OnceLock::new();
|
||||||
|
match pattern {
|
||||||
|
"auth" => AUTH.get_or_init(|| regex::Regex::new(r"(?i)(authorization\s*[:=]\s*)(?:bearer\s+)?[^\s,;]+" ).expect("redaction regex")),
|
||||||
|
"query" => QUERY.get_or_init(|| regex::Regex::new(r"(?i)([?&](?:token|access_token|api_token|password|backup_password|secret|credential|credentials|key_secret|application_secret|consumer_key)=)[^&\s]+" ).expect("redaction regex")),
|
||||||
|
"json" => JSON.get_or_init(|| regex::Regex::new(r#"(?i)(\"(?:authorization|proxy_authorization|api_token|access_token|application_secret|consumer_key|key_secret|backup_password|password|credential|credentials|secret|token|private_key|master_key)\"\s*:\s*\")[^\"]*(\")"#).expect("redaction regex")),
|
||||||
|
"kv" => KV.get_or_init(|| regex::Regex::new(r#"(?i)((?:authorization|proxy[_-]?authorization|api[_-]?token|access[_-]?token|application[_-]?secret|consumer[_-]?key|key[_-]?secret|backup[_-]?password|password|credential|credentials|secret|token|private[_-]?key|master[_-]?key)\s*[=:]\s*)[^\s,;"']+"#).expect("redaction regex")),
|
||||||
|
"pem" => PEM.get_or_init(|| regex::Regex::new(r"(?s)-----BEGIN [^-\r\n]*PRIVATE KEY-----.*?-----END [^-\r\n]*PRIVATE KEY-----" ).expect("redaction regex")),
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_secret_field(name: &str) -> bool {
|
||||||
|
matches!(
|
||||||
|
name.to_ascii_lowercase().as_str(),
|
||||||
|
"authorization"
|
||||||
|
| "proxy_authorization"
|
||||||
|
| "api_token"
|
||||||
|
| "access_token"
|
||||||
|
| "application_secret"
|
||||||
|
| "consumer_key"
|
||||||
|
| "key_secret"
|
||||||
|
| "backup_password"
|
||||||
|
| "password"
|
||||||
|
| "credential"
|
||||||
|
| "credentials"
|
||||||
|
| "secret"
|
||||||
|
| "token"
|
||||||
|
| "private_key"
|
||||||
|
| "master_key"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn redact_text(input: &str) -> String {
|
||||||
|
let mut value = redact_regex("pem")
|
||||||
|
.replace_all(input, "[REDACTED PRIVATE KEY]")
|
||||||
|
.into_owned();
|
||||||
|
value = redact_regex("auth")
|
||||||
|
.replace_all(&value, "$1[REDACTED]")
|
||||||
|
.into_owned();
|
||||||
|
value = redact_regex("query")
|
||||||
|
.replace_all(&value, "$1[REDACTED]")
|
||||||
|
.into_owned();
|
||||||
|
value = redact_regex("json")
|
||||||
|
.replace_all(&value, "$1[REDACTED]$2")
|
||||||
|
.into_owned();
|
||||||
|
redact_regex("kv")
|
||||||
|
.replace_all(&value, "$1[REDACTED]")
|
||||||
|
.into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redact_value(value: &mut Value) {
|
||||||
|
match value {
|
||||||
|
Value::String(text) => *text = redact_text(text),
|
||||||
|
Value::Array(items) => {
|
||||||
|
for item in items {
|
||||||
|
redact_value(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Object(items) => {
|
||||||
|
for (key, item) in items {
|
||||||
|
if is_secret_field(key) {
|
||||||
|
*item = Value::String("[REDACTED]".into());
|
||||||
|
} else {
|
||||||
|
redact_value(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rfc3339_now() -> String {
|
||||||
|
let ms = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis() as i64;
|
||||||
|
let seconds = ms / 1000;
|
||||||
|
let millis = ms % 1000;
|
||||||
|
let days = seconds.div_euclid(86_400);
|
||||||
|
let day_seconds = seconds.rem_euclid(86_400);
|
||||||
|
let z = days + 719_468;
|
||||||
|
let era = if z >= 0 { z } else { z - 146_096 }.div_euclid(146_097);
|
||||||
|
let doe = z - era * 146_097;
|
||||||
|
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
|
||||||
|
let mut year = yoe + era * 400;
|
||||||
|
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
||||||
|
let mp = (5 * doy + 2) / 153;
|
||||||
|
let day = doy - (153 * mp + 2) / 5 + 1;
|
||||||
|
let month = mp + if mp < 10 { 3 } else { -9 };
|
||||||
|
if month <= 2 {
|
||||||
|
year += 1;
|
||||||
|
}
|
||||||
|
let hour = day_seconds / 3600;
|
||||||
|
let minute = (day_seconds % 3600) / 60;
|
||||||
|
let second = day_seconds % 60;
|
||||||
|
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z")
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn open_error_file(path: PathBuf) -> Result<File> {
|
||||||
|
let file = tokio::task::spawn_blocking(move || -> Result<std::fs::File> {
|
||||||
|
let mut options = std::fs::OpenOptions::new();
|
||||||
|
options.create(true).append(true).write(true);
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
|
||||||
|
}
|
||||||
|
let file = options.open(&path)?;
|
||||||
|
let metadata = file.metadata()?;
|
||||||
|
ensure!(metadata.is_file(), "error log path is not a regular file");
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
|
||||||
|
}
|
||||||
|
Ok(file)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow::anyhow!("error log open worker failed"))??;
|
||||||
|
Ok(File::from_std(file))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_error_event(event: &ErrorEvent, format: ErrorLogFormat) -> Vec<u8> {
|
||||||
|
match format {
|
||||||
|
ErrorLogFormat::Json => {
|
||||||
|
let mut object = Map::new();
|
||||||
|
object.insert("timestamp".into(), Value::String(event.timestamp.clone()));
|
||||||
|
object.insert("level".into(), Value::String("ERROR".into()));
|
||||||
|
object.insert("target".into(), Value::String(event.target.clone()));
|
||||||
|
object.insert("message".into(), Value::String(event.message.clone()));
|
||||||
|
for (key, value) in &event.fields {
|
||||||
|
if !matches!(key.as_str(), "timestamp" | "level" | "target" | "message") {
|
||||||
|
object.insert(key.clone(), Value::String(value.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut line = serde_json::to_vec(&Value::Object(object)).unwrap_or_default();
|
||||||
|
line.push(b'\n');
|
||||||
|
line
|
||||||
|
}
|
||||||
|
ErrorLogFormat::Raw => {
|
||||||
|
let one_line = |value: &str| value.replace('\r', "\\r").replace('\n', "\\n");
|
||||||
|
let mut line = format!(
|
||||||
|
"{} ERROR {} {}",
|
||||||
|
event.timestamp,
|
||||||
|
event.target,
|
||||||
|
one_line(&event.message)
|
||||||
|
);
|
||||||
|
for (key, value) in &event.fields {
|
||||||
|
line.push(' ');
|
||||||
|
line.push_str(key);
|
||||||
|
line.push('=');
|
||||||
|
line.push_str(&one_line(value));
|
||||||
|
}
|
||||||
|
line.push('\n');
|
||||||
|
line.into_bytes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn error_log_loop(
|
||||||
|
mut receiver: mpsc::Receiver<ErrorEvent>,
|
||||||
|
settings: Arc<ArcSwap<ErrorLogSettings>>,
|
||||||
|
mut settings_changed: watch::Receiver<()>,
|
||||||
|
written: Arc<AtomicU64>,
|
||||||
|
errors: Arc<AtomicU64>,
|
||||||
|
) {
|
||||||
|
let mut open_path: Option<PathBuf> = None;
|
||||||
|
let mut file: Option<File> = None;
|
||||||
|
let mut retry_after: Option<Instant> = None;
|
||||||
|
let mut last_warning: Option<Instant> = None;
|
||||||
|
loop {
|
||||||
|
let event = tokio::select! {
|
||||||
|
event = receiver.recv() => match event {
|
||||||
|
Some(event) => event,
|
||||||
|
None => break,
|
||||||
|
},
|
||||||
|
changed = settings_changed.changed() => {
|
||||||
|
if changed.is_err() && receiver.is_closed() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let config = settings.load_full();
|
||||||
|
let path = PathBuf::from(&config.path);
|
||||||
|
if !config.enabled || open_path.as_ref() != Some(&path) {
|
||||||
|
// Dropping Tokio's File closes the descriptor without blocking
|
||||||
|
// the request path. A changed path is reopened on the next event.
|
||||||
|
file = None;
|
||||||
|
open_path = None;
|
||||||
|
retry_after = None;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let config = settings.load_full();
|
||||||
|
if !config.enabled {
|
||||||
|
file = None;
|
||||||
|
open_path = None;
|
||||||
|
retry_after = None;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let path = PathBuf::from(&config.path);
|
||||||
|
if open_path.as_ref() != Some(&path) {
|
||||||
|
file = None;
|
||||||
|
open_path = Some(path.clone());
|
||||||
|
retry_after = None;
|
||||||
|
}
|
||||||
|
if file.is_none() && retry_after.is_none_or(|until| Instant::now() >= until) {
|
||||||
|
match open_error_file(path.clone()).await {
|
||||||
|
Ok(opened) => {
|
||||||
|
file = Some(opened);
|
||||||
|
retry_after = None;
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
errors.fetch_add(1, Ordering::Relaxed);
|
||||||
|
retry_after = Some(Instant::now() + ERROR_LOG_RETRY);
|
||||||
|
if last_warning.is_none_or(|time| time.elapsed() >= ERROR_LOG_RETRY) {
|
||||||
|
tracing::warn!(path=%path.display(), %error, "optional error log file is unavailable");
|
||||||
|
last_warning = Some(Instant::now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(opened) = file.as_mut() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let line = format_error_event(&event, config.format);
|
||||||
|
if let Err(error) = opened.write_all(&line).await {
|
||||||
|
errors.fetch_add(1, Ordering::Relaxed);
|
||||||
|
file = None;
|
||||||
|
retry_after = Some(Instant::now() + ERROR_LOG_RETRY);
|
||||||
|
if last_warning.is_none_or(|time| time.elapsed() >= ERROR_LOG_RETRY) {
|
||||||
|
tracing::warn!(path=%path.display(), %error, "optional error log write failed");
|
||||||
|
last_warning = Some(Instant::now());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
written.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+95
-276
@@ -1,281 +1,10 @@
|
|||||||
impl IoWrite for RedactingWriter {
|
|
||||||
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
|
|
||||||
self.buffer.extend_from_slice(bytes);
|
|
||||||
Ok(bytes.len())
|
|
||||||
}
|
|
||||||
fn flush(&mut self) -> io::Result<()> {
|
|
||||||
if self.buffer.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let text = String::from_utf8_lossy(&self.buffer);
|
|
||||||
let redacted = redact_text(&text);
|
|
||||||
let mut stderr = io::stderr().lock();
|
|
||||||
stderr.write_all(redacted.as_bytes())?;
|
|
||||||
stderr.flush()?;
|
|
||||||
self.buffer.clear();
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl Drop for RedactingWriter {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let _ = self.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for RedactingStderr {
|
|
||||||
type Writer = RedactingWriter;
|
|
||||||
fn make_writer(&'a self) -> Self::Writer {
|
|
||||||
RedactingWriter {
|
|
||||||
buffer: Vec::with_capacity(512),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fn redact_regex(pattern: &'static str) -> &'static regex::Regex {
|
|
||||||
static AUTH: OnceLock<regex::Regex> = OnceLock::new();
|
|
||||||
static QUERY: OnceLock<regex::Regex> = OnceLock::new();
|
|
||||||
static JSON: OnceLock<regex::Regex> = OnceLock::new();
|
|
||||||
static KV: OnceLock<regex::Regex> = OnceLock::new();
|
|
||||||
static PEM: OnceLock<regex::Regex> = OnceLock::new();
|
|
||||||
match pattern {
|
|
||||||
"auth" => AUTH.get_or_init(|| regex::Regex::new(r"(?i)(authorization\s*[:=]\s*)(?:bearer\s+)?[^\s,;]+" ).expect("redaction regex")),
|
|
||||||
"query" => QUERY.get_or_init(|| regex::Regex::new(r"(?i)([?&](?:token|access_token|api_token|password|backup_password|secret|credential|credentials|key_secret|application_secret|consumer_key)=)[^&\s]+" ).expect("redaction regex")),
|
|
||||||
"json" => JSON.get_or_init(|| regex::Regex::new(r#"(?i)(\"(?:authorization|proxy_authorization|api_token|access_token|application_secret|consumer_key|key_secret|backup_password|password|credential|credentials|secret|token|private_key|master_key)\"\s*:\s*\")[^\"]*(\")"#).expect("redaction regex")),
|
|
||||||
"kv" => KV.get_or_init(|| regex::Regex::new(r#"(?i)((?:authorization|proxy[_-]?authorization|api[_-]?token|access[_-]?token|application[_-]?secret|consumer[_-]?key|key[_-]?secret|backup[_-]?password|password|credential|credentials|secret|token|private[_-]?key|master[_-]?key)\s*[=:]\s*)[^\s,;"']+"#).expect("redaction regex")),
|
|
||||||
"pem" => PEM.get_or_init(|| regex::Regex::new(r"(?s)-----BEGIN [^-\r\n]*PRIVATE KEY-----.*?-----END [^-\r\n]*PRIVATE KEY-----" ).expect("redaction regex")),
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_secret_field(name: &str) -> bool {
|
|
||||||
matches!(
|
|
||||||
name.to_ascii_lowercase().as_str(),
|
|
||||||
"authorization"
|
|
||||||
| "proxy_authorization"
|
|
||||||
| "api_token"
|
|
||||||
| "access_token"
|
|
||||||
| "application_secret"
|
|
||||||
| "consumer_key"
|
|
||||||
| "key_secret"
|
|
||||||
| "backup_password"
|
|
||||||
| "password"
|
|
||||||
| "credential"
|
|
||||||
| "credentials"
|
|
||||||
| "secret"
|
|
||||||
| "token"
|
|
||||||
| "private_key"
|
|
||||||
| "master_key"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn redact_text(input: &str) -> String {
|
|
||||||
let mut value = redact_regex("pem")
|
|
||||||
.replace_all(input, "[REDACTED PRIVATE KEY]")
|
|
||||||
.into_owned();
|
|
||||||
value = redact_regex("auth")
|
|
||||||
.replace_all(&value, "$1[REDACTED]")
|
|
||||||
.into_owned();
|
|
||||||
value = redact_regex("query")
|
|
||||||
.replace_all(&value, "$1[REDACTED]")
|
|
||||||
.into_owned();
|
|
||||||
value = redact_regex("json")
|
|
||||||
.replace_all(&value, "$1[REDACTED]$2")
|
|
||||||
.into_owned();
|
|
||||||
redact_regex("kv")
|
|
||||||
.replace_all(&value, "$1[REDACTED]")
|
|
||||||
.into_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn redact_value(value: &mut Value) {
|
|
||||||
match value {
|
|
||||||
Value::String(text) => *text = redact_text(text),
|
|
||||||
Value::Array(items) => {
|
|
||||||
for item in items {
|
|
||||||
redact_value(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Value::Object(items) => {
|
|
||||||
for (key, item) in items {
|
|
||||||
if is_secret_field(key) {
|
|
||||||
*item = Value::String("[REDACTED]".into());
|
|
||||||
} else {
|
|
||||||
redact_value(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn rfc3339_now() -> String {
|
|
||||||
let ms = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_millis() as i64;
|
|
||||||
let seconds = ms / 1000;
|
|
||||||
let millis = ms % 1000;
|
|
||||||
let days = seconds.div_euclid(86_400);
|
|
||||||
let day_seconds = seconds.rem_euclid(86_400);
|
|
||||||
let z = days + 719_468;
|
|
||||||
let era = if z >= 0 { z } else { z - 146_096 }.div_euclid(146_097);
|
|
||||||
let doe = z - era * 146_097;
|
|
||||||
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
|
|
||||||
let mut year = yoe + era * 400;
|
|
||||||
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
|
|
||||||
let mp = (5 * doy + 2) / 153;
|
|
||||||
let day = doy - (153 * mp + 2) / 5 + 1;
|
|
||||||
let month = mp + if mp < 10 { 3 } else { -9 };
|
|
||||||
if month <= 2 {
|
|
||||||
year += 1;
|
|
||||||
}
|
|
||||||
let hour = day_seconds / 3600;
|
|
||||||
let minute = (day_seconds % 3600) / 60;
|
|
||||||
let second = day_seconds % 60;
|
|
||||||
format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z")
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn open_error_file(path: PathBuf) -> Result<File> {
|
|
||||||
let file = tokio::task::spawn_blocking(move || -> Result<std::fs::File> {
|
|
||||||
let mut options = std::fs::OpenOptions::new();
|
|
||||||
options.create(true).append(true).write(true);
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::OpenOptionsExt;
|
|
||||||
options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
|
|
||||||
}
|
|
||||||
let file = options.open(&path)?;
|
|
||||||
let metadata = file.metadata()?;
|
|
||||||
ensure!(metadata.is_file(), "error log path is not a regular file");
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
|
|
||||||
}
|
|
||||||
Ok(file)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(|_| anyhow::anyhow!("error log open worker failed"))??;
|
|
||||||
Ok(File::from_std(file))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_error_event(event: &ErrorEvent, format: ErrorLogFormat) -> Vec<u8> {
|
|
||||||
match format {
|
|
||||||
ErrorLogFormat::Json => {
|
|
||||||
let mut object = Map::new();
|
|
||||||
object.insert("timestamp".into(), Value::String(event.timestamp.clone()));
|
|
||||||
object.insert("level".into(), Value::String("ERROR".into()));
|
|
||||||
object.insert("target".into(), Value::String(event.target.clone()));
|
|
||||||
object.insert("message".into(), Value::String(event.message.clone()));
|
|
||||||
for (key, value) in &event.fields {
|
|
||||||
if !matches!(key.as_str(), "timestamp" | "level" | "target" | "message") {
|
|
||||||
object.insert(key.clone(), Value::String(value.clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut line = serde_json::to_vec(&Value::Object(object)).unwrap_or_default();
|
|
||||||
line.push(b'\n');
|
|
||||||
line
|
|
||||||
}
|
|
||||||
ErrorLogFormat::Raw => {
|
|
||||||
let one_line = |value: &str| value.replace('\r', "\\r").replace('\n', "\\n");
|
|
||||||
let mut line = format!(
|
|
||||||
"{} ERROR {} {}",
|
|
||||||
event.timestamp,
|
|
||||||
event.target,
|
|
||||||
one_line(&event.message)
|
|
||||||
);
|
|
||||||
for (key, value) in &event.fields {
|
|
||||||
line.push(' ');
|
|
||||||
line.push_str(key);
|
|
||||||
line.push('=');
|
|
||||||
line.push_str(&one_line(value));
|
|
||||||
}
|
|
||||||
line.push('\n');
|
|
||||||
line.into_bytes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn error_log_loop(
|
|
||||||
mut receiver: mpsc::Receiver<ErrorEvent>,
|
|
||||||
settings: Arc<ArcSwap<ErrorLogSettings>>,
|
|
||||||
mut settings_changed: watch::Receiver<()>,
|
|
||||||
written: Arc<AtomicU64>,
|
|
||||||
errors: Arc<AtomicU64>,
|
|
||||||
) {
|
|
||||||
let mut open_path: Option<PathBuf> = None;
|
|
||||||
let mut file: Option<File> = None;
|
|
||||||
let mut retry_after: Option<Instant> = None;
|
|
||||||
let mut last_warning: Option<Instant> = None;
|
|
||||||
loop {
|
|
||||||
let event = tokio::select! {
|
|
||||||
event = receiver.recv() => match event {
|
|
||||||
Some(event) => event,
|
|
||||||
None => break,
|
|
||||||
},
|
|
||||||
changed = settings_changed.changed() => {
|
|
||||||
if changed.is_err() && receiver.is_closed() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
let config = settings.load_full();
|
|
||||||
let path = PathBuf::from(&config.path);
|
|
||||||
if !config.enabled || open_path.as_ref() != Some(&path) {
|
|
||||||
// Dropping Tokio's File closes the descriptor without blocking
|
|
||||||
// the request path. A changed path is reopened on the next event.
|
|
||||||
file = None;
|
|
||||||
open_path = None;
|
|
||||||
retry_after = None;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = settings.load_full();
|
|
||||||
if !config.enabled {
|
|
||||||
file = None;
|
|
||||||
open_path = None;
|
|
||||||
retry_after = None;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let path = PathBuf::from(&config.path);
|
|
||||||
if open_path.as_ref() != Some(&path) {
|
|
||||||
file = None;
|
|
||||||
open_path = Some(path.clone());
|
|
||||||
retry_after = None;
|
|
||||||
}
|
|
||||||
if file.is_none() && retry_after.is_none_or(|until| Instant::now() >= until) {
|
|
||||||
match open_error_file(path.clone()).await {
|
|
||||||
Ok(opened) => {
|
|
||||||
file = Some(opened);
|
|
||||||
retry_after = None;
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
errors.fetch_add(1, Ordering::Relaxed);
|
|
||||||
retry_after = Some(Instant::now() + ERROR_LOG_RETRY);
|
|
||||||
if last_warning.is_none_or(|time| time.elapsed() >= ERROR_LOG_RETRY) {
|
|
||||||
tracing::warn!(path=%path.display(), %error, "optional error log file is unavailable");
|
|
||||||
last_warning = Some(Instant::now());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let Some(opened) = file.as_mut() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let line = format_error_event(&event, config.format);
|
|
||||||
if let Err(error) = opened.write_all(&line).await {
|
|
||||||
errors.fetch_add(1, Ordering::Relaxed);
|
|
||||||
file = None;
|
|
||||||
retry_after = Some(Instant::now() + ERROR_LOG_RETRY);
|
|
||||||
if last_warning.is_none_or(|time| time.elapsed() >= ERROR_LOG_RETRY) {
|
|
||||||
tracing::warn!(path=%path.display(), %error, "optional error log write failed");
|
|
||||||
last_warning = Some(Instant::now());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
written.fetch_add(1, Ordering::Relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize)]
|
#[derive(Debug, Clone, Serialize)]
|
||||||
pub struct LogEntry {
|
pub struct LogEntry {
|
||||||
|
pub event: &'static str,
|
||||||
|
pub protocol_error: Option<&'static str>,
|
||||||
|
pub error_detail: Option<String>,
|
||||||
|
pub local_addr: Option<String>,
|
||||||
|
pub tls_sni: Option<String>,
|
||||||
pub vhost: Option<String>,
|
pub vhost: Option<String>,
|
||||||
pub route: Option<String>,
|
pub route: Option<String>,
|
||||||
pub cache_effective_enabled: Option<bool>,
|
pub cache_effective_enabled: Option<bool>,
|
||||||
@@ -342,6 +71,20 @@ impl Default for Logs {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn protocol_error_detail(detail: impl std::fmt::Display) -> String {
|
||||||
|
let mut detail = redact_text(&detail.to_string());
|
||||||
|
if detail.len() > 512 {
|
||||||
|
let mut end = 512;
|
||||||
|
while !detail.is_char_boundary(end) {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
detail.truncate(end);
|
||||||
|
detail.push_str("...");
|
||||||
|
}
|
||||||
|
detail
|
||||||
|
}
|
||||||
|
|
||||||
impl Logs {
|
impl Logs {
|
||||||
pub fn start(settings: Settings, shutdown: CancellationToken) -> Arc<Self> {
|
pub fn start(settings: Settings, shutdown: CancellationToken) -> Arc<Self> {
|
||||||
let (tx, rx) = mpsc::channel(SINK_QUEUE);
|
let (tx, rx) = mpsc::channel(SINK_QUEUE);
|
||||||
@@ -382,10 +125,66 @@ impl Logs {
|
|||||||
self.dropped.fetch_add(1, Ordering::Relaxed);
|
self.dropped.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
let _ = self.live.send(entry.clone());
|
let _ = self.live.send(entry.clone());
|
||||||
|
self.push_sink(entry);
|
||||||
|
}
|
||||||
|
fn push_sink(&self, entry: LogEntry) {
|
||||||
if self.settings.load().enabled() && self.sink.try_send(entry).is_err() {
|
if self.settings.load().enabled() && self.sink.try_send(entry).is_err() {
|
||||||
self.sink_dropped.fetch_add(1, Ordering::Relaxed);
|
self.sink_dropped.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// Persist transport/protocol failures without polluting the bounded Live Logs buffer.
|
||||||
|
pub fn push_protocol_error(
|
||||||
|
&self,
|
||||||
|
kind: &'static str,
|
||||||
|
detail: impl std::fmt::Display,
|
||||||
|
client: std::net::SocketAddr,
|
||||||
|
local: std::net::SocketAddr,
|
||||||
|
transport: &'static str,
|
||||||
|
http_version: &'static str,
|
||||||
|
status: u16,
|
||||||
|
tls_sni: Option<String>,
|
||||||
|
) {
|
||||||
|
if !self.settings.load().enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let detail = protocol_error_detail(detail);
|
||||||
|
self.push_sink(LogEntry {
|
||||||
|
event: "protocol_error",
|
||||||
|
protocol_error: Some(kind),
|
||||||
|
error_detail: Some(detail),
|
||||||
|
local_addr: Some(local.to_string()),
|
||||||
|
tls_sni,
|
||||||
|
vhost: None,
|
||||||
|
route: None,
|
||||||
|
cache_effective_enabled: None,
|
||||||
|
transform_effective_enabled: None,
|
||||||
|
backend: None,
|
||||||
|
id: 0,
|
||||||
|
request_id: String::new(),
|
||||||
|
time_ms: now_ms(),
|
||||||
|
method: String::new(),
|
||||||
|
host: String::new(),
|
||||||
|
path: String::new(),
|
||||||
|
http_version,
|
||||||
|
transport,
|
||||||
|
websocket_bootstrap: None,
|
||||||
|
websocket_phase: None,
|
||||||
|
websocket_error: None,
|
||||||
|
h3_reset_code: None,
|
||||||
|
upstream_status: None,
|
||||||
|
upstream_error: None,
|
||||||
|
client_ip: Some(client.ip().to_string()),
|
||||||
|
user_agent: None,
|
||||||
|
country: None,
|
||||||
|
status,
|
||||||
|
duration_ms: 0,
|
||||||
|
request_bytes: 0,
|
||||||
|
response_bytes: 0,
|
||||||
|
outcome: "protocol_error",
|
||||||
|
cache: "-",
|
||||||
|
cache_reason: "protocol_error",
|
||||||
|
});
|
||||||
|
}
|
||||||
pub fn subscribe(&self) -> broadcast::Receiver<LogEntry> {
|
pub fn subscribe(&self) -> broadcast::Receiver<LogEntry> {
|
||||||
self.live.subscribe()
|
self.live.subscribe()
|
||||||
}
|
}
|
||||||
@@ -417,6 +216,26 @@ impl Logs {
|
|||||||
|
|
||||||
fn value_for(entry: &LogEntry, name: &str) -> Value {
|
fn value_for(entry: &LogEntry, name: &str) -> Value {
|
||||||
match name {
|
match name {
|
||||||
|
"event" => json!(entry.event),
|
||||||
|
"protocol_error" => entry
|
||||||
|
.protocol_error
|
||||||
|
.map(|v| Value::String(v.into()))
|
||||||
|
.unwrap_or(Value::Null),
|
||||||
|
"error_detail" => entry
|
||||||
|
.error_detail
|
||||||
|
.clone()
|
||||||
|
.map(Value::String)
|
||||||
|
.unwrap_or(Value::Null),
|
||||||
|
"local_addr" => entry
|
||||||
|
.local_addr
|
||||||
|
.clone()
|
||||||
|
.map(Value::String)
|
||||||
|
.unwrap_or(Value::Null),
|
||||||
|
"tls_sni" => entry
|
||||||
|
.tls_sni
|
||||||
|
.clone()
|
||||||
|
.map(Value::String)
|
||||||
|
.unwrap_or(Value::Null),
|
||||||
"vhost" => entry
|
"vhost" => entry
|
||||||
.vhost
|
.vhost
|
||||||
.clone()
|
.clone()
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ use std::{
|
|||||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
fs::{File, OpenOptions},
|
fs::File,
|
||||||
io::AsyncWriteExt,
|
io::AsyncWriteExt,
|
||||||
net::TcpStream,
|
net::TcpStream,
|
||||||
sync::{broadcast, mpsc, watch},
|
sync::{broadcast, mpsc, watch},
|
||||||
@@ -229,7 +229,12 @@ fn validate_json_key(key: &str) -> Result<()> {
|
|||||||
fn source_value_name(name: &str) -> bool {
|
fn source_value_name(name: &str) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
name,
|
name,
|
||||||
"vhost"
|
"event"
|
||||||
|
| "protocol_error"
|
||||||
|
| "error_detail"
|
||||||
|
| "local_addr"
|
||||||
|
| "tls_sni"
|
||||||
|
| "vhost"
|
||||||
| "route"
|
| "route"
|
||||||
| "cache_effective_enabled"
|
| "cache_effective_enabled"
|
||||||
| "transform_effective_enabled"
|
| "transform_effective_enabled"
|
||||||
|
|||||||
+43
-12
@@ -15,6 +15,23 @@ fn format_entry(entry: &LogEntry, format: &JsonFormat) -> Value {
|
|||||||
for (key, value) in &format.static_fields {
|
for (key, value) in &format.static_fields {
|
||||||
object.insert(key.clone(), Value::String(value.clone()));
|
object.insert(key.clone(), Value::String(value.clone()));
|
||||||
}
|
}
|
||||||
|
if entry.event == "protocol_error" {
|
||||||
|
for name in [
|
||||||
|
"event",
|
||||||
|
"protocol_error",
|
||||||
|
"error_detail",
|
||||||
|
"time_ms",
|
||||||
|
"client_ip",
|
||||||
|
"local_addr",
|
||||||
|
"tls_sni",
|
||||||
|
"transport",
|
||||||
|
"http_version",
|
||||||
|
"status",
|
||||||
|
"outcome",
|
||||||
|
] {
|
||||||
|
object.insert(name.into(), value_for(entry, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
Value::Object(object)
|
Value::Object(object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +86,30 @@ struct LogstashState {
|
|||||||
address: String,
|
address: String,
|
||||||
stream: TcpStream,
|
stream: TcpStream,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn open_access_file(path: PathBuf) -> Result<File> {
|
||||||
|
let file = tokio::task::spawn_blocking(move || -> Result<std::fs::File> {
|
||||||
|
let mut options = std::fs::OpenOptions::new();
|
||||||
|
options.create(true).append(true).write(true);
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::OpenOptionsExt;
|
||||||
|
options.mode(0o600).custom_flags(libc::O_NOFOLLOW);
|
||||||
|
}
|
||||||
|
let file = options.open(&path)?;
|
||||||
|
let metadata = file.metadata()?;
|
||||||
|
ensure!(metadata.is_file(), "access log path is not a regular file");
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
file.set_permissions(std::fs::Permissions::from_mode(0o600))?;
|
||||||
|
}
|
||||||
|
Ok(file)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| anyhow::anyhow!("access log open worker failed"))??;
|
||||||
|
Ok(File::from_std(file))
|
||||||
|
}
|
||||||
async fn sink_loop(
|
async fn sink_loop(
|
||||||
mut rx: mpsc::Receiver<LogEntry>,
|
mut rx: mpsc::Receiver<LogEntry>,
|
||||||
settings: Arc<ArcSwap<Settings>>,
|
settings: Arc<ArcSwap<Settings>>,
|
||||||
@@ -113,12 +154,7 @@ async fn sink_loop(
|
|||||||
vhost_files.remove(&path);
|
vhost_files.remove(&path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match OpenOptions::new()
|
match open_access_file(path.clone()).await {
|
||||||
.create(true)
|
|
||||||
.append(true)
|
|
||||||
.open(&path)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(opened) => {
|
Ok(opened) => {
|
||||||
vhost_files.insert(path.clone(), opened);
|
vhost_files.insert(path.clone(), opened);
|
||||||
}
|
}
|
||||||
@@ -142,12 +178,7 @@ async fn sink_loop(
|
|||||||
vhost_directory = None;
|
vhost_directory = None;
|
||||||
let path = PathBuf::from(&config.file.path);
|
let path = PathBuf::from(&config.file.path);
|
||||||
if file.as_ref().is_none_or(|state| state.path != path) {
|
if file.as_ref().is_none_or(|state| state.path != path) {
|
||||||
file = match OpenOptions::new()
|
file = match open_access_file(path.clone()).await {
|
||||||
.create(true)
|
|
||||||
.append(true)
|
|
||||||
.open(&path)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(opened) => Some(FileState {
|
Ok(opened) => Some(FileState {
|
||||||
path: path.clone(),
|
path: path.clone(),
|
||||||
file: opened,
|
file: opened,
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ mod tests {
|
|||||||
|
|
||||||
fn entry() -> LogEntry {
|
fn entry() -> LogEntry {
|
||||||
LogEntry {
|
LogEntry {
|
||||||
|
event: "request",
|
||||||
|
protocol_error: None,
|
||||||
|
error_detail: None,
|
||||||
|
local_addr: Some("192.0.2.1:443".into()),
|
||||||
|
tls_sni: Some("app.example".into()),
|
||||||
vhost: Some("app".into()),
|
vhost: Some("app".into()),
|
||||||
route: Some("root".into()),
|
route: Some("root".into()),
|
||||||
cache_effective_enabled: Some(true),
|
cache_effective_enabled: Some(true),
|
||||||
@@ -108,11 +113,63 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn default_json_format_keeps_standard_entry() {
|
fn default_json_format_keeps_standard_entry() {
|
||||||
let value = format_entry(&entry(), &JsonFormat::default());
|
let value = format_entry(&entry(), &JsonFormat::default());
|
||||||
|
assert_eq!(value["event"], "request");
|
||||||
assert_eq!(value["method"], "GET");
|
assert_eq!(value["method"], "GET");
|
||||||
assert_eq!(value["cache"], "HIT");
|
assert_eq!(value["cache"], "HIT");
|
||||||
assert_eq!(value["response_bytes"], 42);
|
assert_eq!(value["response_bytes"], 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn protocol_error_keeps_required_diagnostic_fields_with_custom_mapping() {
|
||||||
|
let mut item = entry();
|
||||||
|
item.event = "protocol_error";
|
||||||
|
item.protocol_error = Some("tls_handshake_error");
|
||||||
|
item.error_detail = Some("invalid handshake".into());
|
||||||
|
item.outcome = "protocol_error";
|
||||||
|
item.status = 0;
|
||||||
|
item.transport = "tls";
|
||||||
|
let mut format = JsonFormat::default();
|
||||||
|
format.fields.insert("site".into(), "vhost".into());
|
||||||
|
format.static_fields.insert("event".into(), "custom".into());
|
||||||
|
let value = format_entry(&item, &format);
|
||||||
|
assert_eq!(value["site"], "app");
|
||||||
|
assert_eq!(value["event"], "protocol_error");
|
||||||
|
assert_eq!(value["protocol_error"], "tls_handshake_error");
|
||||||
|
assert_eq!(value["error_detail"], "invalid handshake");
|
||||||
|
assert_eq!(value["client_ip"], "192.0.2.10");
|
||||||
|
assert_eq!(value["transport"], "tls");
|
||||||
|
assert_eq!(value["outcome"], "protocol_error");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn protocol_errors_skip_live_history_and_use_persistent_sink_only() {
|
||||||
|
let logs = Logs::default();
|
||||||
|
let mut settings = Settings::default();
|
||||||
|
settings.file.enabled = true;
|
||||||
|
logs.configure(settings);
|
||||||
|
logs.push_protocol_error(
|
||||||
|
"tls_handshake_error",
|
||||||
|
"invalid token=topsecret",
|
||||||
|
"192.0.2.10:50000".parse().unwrap(),
|
||||||
|
"192.0.2.20:443".parse().unwrap(),
|
||||||
|
"tls",
|
||||||
|
"",
|
||||||
|
0,
|
||||||
|
Some("app.example".into()),
|
||||||
|
);
|
||||||
|
assert!(logs.recent().is_empty());
|
||||||
|
assert_eq!(logs.sink_dropped.load(Ordering::Relaxed), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn protocol_error_detail_is_utf8_safe_bounded_and_redacted() {
|
||||||
|
let detail = protocol_error_detail(format!("token=topsecret {}", "€".repeat(300)));
|
||||||
|
assert!(!detail.contains("topsecret"));
|
||||||
|
assert!(detail.is_char_boundary(detail.len()));
|
||||||
|
assert!(detail.len() <= 515);
|
||||||
|
assert!(detail.ends_with("..."));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn error_log_defaults_off_and_redacts_common_secret_shapes() {
|
fn error_log_defaults_off_and_redacts_common_secret_shapes() {
|
||||||
let settings = ErrorLogSettings::default();
|
let settings = ErrorLogSettings::default();
|
||||||
|
|||||||
@@ -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.25.7"
|
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.26.0"
|
||||||
)]
|
)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[arg(
|
#[arg(
|
||||||
|
|||||||
+1
-1
@@ -239,7 +239,7 @@ async fn live_logs_command(dir: PathBuf, addr: SocketAddr) -> Result<()> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
|
let interactive = io::stdin().is_terminal() && io::stdout().is_terminal();
|
||||||
println!("RustProxy live logs 0.25.7 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
println!("RustProxy live logs 0.26.0 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
||||||
if interactive { print!("> "); io::stdout().flush()?; }
|
if interactive { print!("> "); io::stdout().flush()?; }
|
||||||
let mut filters = LiveLogFilters::default();
|
let mut filters = LiveLogFilters::default();
|
||||||
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
|
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
|
||||||
|
|||||||
@@ -202,6 +202,11 @@ impl Drop for Exchange {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
self.logs.push(LogEntry {
|
self.logs.push(LogEntry {
|
||||||
|
event: "request",
|
||||||
|
protocol_error: None,
|
||||||
|
error_detail: None,
|
||||||
|
local_addr: None,
|
||||||
|
tls_sni: None,
|
||||||
cache_reason: *self.cache_reason.lock().unwrap_or_else(|e| e.into_inner()),
|
cache_reason: *self.cache_reason.lock().unwrap_or_else(|e| e.into_inner()),
|
||||||
vhost: self.vhost.clone(),
|
vhost: self.vhost.clone(),
|
||||||
route,
|
route,
|
||||||
|
|||||||
@@ -87,6 +87,31 @@ pub struct LocalAddr(pub SocketAddr);
|
|||||||
pub struct Transport {
|
pub struct Transport {
|
||||||
pub tls: bool,
|
pub tls: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn log_protocol_error(
|
||||||
|
state: Option<&Arc<App>>,
|
||||||
|
kind: &'static str,
|
||||||
|
detail: impl std::fmt::Display,
|
||||||
|
peer: SocketAddr,
|
||||||
|
local: SocketAddr,
|
||||||
|
transport: &'static str,
|
||||||
|
http_version: &'static str,
|
||||||
|
status: u16,
|
||||||
|
tls_sni: Option<String>,
|
||||||
|
) {
|
||||||
|
if let Some(state) = state {
|
||||||
|
state.logs.push_protocol_error(
|
||||||
|
kind,
|
||||||
|
detail,
|
||||||
|
peer,
|
||||||
|
local,
|
||||||
|
transport,
|
||||||
|
http_version,
|
||||||
|
status,
|
||||||
|
tls_sni,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ServerShutdown(pub CancellationToken);
|
pub struct ServerShutdown(pub CancellationToken);
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ async fn http2_connection<I>(
|
|||||||
local: SocketAddr,
|
local: SocketAddr,
|
||||||
transport: Transport,
|
transport: Transport,
|
||||||
extended_connect: bool,
|
extended_connect: bool,
|
||||||
|
frontend_state: Option<Arc<App>>,
|
||||||
lease: Arc<ConnectionLease>,
|
lease: Arc<ConnectionLease>,
|
||||||
stop: CancellationToken,
|
stop: CancellationToken,
|
||||||
grace: Duration,
|
grace: Duration,
|
||||||
@@ -66,6 +67,10 @@ async fn http2_connection<I>(
|
|||||||
tokio::select! {
|
tokio::select! {
|
||||||
result = &mut conn => {
|
result = &mut conn => {
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
|
log_protocol_error(
|
||||||
|
frontend_state.as_ref(), "http2_connection_error", &error, peer, local,
|
||||||
|
if transport.tls { "tls" } else { "tcp" }, "2", 0, None,
|
||||||
|
);
|
||||||
tracing::debug!(%error, "HTTP/2 connection closed");
|
tracing::debug!(%error, "HTTP/2 connection closed");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -94,6 +99,17 @@ async fn http2_connection<I>(
|
|||||||
match tokio::time::timeout(drain, &mut conn).await {
|
match tokio::time::timeout(drain, &mut conn).await {
|
||||||
Ok(Ok(())) => tracing::debug!(?cause, "HTTP/2 graceful shutdown drained"),
|
Ok(Ok(())) => tracing::debug!(?cause, "HTTP/2 graceful shutdown drained"),
|
||||||
Ok(Err(error)) => {
|
Ok(Err(error)) => {
|
||||||
|
log_protocol_error(
|
||||||
|
frontend_state.as_ref(),
|
||||||
|
"http2_drain_protocol_error",
|
||||||
|
&error,
|
||||||
|
peer,
|
||||||
|
local,
|
||||||
|
if transport.tls { "tls" } else { "tcp" },
|
||||||
|
"2",
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
);
|
||||||
tracing::debug!(?cause, %error, "HTTP/2 graceful shutdown ended with protocol error")
|
tracing::debug!(?cause, %error, "HTTP/2 graceful shutdown ended with protocol error")
|
||||||
}
|
}
|
||||||
Err(_) => tracing::debug!(
|
Err(_) => tracing::debug!(
|
||||||
|
|||||||
+108
-15
@@ -23,6 +23,18 @@ async fn serve_inner(
|
|||||||
let local = stream.local_addr().context("accepted local address unavailable")?;
|
let local = stream.local_addr().context("accepted local address unavailable")?;
|
||||||
let Ok(permit) = permits.clone().try_acquire_owned() else {
|
let Ok(permit) = permits.clone().try_acquire_owned() else {
|
||||||
if let Some(m) = &metrics { m.rejected_connections.fetch_add(1, Ordering::Relaxed); }
|
if let Some(m) = &metrics { m.rejected_connections.fetch_add(1, Ordering::Relaxed); }
|
||||||
|
let log_state = match &tls {
|
||||||
|
Some(TlsMode::Frontend(state, _)) => Some(state),
|
||||||
|
_ => frontend_state.as_ref(),
|
||||||
|
};
|
||||||
|
log_protocol_error(
|
||||||
|
log_state,
|
||||||
|
"connection_limit_rejected",
|
||||||
|
"connection concurrency limit reached",
|
||||||
|
peer, local,
|
||||||
|
if tls.is_some() { "tls" } else { "tcp" },
|
||||||
|
"", 0, None,
|
||||||
|
);
|
||||||
drop(stream); continue;
|
drop(stream); continue;
|
||||||
};
|
};
|
||||||
let _ = stream.set_nodelay(true);
|
let _ = stream.set_nodelay(true);
|
||||||
@@ -46,16 +58,31 @@ async fn serve_inner(
|
|||||||
_ = stop.cancelled() => return,
|
_ = stop.cancelled() => return,
|
||||||
started = tokio::time::timeout_at(deadline, acceptor) => started,
|
started = tokio::time::timeout_at(deadline, acceptor) => started,
|
||||||
};
|
};
|
||||||
let Ok(Ok(start)) = started else {
|
let start = match started {
|
||||||
state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed);
|
Ok(Ok(start)) => start,
|
||||||
return;
|
Ok(Err(error)) => {
|
||||||
|
state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed);
|
||||||
|
log_protocol_error(
|
||||||
|
Some(&state), "tls_client_hello_error", error, peer, local,
|
||||||
|
"tls", "", 0, None,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed);
|
||||||
|
log_protocol_error(
|
||||||
|
Some(&state), "tls_client_hello_timeout",
|
||||||
|
"TLS ClientHello timeout", peer, local, "tls", "", 0, None,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
let host = start
|
||||||
|
.client_hello()
|
||||||
|
.server_name()
|
||||||
|
.and_then(|name| crate::config::request_host(name).ok());
|
||||||
let config = {
|
let config = {
|
||||||
let snapshot = state.config.load();
|
let snapshot = state.config.load();
|
||||||
let host = start
|
|
||||||
.client_hello()
|
|
||||||
.server_name()
|
|
||||||
.and_then(|name| crate::config::request_host(name).ok());
|
|
||||||
let frontend = host
|
let frontend = host
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.and_then(|host| snapshot.frontend(host, local, true));
|
.and_then(|host| snapshot.frontend(host, local, true));
|
||||||
@@ -74,7 +101,14 @@ async fn serve_inner(
|
|||||||
snapshot.tls_http1.clone()
|
snapshot.tls_http1.clone()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let Some(config) = config else { return; };
|
let Some(config) = config else {
|
||||||
|
log_protocol_error(
|
||||||
|
Some(&state), "tls_configuration_unavailable",
|
||||||
|
"no TLS configuration is available for this connection",
|
||||||
|
peer, local, "tls", "", 0, host.clone(),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
let accepted = tokio::select! {
|
let accepted = tokio::select! {
|
||||||
_ = stop.cancelled() => return,
|
_ = stop.cancelled() => return,
|
||||||
accepted = tokio::time::timeout_at(deadline, start.into_stream(config)) => accepted,
|
accepted = tokio::time::timeout_at(deadline, start.into_stream(config)) => accepted,
|
||||||
@@ -94,7 +128,7 @@ async fn serve_inner(
|
|||||||
h2,
|
h2,
|
||||||
extended_connect,
|
extended_connect,
|
||||||
false,
|
false,
|
||||||
None,
|
Some(state.clone()),
|
||||||
lease,
|
lease,
|
||||||
stop,
|
stop,
|
||||||
grace,
|
grace,
|
||||||
@@ -102,7 +136,20 @@ async fn serve_inner(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
},
|
},
|
||||||
_ => { state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed); },
|
Ok(Err(error)) => {
|
||||||
|
state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed);
|
||||||
|
log_protocol_error(
|
||||||
|
Some(&state), "tls_handshake_error", error, peer, local,
|
||||||
|
"tls", "", 0, host.clone(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
Err(_) => {
|
||||||
|
state.metrics.tls_handshake_errors.fetch_add(1, Ordering::Relaxed);
|
||||||
|
log_protocol_error(
|
||||||
|
Some(&state), "tls_handshake_timeout", "TLS handshake timeout",
|
||||||
|
peer, local, "tls", "", 0, host.clone(),
|
||||||
|
);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(TlsMode::Fixed(config, plaintext_router)) => {
|
Some(TlsMode::Fixed(config, plaintext_router)) => {
|
||||||
@@ -232,7 +279,7 @@ async fn connection<I>(
|
|||||||
h2: bool,
|
h2: bool,
|
||||||
extended_connect: bool,
|
extended_connect: bool,
|
||||||
h2c_upgrade: bool,
|
h2c_upgrade: bool,
|
||||||
h2c_state: Option<Arc<App>>,
|
frontend_state: Option<Arc<App>>,
|
||||||
lease: Arc<ConnectionLease>,
|
lease: Arc<ConnectionLease>,
|
||||||
stop: CancellationToken,
|
stop: CancellationToken,
|
||||||
grace: Duration,
|
grace: Duration,
|
||||||
@@ -248,6 +295,7 @@ async fn connection<I>(
|
|||||||
local,
|
local,
|
||||||
transport,
|
transport,
|
||||||
extended_connect,
|
extended_connect,
|
||||||
|
frontend_state.clone(),
|
||||||
lease,
|
lease,
|
||||||
stop,
|
stop,
|
||||||
grace,
|
grace,
|
||||||
@@ -257,11 +305,12 @@ async fn connection<I>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let request_stop = stop.clone();
|
let request_stop = stop.clone();
|
||||||
|
let service_frontend_state = frontend_state.clone();
|
||||||
let service = service_fn(move |mut request: Request<Incoming>| {
|
let service = service_fn(move |mut request: Request<Incoming>| {
|
||||||
let router = router.clone();
|
let router = router.clone();
|
||||||
let request_stop = request_stop.clone();
|
let request_stop = request_stop.clone();
|
||||||
let request_lease = lease.clone();
|
let request_lease = lease.clone();
|
||||||
let upgrade_state = h2c_state.clone();
|
let upgrade_state = service_frontend_state.clone();
|
||||||
async move {
|
async move {
|
||||||
request.extensions_mut().insert(PeerAddr(peer));
|
request.extensions_mut().insert(PeerAddr(peer));
|
||||||
request.extensions_mut().insert(LocalAddr(local));
|
request.extensions_mut().insert(LocalAddr(local));
|
||||||
@@ -273,6 +322,17 @@ async fn connection<I>(
|
|||||||
|
|
||||||
if h2c_attempt(&request) {
|
if h2c_attempt(&request) {
|
||||||
if !h2c_upgrade {
|
if !h2c_upgrade {
|
||||||
|
log_protocol_error(
|
||||||
|
upgrade_state.as_ref(),
|
||||||
|
"h2c_upgrade_rejected",
|
||||||
|
"h2c upgrade is disabled on this listener",
|
||||||
|
peer,
|
||||||
|
local,
|
||||||
|
"tcp",
|
||||||
|
"1.1",
|
||||||
|
400,
|
||||||
|
None,
|
||||||
|
);
|
||||||
return Ok::<_, Infallible>(
|
return Ok::<_, Infallible>(
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(http::StatusCode::BAD_REQUEST)
|
.status(http::StatusCode::BAD_REQUEST)
|
||||||
@@ -284,13 +344,24 @@ async fn connection<I>(
|
|||||||
let injected_headers = match h2_headers_frame(&request) {
|
let injected_headers = match h2_headers_frame(&request) {
|
||||||
Ok(frame) => frame,
|
Ok(frame) => frame,
|
||||||
Err(message) => {
|
Err(message) => {
|
||||||
|
log_protocol_error(
|
||||||
|
upgrade_state.as_ref(),
|
||||||
|
"h2c_upgrade_invalid",
|
||||||
|
message,
|
||||||
|
peer,
|
||||||
|
local,
|
||||||
|
"tcp",
|
||||||
|
"1.1",
|
||||||
|
400,
|
||||||
|
None,
|
||||||
|
);
|
||||||
return Ok::<_, Infallible>(
|
return Ok::<_, Infallible>(
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(http::StatusCode::BAD_REQUEST)
|
.status(http::StatusCode::BAD_REQUEST)
|
||||||
.header(http::header::CACHE_CONTROL, "no-store")
|
.header(http::header::CACHE_CONTROL, "no-store")
|
||||||
.body(Body::from(message))
|
.body(Body::from(message))
|
||||||
.expect("static h2c validation response"),
|
.expect("static h2c validation response"),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let on_upgrade = hyper::upgrade::on(&mut request);
|
let on_upgrade = hyper::upgrade::on(&mut request);
|
||||||
@@ -319,6 +390,7 @@ async fn connection<I>(
|
|||||||
local,
|
local,
|
||||||
Transport { tls: false },
|
Transport { tls: false },
|
||||||
extended_connect,
|
extended_connect,
|
||||||
|
upgrade_state_for_task.clone(),
|
||||||
upgrade_lease,
|
upgrade_lease,
|
||||||
upgrade_stop,
|
upgrade_stop,
|
||||||
grace,
|
grace,
|
||||||
@@ -326,7 +398,20 @@ async fn connection<I>(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
Err(error) => tracing::debug!(%error, "h2c upgrade transport failed"),
|
Err(error) => {
|
||||||
|
log_protocol_error(
|
||||||
|
upgrade_state_for_task.as_ref(),
|
||||||
|
"h2c_transport_error",
|
||||||
|
&error,
|
||||||
|
peer,
|
||||||
|
local,
|
||||||
|
"tcp",
|
||||||
|
"2",
|
||||||
|
0,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
tracing::debug!(%error, "h2c upgrade transport failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(state) = upgrade_state {
|
if let Some(state) = upgrade_state {
|
||||||
@@ -359,7 +444,15 @@ async fn connection<I>(
|
|||||||
.with_upgrades();
|
.with_upgrades();
|
||||||
tokio::pin!(conn);
|
tokio::pin!(conn);
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
result = &mut conn => { if let Err(error) = result { tracing::debug!(%error, "HTTP/1 connection closed"); } },
|
result = &mut conn => {
|
||||||
|
if let Err(error) = result {
|
||||||
|
log_protocol_error(
|
||||||
|
frontend_state.as_ref(), "http1_connection_error", &error, peer, local,
|
||||||
|
if transport.tls { "tls" } else { "tcp" }, "1.1", 400, None,
|
||||||
|
);
|
||||||
|
tracing::debug!(%error, "HTTP/1 connection closed");
|
||||||
|
}
|
||||||
|
},
|
||||||
_ = stop.cancelled() => {
|
_ = stop.cancelled() => {
|
||||||
conn.as_mut().graceful_shutdown(); let _ = tokio::time::timeout(grace, &mut conn).await;
|
conn.as_mut().graceful_shutdown(); let _ = tokio::time::timeout(grace, &mut conn).await;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ use super::{
|
|||||||
config::{CompiledPhp, FastCgiEndpoint, MAX_HEADER_VALUE_BYTES},
|
config::{CompiledPhp, FastCgiEndpoint, MAX_HEADER_VALUE_BYTES},
|
||||||
filesystem::path_components,
|
filesystem::path_components,
|
||||||
htaccess,
|
htaccess,
|
||||||
response::{builtin_error_response, status_response},
|
|
||||||
request::server_protocol,
|
request::server_protocol,
|
||||||
|
response::{builtin_error_response, status_response},
|
||||||
CompiledService, RequestContext,
|
CompiledService, RequestContext,
|
||||||
};
|
};
|
||||||
use anyhow::{Context, ensure, Result};
|
use anyhow::{ensure, Context, Result};
|
||||||
use axum::{body::Body, response::Response};
|
use axum::{body::Body, response::Response};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::stream;
|
use futures_util::stream;
|
||||||
@@ -38,9 +38,7 @@ impl<T> FastCgiIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
|
|||||||
async fn connect_fastcgi(endpoint: &FastCgiEndpoint) -> io::Result<Box<dyn FastCgiIo>> {
|
async fn connect_fastcgi(endpoint: &FastCgiEndpoint) -> io::Result<Box<dyn FastCgiIo>> {
|
||||||
match endpoint {
|
match endpoint {
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
FastCgiEndpoint::Unix(path) => {
|
FastCgiEndpoint::Unix(path) => Ok(Box::new(tokio::net::UnixStream::connect(path).await?)),
|
||||||
Ok(Box::new(tokio::net::UnixStream::connect(path).await?))
|
|
||||||
}
|
|
||||||
FastCgiEndpoint::Tcp(address) => {
|
FastCgiEndpoint::Tcp(address) => {
|
||||||
Ok(Box::new(tokio::net::TcpStream::connect(address).await?))
|
Ok(Box::new(tokio::net::TcpStream::connect(address).await?))
|
||||||
}
|
}
|
||||||
@@ -95,7 +93,6 @@ fn encode_fcgi_param(target: &mut Vec<u8>, name: &str, value: &str) -> io::Resul
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fn fastcgi_header_param(name: &HeaderName) -> Option<String> {
|
fn fastcgi_header_param(name: &HeaderName) -> Option<String> {
|
||||||
let lower = name.as_str();
|
let lower = name.as_str();
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -147,32 +144,45 @@ pub(super) fn fastcgi_params(
|
|||||||
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);
|
||||||
params.insert("SERVER_PROTOCOL".into(), server_protocol(context.version).into());
|
params.insert(
|
||||||
|
"SERVER_PROTOCOL".into(),
|
||||||
|
server_protocol(context.version).into(),
|
||||||
|
);
|
||||||
params.insert("SERVER_NAME".into(), context.server_name.clone());
|
params.insert("SERVER_NAME".into(), context.server_name.clone());
|
||||||
params.insert("SERVER_PORT".into(), context.server_port.to_string());
|
params.insert("SERVER_PORT".into(), context.server_port.to_string());
|
||||||
params.insert("REMOTE_ADDR".into(), context.client_ip.to_string());
|
params.insert("REMOTE_ADDR".into(), context.client_ip.to_string());
|
||||||
if let Some(port) = context.client_port {
|
if let Some(port) = context.client_port {
|
||||||
params.insert("REMOTE_PORT".into(), port.to_string());
|
params.insert("REMOTE_PORT".into(), port.to_string());
|
||||||
}
|
}
|
||||||
params.insert("HTTPS".into(), if context.tls { "on" } else { "off" }.into());
|
params.insert(
|
||||||
|
"HTTPS".into(),
|
||||||
|
if context.tls { "on" } else { "off" }.into(),
|
||||||
|
);
|
||||||
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
|
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
|
||||||
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.25.7".into());
|
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.26.0".into());
|
||||||
params.insert("REDIRECT_STATUS".into(), "200".into());
|
params.insert("REDIRECT_STATUS".into(), "200".into());
|
||||||
if let Some(value) = headers.get(CONTENT_TYPE).and_then(|value| value.to_str().ok()) {
|
if let Some(value) = headers
|
||||||
|
.get(CONTENT_TYPE)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
{
|
||||||
params.insert("CONTENT_TYPE".into(), value.into());
|
params.insert("CONTENT_TYPE".into(), value.into());
|
||||||
}
|
}
|
||||||
if let Some(value) = headers.get(CONTENT_LENGTH).and_then(|value| value.to_str().ok()) {
|
if let Some(value) = headers
|
||||||
|
.get(CONTENT_LENGTH)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
{
|
||||||
params.insert("CONTENT_LENGTH".into(), value.into());
|
params.insert("CONTENT_LENGTH".into(), value.into());
|
||||||
}
|
}
|
||||||
for (name, value) in headers {
|
for (name, value) in headers {
|
||||||
// HTTP_PROXY is a well-known CGI/FastCGI injection vector (httpoxy).
|
// HTTP_PROXY is a well-known CGI/FastCGI injection vector (httpoxy).
|
||||||
// This must never depend on the optional Web Service hardening switch.
|
// This must never depend on the optional Web Service hardening switch.
|
||||||
if name.as_str() == "proxy"
|
if name.as_str() == "proxy" || (!php.pass_authorization && name.as_str() == "authorization")
|
||||||
|| (!php.pass_authorization && name.as_str() == "authorization")
|
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(param) = fastcgi_header_param(name) else { continue };
|
let Some(param) = fastcgi_header_param(name) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
let Ok(value) = value.to_str() else { continue };
|
let Ok(value) = value.to_str() else { continue };
|
||||||
if value.len() > MAX_HEADER_VALUE_BYTES {
|
if value.len() > MAX_HEADER_VALUE_BYTES {
|
||||||
continue;
|
continue;
|
||||||
@@ -224,7 +234,8 @@ pub(super) async fn read_fcgi_record(
|
|||||||
tokio::time::timeout_at(deadline, io.read_exact(&mut header))
|
tokio::time::timeout_at(deadline, io.read_exact(&mut header))
|
||||||
.await
|
.await
|
||||||
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "FastCGI response timeout"))??;
|
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "FastCGI response timeout"))??;
|
||||||
if header[0] != FCGI_VERSION_1 || u16::from_be_bytes([header[2], header[3]]) != FCGI_REQUEST_ID {
|
if header[0] != FCGI_VERSION_1 || u16::from_be_bytes([header[2], header[3]]) != FCGI_REQUEST_ID
|
||||||
|
{
|
||||||
return Err(io::Error::new(
|
return Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidData,
|
io::ErrorKind::InvalidData,
|
||||||
"invalid FastCGI record header",
|
"invalid FastCGI record header",
|
||||||
@@ -325,7 +336,10 @@ async fn drain_fastcgi_body(
|
|||||||
loop {
|
loop {
|
||||||
match read_fcgi_record(io.as_mut(), deadline).await {
|
match read_fcgi_record(io.as_mut(), deadline).await {
|
||||||
Ok((FCGI_STDOUT, content)) => {
|
Ok((FCGI_STDOUT, content)) => {
|
||||||
if send_body && !content.is_empty() && tx.send(Ok(Bytes::from(content))).await.is_err() {
|
if send_body
|
||||||
|
&& !content.is_empty()
|
||||||
|
&& tx.send(Ok(Bytes::from(content))).await.is_err()
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ pub(super) async fn file_response(
|
|||||||
if let Some(range) = range {
|
if let Some(range) = range {
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
CONTENT_RANGE,
|
CONTENT_RANGE,
|
||||||
HeaderValue::from_str(&format!("bytes {}-{}/{length}", range.start, range.end)).unwrap(),
|
HeaderValue::from_str(&format!("bytes {}-{}/{length}", range.start, range.end))
|
||||||
|
.unwrap(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
add_validators(&mut response, etag.as_deref(), modified);
|
add_validators(&mut response, etag.as_deref(), modified);
|
||||||
@@ -173,14 +174,18 @@ pub(super) fn directory_listing(path: &str, entries: &[DirectoryEntry]) -> Vec<u
|
|||||||
}
|
}
|
||||||
let href = html_escape(&href);
|
let href = html_escape(&href);
|
||||||
let slash = if entry.directory { "/" } else { "" };
|
let slash = if entry.directory { "/" } else { "" };
|
||||||
let class = if entry.directory { "entry directory" } else { "entry file" };
|
let class = if entry.directory {
|
||||||
|
"entry directory"
|
||||||
|
} else {
|
||||||
|
"entry file"
|
||||||
|
};
|
||||||
let kind = if entry.directory { "Directory" } else { "File" };
|
let kind = if entry.directory { "Directory" } else { "File" };
|
||||||
let type_label = if entry.directory { "DIR" } else { "FILE" };
|
let type_label = if entry.directory { "DIR" } else { "FILE" };
|
||||||
html.push_str(&format!(
|
html.push_str(&format!(
|
||||||
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
|
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.25.7</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
|
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.26.0</b></span><span>Web Server · Directory listing</span></footer></main></body></html>"#);
|
||||||
html.into_bytes()
|
html.into_bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,7 +202,7 @@ async fn empty_index_can_go_directly_to_directory_listing() {
|
|||||||
let body = std::str::from_utf8(&body).unwrap();
|
let body = std::str::from_utf8(&body).unwrap();
|
||||||
assert!(body.contains("file.txt"));
|
assert!(body.contains("file.txt"));
|
||||||
assert!(body.contains("DIRECTORY LISTING"));
|
assert!(body.contains("DIRECTORY LISTING"));
|
||||||
assert!(body.contains("RustProxy <b>0.25.7</b>"));
|
assert!(body.contains("RustProxy <b>0.26.0</b>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
Reference in new issue
Block a user