v0.26.1
This commit is contained in:
1 parent
ddc5c6593f
commit
b9d137eee1
63 files changed
+1227
-440
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.26.0.
|
compatibility. Current source version: 0.26.1.
|
||||||
|
|
||||||
## 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.1
|
||||||
|
|
||||||
|
- Route low-level protocol errors to per-vhost persistent logs when a vhost can be safely resolved from HTTP authority/Host or TLS SNI; unknown, malformed and non-attributable traffic remains in `_unmatched.jsonl`.
|
||||||
|
- Add `sni`, `authority`, `matched_by`, `reason` and `error` diagnostics to stored protocol-error records while keeping the 0.26.0 compatibility fields.
|
||||||
|
- Keep protocol-error persistence independent from the Live Logs memory buffer.
|
||||||
|
- Improve Logs Explorer with selectable saved columns, a sticky table header, highlighted selection and inline JSON record details.
|
||||||
|
- Collapse the configured-file inventory by default, repair mapped `client_ip` display and extend keyword filtering to protocol diagnostics.
|
||||||
|
- Keep schema 8 and the existing saved proxy configuration format.
|
||||||
|
|
||||||
## 0.26.0
|
## 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 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.
|
||||||
|
|||||||
Generated
+1
-1
@@ -1688,7 +1688,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-proxy"
|
name = "rust-proxy"
|
||||||
version = "0.26.0"
|
version = "0.26.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rust-proxy"
|
name = "rust-proxy"
|
||||||
version = "0.26.0"
|
version = "0.26.1"
|
||||||
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.26.0
|
# RustProxy 0.26.1
|
||||||
|
|
||||||
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
|
Independent Rust reverse proxy and static Web Server with a dashboard, SQLite configuration,
|
||||||
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
|
streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhosts and backend pools.
|
||||||
|
|
||||||
**0.26.0 release source.**
|
**0.26.1 release source.**
|
||||||
|
|
||||||
## Layer-4 streams
|
## Layer-4 streams
|
||||||
|
|
||||||
@@ -11,17 +11,17 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a
|
|||||||
|
|
||||||
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
|
Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners.
|
||||||
|
|
||||||
## What changed in 0.26.0
|
## What changed in 0.26.1
|
||||||
|
|
||||||
- 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.
|
- Route persistent `protocol_error` records to the matching per-vhost JSONL file when the vhost can be safely resolved from normal routing, HTTP authority/Host or TLS SNI; unresolved traffic remains in `_unmatched.jsonl`.
|
||||||
- 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.
|
- Add `reason`, `error`, `sni`, `authority` and `matched_by` diagnostics while retaining the 0.26.0 protocol fields for compatibility; protocol errors still bypass Live Logs.
|
||||||
- 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.
|
- Reuse the normal exact/wildcard hostname resolver for logging attribution instead of maintaining protocol-specific hostname matching.
|
||||||
- Keep file-backed Logs independent from Live Logs: existing configured files can be viewed even when their writers are currently disabled.
|
- Improve the **Logs** workspace with selectable account-scoped columns, a sticky table header, highlighted selected rows and inline record details.
|
||||||
- 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.
|
- Collapse the configured-file inventory by default so large per-vhost installations do not dominate the page.
|
||||||
- Extend standard structured records with `event`, `protocol_error`, `error_detail`, `local_addr` and `tls_sni`.
|
- Fix Stored Logs client-IP display for custom structured-log mappings and make keyword search cover diagnostic values, including `reason`, `error`, `sni` and `authority`, with debounced search while typing.
|
||||||
- Keep SQLite schema 8 and the existing saved configuration format; upgrading from 0.25.7 requires no data migration.
|
- Keep SQLite schema 8 and the saved configuration format unchanged; upgrading from 0.26.0 requires no data migration.
|
||||||
|
|
||||||
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).
|
The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.26.1 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md).
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@@ -99,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.26.0 source archive preserves the resolved `Cargo.lock` supplied with the source tree and
|
The 0.26.1 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
|
||||||
@@ -338,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.26.0.zip
|
python3 make_zip.py /tmp/RustProxy-0.26.1.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
## New management and test-origin guide
|
## New management and test-origin guide
|
||||||
|
|||||||
+12
-1
@@ -1,8 +1,19 @@
|
|||||||
# RustProxy roadmap
|
# RustProxy roadmap
|
||||||
|
|
||||||
|
## 0.26.1 - Vhost-aware protocol diagnostics and Logs Explorer UX
|
||||||
|
|
||||||
|
Current direction: 0.26.0 -> 0.26.1
|
||||||
|
|
||||||
|
- Attribute persistent protocol errors through one shared vhost resolver using routed vhost, authority/Host and SNI in that order.
|
||||||
|
- Keep unresolved traffic in `_unmatched.jsonl` and keep low-level protocol errors out of Live Logs.
|
||||||
|
- Add bounded protocol diagnostic identity fields for Stored Logs filtering and inspection.
|
||||||
|
- Add selectable Stored Logs columns, sticky headers, highlighted selection and inline details.
|
||||||
|
- Hide configured-file inventory by default and repair mapped client-IP/search behavior.
|
||||||
|
- Preserve schema 8 and the existing saved proxy configuration format.
|
||||||
|
|
||||||
## 0.26.0 - Persistent protocol diagnostics and stored-log explorer
|
## 0.26.0 - Persistent protocol diagnostics and stored-log explorer
|
||||||
|
|
||||||
Current direction: 0.25.7 -> 0.26.0
|
Completed 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] 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] Keep low-level protocol failures out of the Live Logs memory ring so hostile scans cannot displace ordinary live request history.
|
||||||
|
|||||||
+62
-60
@@ -1,19 +1,19 @@
|
|||||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||||
4a5f6bf0b46f663a37e8759e8e8962e081d154b47e859f1faed7b603726c01de AGENTS.md
|
ef0142e762f044784ceb61c15c8bf16333250886963850c58e3af45b1419cf5d AGENTS.md
|
||||||
d951a8cc95cb902e8d1ec6d702198784458465d36d65a4b90d1df165fb2135d5 CHANGELOG.md
|
ffce7ba9ca03095bb1aee69eb3e806e5849ae6d1e1f24c0a105230fd27162737 CHANGELOG.md
|
||||||
1af885bbc3120d440ea6c2025f4d1cb2ee29a59f62230f5202f587158864f171 Cargo.lock
|
eabd3b41a3f62cc93b6803ba759643bb8b9a7cc1de43b9587ca2154bcc04ff09 Cargo.lock
|
||||||
1845fd0b917d70a6b092176a211d49577b81d5c6a75ddb4b33eee790ef299089 Cargo.toml
|
f2cb130695ac4eb7f4e5255589e282e4d1a5fd269996a74090a6275f464733b1 Cargo.toml
|
||||||
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
|
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
|
||||||
a33a2aab100f87a4f3a868ca32276dbf1e122b3294711c8543ea1ad3e15ab869 README.md
|
d3f596b1e2a8caa7129bca66f13b5c6f26cf3513de495c96f20a64a6763cac26 README.md
|
||||||
a49f373ddac5ba6a0ad778fb545a7ec82dec6b0c436c5f00b3f94c2ebfacd994 ROADMAP.md
|
6065f28237d57a21f58fdfa83050781b118fdef3dedf961b1adabc8e734bb788 ROADMAP.md
|
||||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||||
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
|
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
|
||||||
e6787f5b31d63a6c7b997562855ac7349680268d6f9a7d55d550f33a702b7ad1 debian/README.md
|
fe2a113daf65042c84838b1e1b7268a74775288ff3fc9bb1570f973d5f8106b7 debian/README.md
|
||||||
1fa83ad64f747e27822815de2d85221ac4bb20e5003a72112fe9b11d7b95cad8 debian/build-package.sh
|
2690b8c6a61383a2b1752736e44822c8cfc6119beeacbcba326fcc9f02fc162a debian/build-package.sh
|
||||||
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
|
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
|
||||||
228319d6ad21c699dc5cc6104686d02d26f41c5d7d8df73fea021896389b314a debian/changelog
|
0e4306f33dd4a04100af9b3932d2cc264aa901a149cec0d3f95e8aeb4c639152 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
|
||||||
162551960700a3a1c68dba29f37248f6a31d6a0d39144559e9653f6657ca4d94 dev.sh
|
cf4180779795cf06416d34a55372ff7d258f0ce6a1e7208bf7912b62b682aa7f 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,14 +33,14 @@ 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
|
||||||
9de427ff54b2fba7a8ed5ff356b1b0069bda81f3953b0252638e84efc79b73fb docker-agent/model.go
|
d4fa829176c8997a8fc42d59d7c45e252efa8a6f05c818d511fd24146da39a01 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
|
||||||
e386c03dba02287a864329b9b1993cbfd131764d0755c8d9f87b8b198940abcf docs/API_ACCESS.md
|
9b66a67392803805d0d28c7719f263288b5e6ef16f06b9b8c13a3afdf90271ed 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
|
||||||
@@ -49,20 +49,20 @@ db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER
|
|||||||
9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md
|
9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md
|
||||||
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
|
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
|
||||||
86bb62f288f2e999994c302716fc9986dba1ac7a26e598dc97cb56a7bc21cfb5 docs/ERROR_LOGGING.md
|
86bb62f288f2e999994c302716fc9986dba1ac7a26e598dc97cb56a7bc21cfb5 docs/ERROR_LOGGING.md
|
||||||
363e46aa4d6ca133a21e47d5a530154fbd1784eb43593b36022c85c66c0e787b docs/FILE_LOGS.md
|
a1150f7525a20db8e8399eb04ce679e1699fd1e574485e27bdf2f565436fc508 docs/FILE_LOGS.md
|
||||||
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
|
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
|
||||||
5299c5f58056fea84e88d6d86857fb593d0a7458af3be559476de2bb57939b4d docs/INSTALL.md
|
1b120d86a65137319234129ce78af39733e59af4b764869a9d7fdd01b9312382 docs/INSTALL.md
|
||||||
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
|
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
|
||||||
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
|
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
|
||||||
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
|
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
|
||||||
ba3cbe1f80271326d6e92e98e035ff280bb8ddbf9d602273684dca147e09d172 docs/MIGRATION.md
|
526e24e7a9b76941efccf64f4bacab5529e08489add316ec44b33da2756b4dd5 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
|
||||||
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
|
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
|
||||||
a31cce4c476ce9d449a5083510660a569d97dcf165f36e24d26b100a29d66944 docs/README.md
|
087ce068b693b9a0b5fba4b884f82d9e1ece056e5b083e4d2f8cab41062f9bd7 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
|
||||||
@@ -72,7 +72,7 @@ a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.
|
|||||||
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
||||||
4245408286d8bdc3bed585c2f0928ecd4c69a3e48dfacbac9b952079ec2075b1 docs/SETTINGS.md
|
4245408286d8bdc3bed585c2f0928ecd4c69a3e48dfacbac9b952079ec2075b1 docs/SETTINGS.md
|
||||||
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
|
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
|
||||||
d9153240be9cca05079893e10b1649c3d5c9a3cf8517890afe0e3aff1cdd6fad docs/TEST_REPORT.md
|
cc678073533ef97b5b9397a1698e7a33a3d7156cc933d754adbf748443e70693 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
|
||||||
@@ -100,6 +100,7 @@ d0e609febbcd1cfdb8ea0b00b4fa6f081eac01e2d43a81e38a53ae25783152cc docs/V0.24.8_A
|
|||||||
9dbb66e408bcd4d32267724e557fa9f7f44ef9b70db4ab170c61a7a839b7b157 docs/V0.25.3_AUDIT.md
|
9dbb66e408bcd4d32267724e557fa9f7f44ef9b70db4ab170c61a7a839b7b157 docs/V0.25.3_AUDIT.md
|
||||||
583d548b4a5db9f35a2bbf8ec00ec22397764c5850a8e883a3d12680da517df7 docs/V0.25.4_AUDIT.md
|
583d548b4a5db9f35a2bbf8ec00ec22397764c5850a8e883a3d12680da517df7 docs/V0.25.4_AUDIT.md
|
||||||
bf9b4cf4fec2c8ce287e1b3c2e7d4828e101dafe4d6c173f099819f22493d144 docs/V0.25.5_AUDIT.md
|
bf9b4cf4fec2c8ce287e1b3c2e7d4828e101dafe4d6c173f099819f22493d144 docs/V0.25.5_AUDIT.md
|
||||||
|
05e0b4cc074b07ff37d45fabc20c7272bada217286b3db3d7997cae25c1f6f11 docs/V0.26.1_IMPLEMENTATION.md
|
||||||
ce259052c023e90a7bfb6e39fe82e52b845a8b2a051bfa29883d29e87a9301a2 docs/VHOSTS.md
|
ce259052c023e90a7bfb6e39fe82e52b845a8b2a051bfa29883d29e87a9301a2 docs/VHOSTS.md
|
||||||
b979afd3d56731eeb06e9dc38d37730693e7fbf3aa78467932842c0e712e7b26 docs/VHOST_ACCESS.md
|
b979afd3d56731eeb06e9dc38d37730693e7fbf3aa78467932842c0e712e7b26 docs/VHOST_ACCESS.md
|
||||||
f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md
|
f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md
|
||||||
@@ -114,14 +115,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
|
||||||
a25dbbfb5f6ebdf23898cfc50828e965d14bbf9f2a397463010a07840cd84f49 grafana/README.md
|
b14d30a7785545a50a057f020b3df02a2f46a65c75fca0e5aba1dcb959852e57 grafana/README.md
|
||||||
8909eeefb8186c6c8a140b460501db96a7891cd062b7ad3d4ca754ea2185387c grafana/rustproxy-overview.json
|
7ba863b4731bb3240cc55fb3e97e6bb2492e43718980a0cfbddd98da70bef2c5 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
|
||||||
e034c5376b65bf23d49f652d5315e87d0d3f28b02c91955055227235121d78a3 langs/en_US.json
|
1b2f1e7127ccddb4f9c7cbca48282b5cb5f8fd38dbcdbc2c0a5695679821ee73 langs/en_US.json
|
||||||
47680dc4c306623890febef47d847f50ed5e8780550c15391f13c3c15ca1c770 langs/pl_pl.json
|
822c1630aea00228118981a4dffdda05a0dc49dff715c111a0760b0ae50e1f4c langs/pl_pl.json
|
||||||
d5df6eade5883206f52d6e015ddfd4cac0c904faaa7616c1c5c49ff5bd05035a make_zip.py
|
fca0b6e43ee1d1b970d9dd33eba93000c452e0380d9f0c3bbd9ee03b4868cfc1 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
|
||||||
@@ -140,11 +141,11 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p
|
|||||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
|
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
|
||||||
a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py
|
a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py
|
||||||
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
|
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
|
||||||
6a0425b289abfca1bc67d43ce8a586b20ffcb9d839e020ddbb1205c4a6336eb2 scripts/check_source.py
|
3e66e89b51f8b1328a68a68b9a87f865d4034c08f01e8ccbf37fe9fdb7e39d76 scripts/check_source.py
|
||||||
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
|
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
|
||||||
d2e954c80b229f8b4a37b2b401b0ac7611507cb76c03866cf688453c531b903e scripts/deploy_core.py
|
aafb8042851ae43b64fc51c793ed2134a308d1d155574ae79c1889afc226680a scripts/deploy_core.py
|
||||||
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
|
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
|
||||||
948d910904143a98574c7fc256dfa2fac17572c6606876e5b64b0a8baf56eaba scripts/docs_check.py
|
64c746a3d225d810ee8aa9055cbafe80011fba49dbfe0f6857d631d171046ee9 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
|
||||||
@@ -154,8 +155,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
|
||||||
f9f8a13e3a8689a45db81c785acaa8c1a78757d3b101e430340f0a881424f9d3 scripts/preflight.py
|
6a314bfaf04760a0ae5da298a016400dab705c36769b49412400e6811d679b16 scripts/preflight.py
|
||||||
7fdadf0f973aca3c811deb018f1191eee1bcea06446c1e641a80b7096c452bc2 scripts/release_smoke.py
|
78e9f37c2b31314f582a232b3c45f5f620158094ca24619e47997a820ccf09fb 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
|
||||||
@@ -167,11 +168,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
|
||||||
41fe5df23e28d4b13d5e998906a8c3a18a312fbeb6cb856237cbae58291fa91e scripts/swagger_assets.py
|
e7790bf216ce8897d29df92c1b44b056644c3cf32417ead21197734fcc3d0f34 scripts/swagger_assets.py
|
||||||
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
|
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
|
||||||
c4097fecc34a7b545ffba7203b928f5edf4ed932936df903d90673fc23fac766 scripts/upgrade.py
|
8e5e828c8e18e78678664bdf8eea58adb7cfb736a9464cea1988fdf77c824850 scripts/upgrade.py
|
||||||
43db35e7264b0345592172f2d1f6a6e4919fc4cbde2a9ff787acd7fff1d031d6 scripts/web_assets.py
|
43db35e7264b0345592172f2d1f6a6e4919fc4cbde2a9ff787acd7fff1d031d6 scripts/web_assets.py
|
||||||
8af399947a8dac09ecf33aecbfe7a4c1fc38722605d936fba65d9715833fa367 scripts/web_server_contracts.py
|
4016a00b0d503163e01f9dae35067d813e588989a442e41e0d44a758c52a7a63 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
|
||||||
@@ -184,30 +185,31 @@ 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
|
||||||
c9e4237e64a11e7252d0b0b65e12adc2513b8232e9d5fbc81ef22ec81671df7a src/admin/documentation/documents.rs
|
89b6d6a0c2ddef5e77a8f9383105fd8a9b0e195de0ff46c012dd9d1ef57ee2a4 src/admin/documentation/documents.rs
|
||||||
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
|
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
|
||||||
b9eb9fff800ea42da1779ea123d6951f3ea8734f664aed577dbe5961f007443f src/admin/file_logs.rs
|
8ff4e0a57487c4244189f78bfeea0bfc509dcb996ed740000e3fa288fb8657bd src/admin/file_logs.rs
|
||||||
beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs
|
beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs
|
||||||
aeb8351f4d8455cbc81f363862195ce3fda09a07291df7b1eba55cff64861a68 src/admin/mod.rs
|
fb079609d182aa63d32b7f2bfc7cf1a4229d7d500266e981fb0fde4fdd032961 src/admin/mod.rs
|
||||||
ae136f17c60ea724906d170799866ef998e90418d53497696ded7424b8beb1b4 src/admin/openapi.json
|
b40aae24eaf99ed8086951dc30d06d75fa0ae48fc2fee9745491337336b129c0 src/admin/openapi.json
|
||||||
a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs
|
a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs
|
||||||
|
ee3e9c39eb2d645e55356b99c1af7d6a2ec14acd622e0e0287dea2fc47628fb4 src/admin/preferences.rs
|
||||||
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
|
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
|
||||||
d246389975c87d125ebcb8c24282a3729d509cc70e13e0f3b56b4008d80d76b4 src/admin/router.rs
|
d246389975c87d125ebcb8c24282a3729d509cc70e13e0f3b56b4008d80d76b4 src/admin/router.rs
|
||||||
9e2981f9c96140bd6d9ca54e0d03b51b0783360e534a13e3b5cd1bec8c9440fd src/admin/session.rs
|
333d5f9f4c27941c535ac689e5826264ffa2bb86b00832b597a9bb9f811f01e8 src/admin/session.rs
|
||||||
f7938b202919a49fd2ff86ecdf09efadae12e0ffc0a4246a72a76218db11aa88 src/admin/status.rs
|
f7938b202919a49fd2ff86ecdf09efadae12e0ffc0a4246a72a76218db11aa88 src/admin/status.rs
|
||||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||||
e5f973c6fbddc3897bc0e6d9f8e70cbe9279acea6161ffb46e013792e845e1e4 src/admin/tests.rs
|
e5f973c6fbddc3897bc0e6d9f8e70cbe9279acea6161ffb46e013792e845e1e4 src/admin/tests.rs
|
||||||
5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs
|
5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs
|
||||||
6f8c1e30babb2cc4cb7accf4bfa28338651a469b16341f6e7eaec881dd9abf2b src/admin/tests/part_02.rs
|
6f8c1e30babb2cc4cb7accf4bfa28338651a469b16341f6e7eaec881dd9abf2b src/admin/tests/part_02.rs
|
||||||
77608ad33da348dbea41d2a7ceadb75b0bcf05336cb5a20b7f2a47502f1e4842 src/admin/tests/part_03.rs
|
77608ad33da348dbea41d2a7ceadb75b0bcf05336cb5a20b7f2a47502f1e4842 src/admin/tests/part_03.rs
|
||||||
bf3cc7a41c2575003515cc8212111395aaa0f2169b7d04b6bbd0908a6bc2af0f src/admin/tests/part_04.rs
|
60ad5c76db3cd6fdd144d504a7b50153e133ab357e49c8ebadb99c1c8bce562c 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
|
d6479bcf439621fb8ddc8e9ee71b599cf6d6cf72780422c490ed48b16684074d 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
|
||||||
a9953d0ed8ba3d301db51dabafeeac42c0697fc0d11a45abef501e790ac14758 src/admin/web/app/core.js
|
058c1aec00109237a3b5d88134db85cdd4197c45d0335f652b661400edf0fec5 src/admin/web/app/core.js
|
||||||
19abc332a1009bcb087fe4ebb58d458739b27480ab50715b03d07fd408302942 src/admin/web/app/runtime.js
|
19abc332a1009bcb087fe4ebb58d458739b27480ab50715b03d07fd408302942 src/admin/web/app/runtime.js
|
||||||
2bfe0dc64d4386133b04bf71502ea9ed8ff346ab0023dc588c68496f6abf3ba3 src/admin/web/app/settings.js
|
cada294084cfa8cb7080143e1ad21d46136bb644ff5f15bb6d457dc034fe11ff 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
|
||||||
@@ -221,9 +223,9 @@ 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
|
bd5b8789d03df1e0fd0c4da31f7980e74870c26485311f566e511433f9c09e2e src/admin/web/file-logs.js
|
||||||
dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js
|
dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js
|
||||||
c3d38fbf046e287a77c51a5b0182e377a90d7dce7b3fb23c2e42a712e1347d80 src/admin/web/index.html
|
97444c4ab0441af6767e7383bf70c9f3c163b846614ce9b3623ab40e05731118 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
|
||||||
@@ -232,7 +234,7 @@ e41619e5cb0ba01a3d287748f28eb38bbe7641f9bedade252d286c18e619c400 src/admin/web/
|
|||||||
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
|
||||||
a68cbb41d43d699aea8f71ec58af347695dfd91f5a4b13d17b96a231fa942749 src/admin/web/style.css
|
c6f8dcdf07f0bf27036d9b2f37bdec56c0144b7f7365e6f8b968f700047697b4 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
|
||||||
@@ -308,11 +310,11 @@ ad47db6519e471ec53a408129bb10a794033430e28fcd3d9a4cf273669900829 src/compressio
|
|||||||
7921ae451ccd65d647165b02919bd97399399da471454a6a1a1f63e682b76fc8 src/config.rs
|
7921ae451ccd65d647165b02919bd97399399da471454a6a1a1f63e682b76fc8 src/config.rs
|
||||||
36c8774fe6fdc44d13286b7688726ceaa9b55cee4c63113689df63456cbecaac src/config/backend.rs
|
36c8774fe6fdc44d13286b7688726ceaa9b55cee4c63113689df63456cbecaac src/config/backend.rs
|
||||||
8b39e92594133c32b6e113c67849fc10f51ceec9e3bbf69211779f6628d0e001 src/config/compile.rs
|
8b39e92594133c32b6e113c67849fc10f51ceec9e3bbf69211779f6628d0e001 src/config/compile.rs
|
||||||
66b3061200c06c4fffe3350e3807bd9948b9b05aa91410da5d05edef1e0d791d src/config/model.rs
|
bb1059f07a0e7bbffeb2575334bbe5e95d1f3167c9fe0769696f585523a42471 src/config/model.rs
|
||||||
4d2a4a078e890a9d1582e2cc6f9b8f0e8c2c676c4c94bc927bffdbe55e932610 src/config/routing.rs
|
4d2a4a078e890a9d1582e2cc6f9b8f0e8c2c676c4c94bc927bffdbe55e932610 src/config/routing.rs
|
||||||
9bc22e19b7cfd3b7555faac8497c7410ef48e8c6b4eb3515f77f6a0e2f251c5d src/config/snapshot.rs
|
9662674d17740df4999f28bec42a2fbd3453a93e591c88f0796edc396bb23a18 src/config/snapshot.rs
|
||||||
7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/config/tests.rs
|
7d94d1bd4eb698e0588b822d93e56e3dc5594c46edac4aa9713f148893ebe6d5 src/config/tests.rs
|
||||||
421501ac3c524b14a5b7f51bdca75acd69903cc45dcbc51bd5e295a29e3e5ba0 src/config/tests/part_01.rs
|
7592bdeaad9950a720e2d841fcb1a4edb2a68af5333a6163e672d0392616103c src/config/tests/part_01.rs
|
||||||
fb0d4d91ae95fe551b06b9195f9e6d41bc40fb6a3a7c547b170fcfeb9abe213c src/config/tests/part_02.rs
|
fb0d4d91ae95fe551b06b9195f9e6d41bc40fb6a3a7c547b170fcfeb9abe213c src/config/tests/part_02.rs
|
||||||
fa4e955ad23c0b5b3fb25af9f144c5c483f116fb79568300543b6501575d9e18 src/db.rs
|
fa4e955ad23c0b5b3fb25af9f144c5c483f116fb79568300543b6501575d9e18 src/db.rs
|
||||||
2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs
|
2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs
|
||||||
@@ -330,23 +332,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
|
||||||
1988f116779d725d324fe7b8af0eed071916efaffb38636b55e6276966a36056 src/http3.rs
|
d0cd8589321595309834690fa9ddb98488886bc98f34ea45aa0ccb3035412fb5 src/http3.rs
|
||||||
2165a0a4b27d621985e2030326ad52b8bbd59d7b9e4f092f72f7e92b4e58b497 src/http3/body.rs
|
2165a0a4b27d621985e2030326ad52b8bbd59d7b9e4f092f72f7e92b4e58b497 src/http3/body.rs
|
||||||
68bc11160bc5275a904e48af8698b37433096fb6d592364d8281d59bb9e2b3f4 src/http3/tests.rs
|
68bc11160bc5275a904e48af8698b37433096fb6d592364d8281d59bb9e2b3f4 src/http3/tests.rs
|
||||||
3dab8fac75263120e48ee869ad55a3e2ccc2a19fc9f6d6a6fe055657ec55b1e3 src/lib.rs
|
3dab8fac75263120e48ee869ad55a3e2ccc2a19fc9f6d6a6fe055657ec55b1e3 src/lib.rs
|
||||||
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
|
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
|
||||||
d8abc31f130e85b38c760b96e3369639464bca513991532e025485489a0af70e src/logging/error_runtime.rs
|
d8abc31f130e85b38c760b96e3369639464bca513991532e025485489a0af70e src/logging/error_runtime.rs
|
||||||
2ee000906a56219cda4124b5bafad4af326a70ff0baa160d8f920ad90c5ebef9 src/logging/format.rs
|
a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/format.rs
|
||||||
15f88b956b2a4804ba82f493ab20e8bd1ca57920a9fd218e9e3d85867c796d81 src/logging/model.rs
|
83e7bd1517ede4a2d1696ba0b22bdd5d34a4c29fcc8eb5c30c454dc4eba66853 src/logging/model.rs
|
||||||
c4c2f77e4e64943ac7e5d26688743a19e1349d2c984fec20dcff70373af1c254 src/logging/runtime.rs
|
7c0d8e8f46f96cbaf3a5cb5fd47953f9185d7558dd71e599676bc63578b7ca99 src/logging/runtime.rs
|
||||||
a978bb38c87336145395d598213718ee9ede41e82e3ab89883fdb8e3b024d944 src/logging/tests.rs
|
17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs
|
||||||
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
||||||
46732ed6d9cf2a33031624f941b8318d2676fb1b6bb5142e96820301f9fa1ac4 src/main/admin_runtime.rs
|
46732ed6d9cf2a33031624f941b8318d2676fb1b6bb5142e96820301f9fa1ac4 src/main/admin_runtime.rs
|
||||||
86d62387fb73c221aeed3d0136b4b4954e5feac8533f8b3490b585d17fb743ac src/main/bootstrap.rs
|
7d1ed0a639ed79b914d609d913b3004d5da7ad60695a487799ef42bcc4d74c9c 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
|
||||||
7110171b265945d44a45ca09a02c9860e7bbae4e9aec2bc9583c7a65a3fea907 src/main/tasks.rs
|
d2b53a7681476f368b169a1c3bb2e4646af0c27084e52cd45cc7c37a2451cfcb 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
|
||||||
@@ -362,7 +364,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
|
||||||
b4ab7010056ff095e4c4d2f878d2301b8ce626bd2059ef4fd54c3647c16d4f61 src/proxy/body.rs
|
b5856fd38a957b2ca9c7e3606d84d13d5cfcf3a45b487b15ae1d2dc248cbda57 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
|
||||||
@@ -399,11 +401,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
|
||||||
a5495ac40a75d8c94b6f1c47b7e9c0946f836d40735464d4d18ce6705541d063 src/server/http.rs
|
f2f6196632b2fdb2444189624f961edaf5bdfff57f700b0207b57e2491c1daa5 src/server/http.rs
|
||||||
8c6ca5eaffb9acab57199d0349722703b903cfff3752baad2cf2596bd51188de src/server/listeners.rs
|
8c6ca5eaffb9acab57199d0349722703b903cfff3752baad2cf2596bd51188de src/server/listeners.rs
|
||||||
c362593f2feb7cb3b22b2a4fb22f8afa128e72ca8185111ceb9bd0f8125abe55 src/server/serve.rs
|
71899a9b1897fa22ab9e8eb6a670054a6d382bad78714006b9b218d07ba99902 src/server/serve.rs
|
||||||
b209c1135aa30c5a2d1edb3f8a80b9b95398b2c2e4c03139b59b8a4955f1eb99 src/server/serve/tests.rs
|
e9e478cf45b88994eaffd115f51764b6b07852ee3c941cafdabb20584d3dabc0 src/server/serve/tests.rs
|
||||||
c123c2e1bfc127621a5c8915ad3e0a32f57d557b2fd8760f1cfc537d703e1a02 src/server/tls_runtime.rs
|
048f2c7a8ef698a172ba7ed844558f4118b367b2cf340748c5c28da7723d8e85 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
|
||||||
@@ -453,7 +455,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
|
||||||
7fd2d89aa016f03e602c140c49ea24c95a66a90abfa6de186e6806b224e68252 src/web_server/fastcgi/protocol.rs
|
4cd184d73f00bc7abc967e0fe574e16f84de1e9d999e599de6f4a873050f9e61 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
|
||||||
@@ -463,14 +465,14 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
|
|||||||
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
|
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
|
||||||
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
|
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs
|
||||||
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
|
c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs
|
||||||
6c1b6b61fadb978620bb6906e535e1b96ac9cb3f062b6178931a8fc267bd8191 src/web_server/response/file.rs
|
9dd6c0861cfd11a278002c665e81440a302d0a91f2e486f9437d0185bbc40542 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
|
||||||
8c15c626e879016d5d3e3f0c30af42aa9acd18ce55b6965976ca6895e0af9028 src/web_server/tests/part_02.rs
|
070141f7e6f0a5659ac18f90e03fd73a73767aded81d752c342440dbee80b5f0 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.26.0.
|
This directory builds the `rustproxy` binary package for RustProxy 0.26.1.
|
||||||
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.26.0_*.deb' -o -name 'rustproxy_0.26.0_*.changes' -o -name 'rustproxy_0.26.0_*.buildinfo' \) \
|
find .. -maxdepth 1 -type f \( -name 'rustproxy_0.26.1_*.deb' -o -name 'rustproxy_0.26.1_*.changes' -o -name 'rustproxy_0.26.1_*.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.1) stable; urgency=medium
|
||||||
|
|
||||||
|
* Attribute persistent protocol errors to configured vhosts using route, Host/authority or SNI.
|
||||||
|
* Improve the Logs explorer with selectable columns, sticky headers and inline record details.
|
||||||
|
* Fix mapped client-IP display and protocol-diagnostic keyword search.
|
||||||
|
|
||||||
|
-- RustProxy Maintainers <noreply@example.invalid> Mon, 05 Oct 2026 17:17:00 +0200
|
||||||
|
|
||||||
rustproxy (0.26.0) stable; urgency=medium
|
rustproxy (0.26.0) stable; urgency=medium
|
||||||
|
|
||||||
* Persist malformed HTTP, TLS/QUIC and HTTP/2/HTTP/3 protocol failures through bounded logging sinks.
|
* Persist malformed HTTP, TLS/QUIC and HTTP/2/HTTP/3 protocol failures through bounded logging sinks.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi
|
|||||||
case "$mode" in
|
case "$mode" in
|
||||||
help|-h|--help)
|
help|-h|--help)
|
||||||
cat <<'HELP'
|
cat <<'HELP'
|
||||||
RustProxy 0.26.0
|
RustProxy 0.26.1
|
||||||
./dev.sh Build and start proxy + local panel
|
./dev.sh Build and start proxy + local panel
|
||||||
./dev.sh run [serve flags] Same, with extra server options
|
./dev.sh run [serve flags] Same, with extra server options
|
||||||
./dev.sh admin setup-token Generate/print a one-time setup capability before first login
|
./dev.sh admin setup-token Generate/print a one-time setup capability before first login
|
||||||
@@ -139,7 +139,7 @@ if [[ "$mode" == "test" ]]; then
|
|||||||
printf 'Checking all Rust targets before tests...\n' >&2
|
printf 'Checking all Rust targets before tests...\n' >&2
|
||||||
cargo check --locked --all-targets
|
cargo check --locked --all-targets
|
||||||
fi
|
fi
|
||||||
printf 'Building RustProxy 0.26.0...\n' >&2
|
printf 'Building RustProxy 0.26.1...\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.26.0"
|
const version = "0.26.1"
|
||||||
|
|
||||||
type config struct {
|
type config struct {
|
||||||
Listen string
|
Listen string
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# Administration API access - 0.26.0
|
# Administration API access - 0.26.1
|
||||||
|
|
||||||
## Sign-in boundary
|
## Sign-in boundary
|
||||||
|
|
||||||
|
|||||||
+45
-19
@@ -1,44 +1,72 @@
|
|||||||
# Stored log explorer
|
# 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**.
|
RustProxy 0.26.1 provides 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
|
## 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.
|
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 routed request activity.
|
||||||
|
|
||||||
The stored-log explorer does not read that memory ring. It reads only files derived from the active RustProxy logging configuration:
|
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 combined structured access-log path, or JSONL files directly inside the configured per-vhost directory;
|
||||||
- the configured optional error-log file.
|
- 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.
|
Existing files remain browsable after their writer is disabled. Low-level `protocol_error` records are sent only through configured persistent structured-log sinks and are deliberately not inserted into Live Logs.
|
||||||
|
|
||||||
## Protocol and malformed-traffic records
|
## Vhost-aware protocol 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:
|
Ordinary completed requests use `event: "request"`. Low-level failures use `event: "protocol_error"`. When RustProxy can safely identify a configured vhost, the record is persisted through that vhost's existing sink. In per-vhost file mode an unresolved record uses `_unmatched.jsonl`.
|
||||||
|
|
||||||
|
Attribution priority is:
|
||||||
|
|
||||||
|
1. vhost already selected by normal routing;
|
||||||
|
2. HTTP `:authority`;
|
||||||
|
3. HTTP `Host`;
|
||||||
|
4. TLS/QUIC SNI;
|
||||||
|
5. no safe match -> `_unmatched`.
|
||||||
|
|
||||||
|
All hostname matching reuses the normal exact/wildcard frontend resolver and listener binding rules. A listener or local port by itself is not sufficient to select a vhost.
|
||||||
|
|
||||||
|
A representative record is:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"event": "protocol_error",
|
"event": "protocol_error",
|
||||||
|
"reason": "tls_handshake_error",
|
||||||
"protocol_error": "tls_handshake_error",
|
"protocol_error": "tls_handshake_error",
|
||||||
|
"error": "...",
|
||||||
"error_detail": "...",
|
"error_detail": "...",
|
||||||
"client_ip": "192.0.2.10",
|
"client_ip": "192.0.2.10",
|
||||||
"local_addr": "192.0.2.20:443",
|
"local_addr": "192.0.2.20:443",
|
||||||
"tls_sni": "app.example",
|
|
||||||
"transport": "tls",
|
"transport": "tls",
|
||||||
"http_version": "",
|
"vhost": "app",
|
||||||
"status": 0,
|
"sni": "app.example",
|
||||||
"outcome": "protocol_error"
|
"tls_sni": "app.example",
|
||||||
|
"authority": null,
|
||||||
|
"matched_by": "sni",
|
||||||
|
"duration_ms": 0
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
`matched_by` is `router`, `authority`, `host`, `sni` or `none`. Compatibility fields from 0.26.0 remain present so existing consumers do not need an immediate migration.
|
||||||
|
|
||||||
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.
|
SNI, authority and Host are lookup inputs only. RustProxy never derives a filename directly from client-provided identity text. Only the configured internal vhost identifier returned by the shared resolver can select a vhost sink. Invalid internal IDs fall back to `_unmatched` rather than being sanitized into a new path.
|
||||||
|
|
||||||
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.
|
Diagnostic identity values are control-character stripped and bounded before persistence. Existing error-detail redaction and limits remain in force. Raw packets, request bodies and raw malformed request bytes are not captured.
|
||||||
|
|
||||||
## Browser security model
|
## Logs Explorer UI
|
||||||
|
|
||||||
|
The workspace supports source, case-insensitive text, event class, HTTP status class, client IP and vhost filters. Text search scans stored values, including protocol diagnostics such as `reason`, `error`, `sni` and `authority`, and refreshes with a short debounce while typing.
|
||||||
|
|
||||||
|
The table provides account-scoped selectable columns. The default view keeps the original concise columns while optional protocol columns expose transport, match source, SNI and authority. At least one column must remain visible.
|
||||||
|
|
||||||
|
The table header stays visible while the result list scrolls. Selecting a row highlights it and expands the complete JSON record inline immediately below that row; selecting it again collapses the details.
|
||||||
|
|
||||||
|
The configured-file inventory is collapsed by default so installations with many per-vhost files do not lose most of the screen to source paths. The source selector still contains the configured sources, including `_unmatched.jsonl` when present.
|
||||||
|
|
||||||
|
For structured access logs, the explorer reconstructs canonical fields from the active JSON field mapping. In particular, `client_ip` remains available to the table and filter even when the output JSON key was renamed by a custom mapping.
|
||||||
|
|
||||||
|
## Browser security and bounds
|
||||||
|
|
||||||
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 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`.
|
||||||
|
|
||||||
@@ -46,16 +74,14 @@ The reader is bounded:
|
|||||||
|
|
||||||
- up to 2 MiB scanned from the tail of one source;
|
- up to 2 MiB scanned from the tail of one source;
|
||||||
- up to 8 MiB across one query;
|
- up to 8 MiB across one query;
|
||||||
- up to 20,000 lines across the complete query, shared fairly across selected sources;
|
- up to 20,000 lines across the complete query, shared across selected sources;
|
||||||
- at most 32 sources in an `all` query;
|
- at most 32 sources in an `all` query;
|
||||||
- page size at most 500 and offset at most 10,000.
|
- page size at most 500 and offset at most 10,000.
|
||||||
|
|
||||||
|
If more than 32 configured files are eligible for an `all` query, the newest 32 are scanned and the response is marked truncated. A specific source can always be selected directly.
|
||||||
|
|
||||||
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.
|
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.
|
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 explorer is diagnostic rather than a retention system. RustProxy does not add rollover, indexing or compression; use the operating-system log rotation policy for retention.
|
||||||
|
|
||||||
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.26.0
|
# Installing and updating RustProxy 0.26.1
|
||||||
|
|
||||||
The scripts prepare a Linux/systemd installation. This source archive is **not a
|
The scripts prepare a Linux/systemd installation. This source archive is **not a
|
||||||
verified production build**: Rust/Cargo, real CA operations, live systemd startup
|
verified production build**: Rust/Cargo, real CA operations, live systemd startup
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# Deployment and configuration migration history
|
# Deployment and configuration migration history
|
||||||
|
|
||||||
|
## 0.26.0 -> 0.26.1
|
||||||
|
|
||||||
|
No SQLite or saved-configuration migration is required. Version 0.26.1 changes persistent protocol-error attribution and the Stored Logs UI/API normalization only. Existing JSONL files remain readable, Live Logs behavior is unchanged, and unresolved protocol errors continue to use `_unmatched.jsonl`.
|
||||||
|
|
||||||
## 0.25.7 -> 0.26.0
|
## 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.
|
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.
|
||||||
|
|||||||
+2
-1
@@ -15,6 +15,7 @@ This is the canonical index for the current project documentation. The authentic
|
|||||||
- [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards.
|
- [0.25.1 release audit](V0.25.1_AUDIT.md) - historical Traffic/module-split release scope and compiler-regression guards.
|
||||||
- [0.25.0 specification audit](V0.25.0_AUDIT.md) - Web Server foundation point-by-point implementation status.
|
- [0.25.0 specification audit](V0.25.0_AUDIT.md) - Web Server foundation point-by-point implementation status.
|
||||||
- [0.25.0 pre-production security audit](V0.25.0_SECURITY_PREPROD_AUDIT.md) - threat-oriented source/configuration review, fixes made in-session and mandatory runtime security gates.
|
- [0.25.0 pre-production security audit](V0.25.0_SECURITY_PREPROD_AUDIT.md) - threat-oriented source/configuration review, fixes made in-session and mandatory runtime security gates.
|
||||||
|
- [0.26.1 implementation note](V0.26.1_IMPLEMENTATION.md) - vhost-aware protocol logging, shared resolver and Stored Logs UX changes.
|
||||||
- [TEST_REPORT](TEST_REPORT.md) - verification commands, results and environment limitations.
|
- [TEST_REPORT](TEST_REPORT.md) - verification commands, results and environment limitations.
|
||||||
|
|
||||||
## Functional documentation
|
## Functional documentation
|
||||||
@@ -60,7 +61,7 @@ This is the canonical index for the current project documentation. The authentic
|
|||||||
|
|
||||||
## Historical or version-labelled context
|
## Historical or version-labelled context
|
||||||
|
|
||||||
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone; they are not current-release documentation. The current audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); [V0.25.4_AUDIT.md](V0.25.4_AUDIT.md) records the prior retained-history restore milestone and [V0.25.3_AUDIT.md](V0.25.3_AUDIT.md) records the prior request-policy/history milestone and [V0.25.2_AUDIT.md](V0.25.2_AUDIT.md) records the prior backup/session hardening milestone and [V0.25.1_AUDIT.md](V0.25.1_AUDIT.md) records the prior Traffic/module-split milestone and [V0.25.0_AUDIT.md](V0.25.0_AUDIT.md) remains the Web Server foundation audit.
|
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone; they are not current-release documentation. The current release implementation note is [V0.26.1_IMPLEMENTATION.md](V0.26.1_IMPLEMENTATION.md); the latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); [V0.25.4_AUDIT.md](V0.25.4_AUDIT.md) records the prior retained-history restore milestone and [V0.25.3_AUDIT.md](V0.25.3_AUDIT.md) records the prior request-policy/history milestone and [V0.25.2_AUDIT.md](V0.25.2_AUDIT.md) records the prior backup/session hardening milestone and [V0.25.1_AUDIT.md](V0.25.1_AUDIT.md) records the prior Traffic/module-split milestone and [V0.25.0_AUDIT.md](V0.25.0_AUDIT.md) remains the Web Server foundation audit.
|
||||||
|
|
||||||
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above.
|
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above.
|
||||||
|
|
||||||
|
|||||||
+17
-32
@@ -1,55 +1,40 @@
|
|||||||
# Verification report - RustProxy 0.26.0
|
# Verification report - RustProxy 0.26.1
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
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.
|
RustProxy 0.26.1 extends the persistent protocol diagnostics introduced in 0.26.0 with safe per-vhost attribution and updates the Stored Logs explorer. `protocol_error` records can now carry canonical `reason`, `error`, `sni`, `authority`, `matched_by` and resolved `vhost` diagnostics while retaining the 0.26.0 compatibility fields. Attribution reuses the normal exact/wildcard frontend resolver and never derives a filename directly from client Host/SNI input. Unresolved traffic remains `_unmatched` in per-vhost file mode, and protocol errors continue to bypass the Live Logs history/broadcast path.
|
||||||
|
|
||||||
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.
|
Stored Logs now supports account-scoped selectable columns, sticky headers, highlighted rows with inline JSON details, a collapsed configured-file inventory, custom-field recovery for canonical `client_ip`, and value-based keyword search including protocol diagnostics.
|
||||||
|
|
||||||
## Implemented automated coverage
|
## Source-level regression 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.
|
0.26.1-specific Rust tests cover shared logging-vhost resolution and precedence, authority/Host normalization, unknown/missing SNI, safe vhost sink names, bounded protocol identity fields, persistent protocol fields under custom JSON mappings, Live Logs separation, Stored Logs canonical client-IP recovery and protocol-diagnostic text matching. Administration preference tests cover the new Stored Logs column list and backward-compatible defaults.
|
||||||
|
|
||||||
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.
|
The HTTP/TLS/H2/H3 sources carry the resolved identity through the persistent logging path. TLS ClientHello SNI is retained for later handshake and HTTP/2 failures; HTTP/3 reads the completed QUIC/Rustls handshake identity for later request/connection diagnostics. Failures before a usable identity exists remain unmatched.
|
||||||
|
|
||||||
Rust-independent release checks additionally validate UI/JSON/language/source contracts and reproducible packaging.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
- `src/admin/documentation.rs` was split so the embedded Reader document catalog lives in `src/admin/documentation/documents.rs`; both files now stay below the 500-line Rust module limit without changing Reader behavior.
|
|
||||||
- The Rust module contract now recognizes the existing `*_tests.rs` naming convention, and the documentation contract validates embedded documents independently of `include_str!` line wrapping.
|
|
||||||
- `python3 scripts/rust_module_contracts.py`: PASS.
|
|
||||||
- `python3 scripts/source_module_contracts.py`: PASS. Dashboard JavaScript, Docker Agent Go implementation and operational Python modules remain within their source-size contracts.
|
|
||||||
- `python3 scripts/docs_check.py`: PASS. The documentation index, authenticated Reader/API split and local Markdown links are consistent.
|
|
||||||
- Cargo parsing/type checking remains a release-host requirement because this workspace has no Rust/Cargo toolchain.
|
|
||||||
|
|
||||||
## Verification status in this workspace
|
## Verification status in this workspace
|
||||||
|
|
||||||
| Check | Status |
|
| Check | Status |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `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/editor.js` | PASS |
|
|
||||||
| 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 |
|
|
||||||
| 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 |
|
||||||
| `python3 scripts/security_dependency_contracts.py` | PASS |
|
| `python3 scripts/security_dependency_contracts.py` | PASS |
|
||||||
| `python3 scripts/source_module_contracts.py` | PASS |
|
| `python3 scripts/source_module_contracts.py` | PASS |
|
||||||
| `python3 scripts/rust_module_contracts.py` | PASS |
|
| `python3 scripts/rust_module_contracts.py` | PASS after keeping modified Rust modules within the 500-line contract |
|
||||||
| `python3 scripts/docs_check.py` | PASS |
|
| `python3 scripts/docs_check.py` | PASS |
|
||||||
| `node scripts/web_service_ui_test.mjs` | PASS |
|
| `node scripts/web_service_ui_test.mjs` | PASS |
|
||||||
| OpenAPI/language/Grafana JSON parse | PASS |
|
| `node --check src/admin/web/file-logs.js` | PASS |
|
||||||
|
| language/OpenAPI JSON parse | PASS |
|
||||||
|
| `python3 scripts/check_source.py` | PASS; explicitly reports Rust parsing/types/runtime as not verified |
|
||||||
|
| `python3 scripts/release_smoke.py` | PASS, including upgrade/package/reproducibility contracts |
|
||||||
| `go test ./...` in `docker-agent` | PASS |
|
| `go test ./...` in `docker-agent` | PASS |
|
||||||
| `python3 scripts/check_source.py` | PASS; explicitly reports Rust parsing/types/runtime not verified |
|
| `./dev.sh test` | Rust-independent gates PASS; exits 127 when the Rust stage is reached because Cargo is unavailable |
|
||||||
| `python3 scripts/release_smoke.py` | PASS, including SQLite/backup/upgrade, Debian/Grafana and reproducible source-archive contracts |
|
|
||||||
| `./dev.sh test` | Static/security/UI/smoke gates PASS; exits 127 when it reaches Rust because Cargo is unavailable |
|
|
||||||
| `./dev.sh check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
| `./dev.sh check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||||
| `cargo fmt --all`, `cargo check --locked --all-targets`, `cargo test --locked --all-targets` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
| `cargo fmt --check` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||||
|
| `cargo clippy --all-targets --all-features -- -D warnings` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||||
|
| `cargo test --all-targets --all-features` | BLOCKED: Rust/Cargo toolchain unavailable in this workspace |
|
||||||
|
| `bash dev.sh` complete release build | BLOCKED at Rust compilation: Rust/Cargo toolchain unavailable in this workspace |
|
||||||
|
|
||||||
All Rust-independent release gates available in this workspace pass. Compiler formatting, parsing, type checking, linking and Rust unit/integration tests still require a Rust-enabled release host before treating the archive as compiler-verified production output.
|
The source archive must not be described as compiler-verified until the Rust-enabled release-host gates pass. Rust-independent gates verify source structure, security contracts, documentation, browser JavaScript, packaging invariants and deterministic archive behavior, but they cannot substitute for Rust parsing, type checking, Clippy or runtime tests.
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# RustProxy 0.26.1 implementation note
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Version 0.26.1 is a focused follow-up to the persistent protocol diagnostics and Stored Logs explorer introduced in 0.26.0. It does not change SQLite schema 8 or the saved proxy configuration format.
|
||||||
|
|
||||||
|
The release has two goals:
|
||||||
|
|
||||||
|
1. Attribute persistent `protocol_error` records to a configured vhost when that attribution is safe and deterministic.
|
||||||
|
2. Make the Stored Logs explorer practical on installations with many vhosts and useful for protocol diagnostics.
|
||||||
|
|
||||||
|
Live Logs remains a separate bounded in-memory request view. Low-level `protocol_error` records are persistence-only and must not be published to its history/broadcast path.
|
||||||
|
|
||||||
|
## One shared vhost-resolution function
|
||||||
|
|
||||||
|
All protocol adapters use the same resolver; TLS, HTTP/1, HTTP/2 and HTTP/3 must not implement independent hostname matching.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
resolve_logging_vhost(
|
||||||
|
local,
|
||||||
|
tls,
|
||||||
|
routed_vhost,
|
||||||
|
authority,
|
||||||
|
host,
|
||||||
|
sni,
|
||||||
|
) -> Option<ResolvedLogVhost>
|
||||||
|
```
|
||||||
|
|
||||||
|
Resolution priority is:
|
||||||
|
|
||||||
|
1. a vhost already selected by normal routing;
|
||||||
|
2. HTTP `:authority`;
|
||||||
|
3. HTTP `Host`;
|
||||||
|
4. TLS/QUIC SNI;
|
||||||
|
5. no match, which means `_unmatched.jsonl` in per-vhost file mode.
|
||||||
|
|
||||||
|
The helper normalizes authority/Host/SNI with the existing host parser and delegates selection to the same `Snapshot::frontend` exact/wildcard and listener-binding logic used by normal request routing. It returns the configured internal vhost ID and `matched_by`; client input is never turned directly into a filename.
|
||||||
|
|
||||||
|
## Persistent protocol records
|
||||||
|
|
||||||
|
A protocol record keeps the compatibility fields from 0.26.0 and also exposes canonical diagnostics when available:
|
||||||
|
|
||||||
|
- `event = protocol_error`;
|
||||||
|
- `reason` and compatibility `protocol_error`;
|
||||||
|
- `error` and compatibility `error_detail`;
|
||||||
|
- `client_ip` and `local_addr`;
|
||||||
|
- `transport` and `http_version`;
|
||||||
|
- `vhost` when safely resolved;
|
||||||
|
- `sni` and compatibility `tls_sni`;
|
||||||
|
- `authority`;
|
||||||
|
- `matched_by = router | authority | host | sni | none`.
|
||||||
|
|
||||||
|
Diagnostic identity strings are control-character stripped and length-bounded before persistence. Existing secret redaction and error-detail limits remain in force.
|
||||||
|
|
||||||
|
In per-vhost file mode, a resolved record uses the existing configured vhost sink. Invalid internal vhost identifiers fall back to `_unmatched` instead of being sanitized into a new filename. In combined-file mode, sink behavior is unchanged.
|
||||||
|
|
||||||
|
## Protocol integration
|
||||||
|
|
||||||
|
TLS reads SNI from ClientHello before completing the handshake. A later handshake failure/timeout carries that SNI into the shared resolver. If ClientHello itself cannot be parsed, attribution remains unavailable and the record is unmatched.
|
||||||
|
|
||||||
|
HTTP/2 retains the TLS SNI for post-handshake connection/drain diagnostics. Request paths that have an HTTP request object can additionally provide authority/Host.
|
||||||
|
|
||||||
|
HTTP/3 obtains SNI from Quinn/Rustls handshake data after a successful QUIC handshake and uses it for later request, stream and connection protocol failures. Failures that happen before usable handshake identity exists remain unmatched.
|
||||||
|
|
||||||
|
No protocol path infers a vhost solely from a shared listener or local port.
|
||||||
|
|
||||||
|
## Stored Logs explorer changes
|
||||||
|
|
||||||
|
The `/logs` workspace now:
|
||||||
|
|
||||||
|
- highlights the selected log row;
|
||||||
|
- expands Record details inline directly below that row;
|
||||||
|
- removes the separate details panel below the table;
|
||||||
|
- keeps the table header sticky while the log list scrolls;
|
||||||
|
- lets each administrator choose visible Stored Logs columns and persists the choice in existing account preferences;
|
||||||
|
- offers protocol columns for transport, match source, SNI and authority;
|
||||||
|
- keeps the configured-file inventory collapsed by default;
|
||||||
|
- canonicalizes fields through the active structured-log field mapping so `client_ip` remains visible when the JSON output key was renamed;
|
||||||
|
- searches record values case-insensitively and includes `reason`, `error`, `sni`, `authority` and other stored values;
|
||||||
|
- refreshes keyword results with a short debounce while typing;
|
||||||
|
- retains the existing bounded source/byte/line/page limits and reports truncation when an `all` query contains more sources than can be scanned safely.
|
||||||
|
|
||||||
|
## Security invariants
|
||||||
|
|
||||||
|
The browser still accepts only opaque configured source IDs; it never accepts arbitrary filesystem paths. File reads remain regular-file-only and retain Unix `O_NOFOLLOW` protection.
|
||||||
|
|
||||||
|
SNI, Host and authority are lookup inputs only. They cannot select a filesystem path directly. Only a configured vhost resolved through the existing routing model can select a per-vhost log sink.
|
||||||
|
|
||||||
|
## Regression coverage
|
||||||
|
|
||||||
|
Source tests cover:
|
||||||
|
|
||||||
|
- exact and wildcard hostname precedence using the shared resolver;
|
||||||
|
- router priority over authority/Host/SNI;
|
||||||
|
- Host with port and case-insensitive normalization;
|
||||||
|
- unknown and missing SNI returning no vhost;
|
||||||
|
- mapped `client_ip` normalization in Stored Logs;
|
||||||
|
- keyword matching against protocol diagnostic values;
|
||||||
|
- safe per-vhost filename fallback;
|
||||||
|
- bounded protocol identity fields;
|
||||||
|
- protocol errors remaining outside Live Logs;
|
||||||
|
- preference validation for selectable Stored Logs columns.
|
||||||
|
|
||||||
|
A Rust-enabled release host must still run `cargo fmt --check`, Clippy with warnings denied, all-target/all-feature tests and the complete `dev.sh` release build before the package is considered compiler-verified.
|
||||||
+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.26.0 metric names and a scrape interval around 15s.
|
dashboard expects RustProxy 0.26.1 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.26.0 - Operations",
|
"title": "RustProxy 0.26.1 - Operations",
|
||||||
"uid": "rustproxy-0170",
|
"uid": "rustproxy-0170",
|
||||||
"version": 5,
|
"version": 5,
|
||||||
"weekStart": "",
|
"weekStart": "",
|
||||||
"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."
|
"description": "RustProxy 0.26.1 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic."
|
||||||
}
|
}
|
||||||
@@ -1995,6 +1995,11 @@
|
|||||||
"fileLogsSearchPlaceholder": "IP, path, error, request id...",
|
"fileLogsSearchPlaceholder": "IP, path, error, request id...",
|
||||||
"fileLogsTruncated": "scan limit reached",
|
"fileLogsTruncated": "scan limit reached",
|
||||||
"fileLogsSummary": "{from}–{to} of {total} matches · scanned {bytes} / {lines} lines",
|
"fileLogsSummary": "{from}–{to} of {total} matches · scanned {bytes} / {lines} lines",
|
||||||
|
"fileLogsMatchedBy": "Matched by",
|
||||||
|
"fileLogsSni": "SNI",
|
||||||
|
"fileLogsAuthority": "Authority / Host",
|
||||||
|
"fileLogsColumnsRequired": "At least one Stored Logs column must remain visible.",
|
||||||
|
"fileLogsSourcesRead": "Configured files",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"all": "All",
|
"all": "All",
|
||||||
"clientIp": "Client IP",
|
"clientIp": "Client IP",
|
||||||
|
|||||||
@@ -1995,6 +1995,11 @@
|
|||||||
"fileLogsSearchPlaceholder": "IP, ścieżka, błąd, request id...",
|
"fileLogsSearchPlaceholder": "IP, ścieżka, błąd, request id...",
|
||||||
"fileLogsTruncated": "osiągnięto limit skanowania",
|
"fileLogsTruncated": "osiągnięto limit skanowania",
|
||||||
"fileLogsSummary": "{from}–{to} z {total} wyników · przeskanowano {bytes} / {lines} linii",
|
"fileLogsSummary": "{from}–{to} z {total} wyników · przeskanowano {bytes} / {lines} linii",
|
||||||
|
"fileLogsMatchedBy": "Dopasowanie przez",
|
||||||
|
"fileLogsSni": "SNI",
|
||||||
|
"fileLogsAuthority": "Authority / Host",
|
||||||
|
"fileLogsColumnsRequired": "Co najmniej jedna kolumna zapisanych logów musi pozostać widoczna.",
|
||||||
|
"fileLogsSourcesRead": "Skonfigurowane pliki",
|
||||||
"search": "Szukaj",
|
"search": "Szukaj",
|
||||||
"all": "Wszystkie",
|
"all": "Wszystkie",
|
||||||
"clientIp": "IP klienta",
|
"clientIp": "IP klienta",
|
||||||
|
|||||||
+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.26.0.zip')
|
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.1.zip')
|
||||||
output = parser.parse_args().output.resolve()
|
output = parser.parse_args().output.resolve()
|
||||||
paths = files()
|
paths = files()
|
||||||
checksum = ROOT / 'SHA256SUMS'
|
checksum = ROOT / 'SHA256SUMS'
|
||||||
|
|||||||
+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.26.0"
|
assert manifest["package"]["version"] == "0.26.1"
|
||||||
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
||||||
h3dep = manifest["dependencies"]["h3"]
|
h3dep = manifest["dependencies"]["h3"]
|
||||||
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
|
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.19" and "quinn" in h3dep["features"]
|
||||||
@@ -32,14 +32,14 @@ assert "h3-quinn" not in manifest["dependencies"]
|
|||||||
lock_path = root / "Cargo.lock"
|
lock_path = root / "Cargo.lock"
|
||||||
if lock_path.exists():
|
if lock_path.exists():
|
||||||
lock = lock_path.read_text()
|
lock = lock_path.read_text()
|
||||||
assert 'name = "rust-proxy"\nversion = "0.26.0"' in lock
|
assert 'name = "rust-proxy"\nversion = "0.26.1"' in lock
|
||||||
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
|
assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries"
|
||||||
for release_doc, marker in {
|
for release_doc, marker in {
|
||||||
"README.md": "# RustProxy 0.26.0",
|
"README.md": "# RustProxy 0.26.1",
|
||||||
"CHANGELOG.md": "## 0.26.0",
|
"CHANGELOG.md": "## 0.26.1",
|
||||||
"ROADMAP.md": "## 0.26.0",
|
"ROADMAP.md": "## 0.26.1",
|
||||||
"AGENTS.md": "Current source version: 0.26.0.",
|
"AGENTS.md": "Current source version: 0.26.1.",
|
||||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.0",
|
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.1",
|
||||||
}.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.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']:
|
for marker in ['"version": "0.26.1"', '"/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.26.0"' in openapi
|
assert '"RuntimeInformation"' in openapi and '"version": "0.26.1"' 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.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("PASS: 0.26.1 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.26.0'})
|
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.1'})
|
||||||
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.26.0 release."""
|
"""Rust-independent documentation/Reader consistency checks for the 0.26.1 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.26.0")
|
assert readme.startswith("# RustProxy 0.26.1")
|
||||||
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
|
assert "docs/WEB_SERVER.md" in readme and "docs/README.md" in readme
|
||||||
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
|
assert readme.count("## What changed in ") == 1, "README duplicates historical release notes; use CHANGELOG/archive"
|
||||||
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
|
assert "## Release history" in readme and "docs/ROADMAP_HISTORY.md" in readme
|
||||||
assert roadmap.startswith("# RustProxy roadmap") and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
|
assert roadmap.startswith("# RustProxy roadmap") and "## 0.26.1" in roadmap and "## 0.26.0" in roadmap and "## 0.25.5" in roadmap and "## 0.25.4" in roadmap and "## 0.25.3" in roadmap and "## 0.25.2" in roadmap and "## 0.25.1" in roadmap and "## 0.25.0" in roadmap
|
||||||
assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
|
assert "## 0.24." not in roadmap and "## Web Server follow-up" in roadmap and "docs/ROADMAP_HISTORY.md" in roadmap
|
||||||
assert changelog.startswith("# Changelog\n")
|
assert changelog.startswith("# Changelog\n")
|
||||||
for release in ["0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
|
for release in ["0.26.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]:
|
||||||
assert f"## {release}" in changelog, release
|
assert f"## {release}" in changelog, release
|
||||||
assert changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
|
assert changelog.index("## 0.26.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5")
|
||||||
assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog
|
assert "CHANGELOG_TECHNICAL_ARCHIVE.md" in changelog
|
||||||
for developer_only in ["Clean the remaining compiler warnings", "Update package/source/UI/OpenAPI/release identity", "source regressions and update release identity"]:
|
for developer_only in ["Clean the remaining compiler warnings", "Update package/source/UI/OpenAPI/release identity", "source regressions and update release identity"]:
|
||||||
assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}"
|
assert developer_only not in changelog, f"developer-only CHANGELOG detail: {developer_only}"
|
||||||
|
|||||||
@@ -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.26.0 source tree: missing {path}")
|
raise ValueError(f"Incomplete RustProxy 0.26.1 source tree: missing {path}")
|
||||||
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
text = (root / "src/lib.rs").read_text(encoding="utf-8")
|
||||||
if '#[path = "auth/mod.rs"]' not in text:
|
if '#[path = "auth/mod.rs"]' not in text:
|
||||||
raise ValueError("src/lib.rs is not the 0.26.0 entry point; extract the complete package, not individual files.")
|
raise ValueError("src/lib.rs is not the 0.26.1 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.26.0"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
assert 'version = "0.26.1"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||||
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
assert (target / "data/keep.txt").read_text(encoding="utf-8") == "preserve\n"
|
||||||
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
assert (target / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
||||||
assert (target / "langs/site.json").is_file()
|
assert (target / "langs/site.json").is_file()
|
||||||
@@ -134,7 +134,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
|||||||
assert (target / "docker-agent/certificate.go").is_file()
|
assert (target / "docker-agent/certificate.go").is_file()
|
||||||
assert (target / "docker-agent/server.go").is_file()
|
assert (target / "docker-agent/server.go").is_file()
|
||||||
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
|
manifest = json.loads((backup / "upgrade.json").read_text(encoding="utf-8"))
|
||||||
assert manifest["to_version"] == "0.26.0"
|
assert manifest["to_version"] == "0.26.1"
|
||||||
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.26.0"' in openapi_0241
|
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.26.1"' 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.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"']:
|
for marker in ['"version": "0.26.1"', '"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.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.")
|
print("PASS: 0.26.1 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.26.0"})
|
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.1"})
|
||||||
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.26.0-" + uuid.uuid4().hex[:12])
|
backup = target / ".upgrade-backup" / ("before-0.26.1-" + uuid.uuid4().hex[:12])
|
||||||
backup.mkdir(parents=True, mode=0o700)
|
backup.mkdir(parents=True, mode=0o700)
|
||||||
installed: list[str] = []
|
installed: list[str] = []
|
||||||
moved: list[str] = []
|
moved: list[str] = []
|
||||||
@@ -84,7 +84,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
|||||||
remove_legacy = legacy.exists()
|
remove_legacy = legacy.exists()
|
||||||
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
|
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
|
||||||
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
|
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
|
||||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.26.0", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.26.1", "managed_paths":MANAGED, "removed_obsolete_runtime_state": remove_legacy}, indent=2)+"\n", encoding="utf-8")
|
||||||
if remove_legacy:
|
if remove_legacy:
|
||||||
legacy.unlink()
|
legacy.unlink()
|
||||||
except BaseException:
|
except BaseException:
|
||||||
@@ -115,7 +115,7 @@ def main() -> None:
|
|||||||
backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir)
|
backup = upgrade(Path(__file__).resolve().parents[1], args.target, stopped=args.stopped, data_dir=data_dir)
|
||||||
except (OSError, ValueError) as error:
|
except (OSError, ValueError) as error:
|
||||||
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
|
parser.exit(2, f"Upgrade refused or rolled back: {error}\n")
|
||||||
print(f"Installed RustProxy 0.26.0. Previous managed source files: {backup}")
|
print(f"Installed RustProxy 0.26.1. 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.26.0</b>",
|
"RustProxy <b>0.26.1</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",
|
||||||
|
|||||||
@@ -399,6 +399,14 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
|||||||
"/docs/V0.25.1_AUDIT.md"
|
"/docs/V0.25.1_AUDIT.md"
|
||||||
)),
|
)),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"docs/V0.26.1_IMPLEMENTATION.md",
|
||||||
|
"technical",
|
||||||
|
include_str!(concat!(
|
||||||
|
env!("CARGO_MANIFEST_DIR"),
|
||||||
|
"/docs/V0.26.1_IMPLEMENTATION.md"
|
||||||
|
)),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"docs/V0.25.5_AUDIT.md",
|
"docs/V0.25.5_AUDIT.md",
|
||||||
"audits",
|
"audits",
|
||||||
|
|||||||
+73
-4
@@ -229,12 +229,62 @@ fn read_tail(
|
|||||||
Ok((lines, bytes, truncated || iter.next().is_some()))
|
Ok((lines, bytes, truncated || iter.next().is_some()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalized_record(source: &FileLogSource, line: &str) -> Value {
|
fn copy_mapped_field(
|
||||||
|
object: &mut serde_json::Map<String, Value>,
|
||||||
|
format: &crate::logging::JsonFormat,
|
||||||
|
canonical: &str,
|
||||||
|
) {
|
||||||
|
if object.contains_key(canonical) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mapped = format
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.find_map(|(output, source)| (source == canonical).then_some(output));
|
||||||
|
if let Some(value) = mapped.and_then(|output| object.get(output)).cloned() {
|
||||||
|
object.insert(canonical.to_owned(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalized_record(
|
||||||
|
source: &FileLogSource,
|
||||||
|
line: &str,
|
||||||
|
format: &crate::logging::JsonFormat,
|
||||||
|
) -> Value {
|
||||||
let mut value = serde_json::from_str::<Value>(line).unwrap_or_else(|_| json!({"raw": line}));
|
let mut value = serde_json::from_str::<Value>(line).unwrap_or_else(|_| json!({"raw": line}));
|
||||||
if !value.is_object() {
|
if !value.is_object() {
|
||||||
value = json!({"value": value});
|
value = json!({"value": value});
|
||||||
}
|
}
|
||||||
let object = value.as_object_mut().expect("record normalized to object");
|
let object = value.as_object_mut().expect("record normalized to object");
|
||||||
|
if source.kind == "access" {
|
||||||
|
for field in [
|
||||||
|
"event", "time_ms", "status", "client_ip", "vhost", "method", "host", "path",
|
||||||
|
"transport", "http_version", "protocol_error", "error_detail", "tls_sni", "reason",
|
||||||
|
"error", "sni", "authority", "matched_by",
|
||||||
|
] {
|
||||||
|
copy_mapped_field(object, format, field);
|
||||||
|
}
|
||||||
|
if !object.contains_key("client_ip") {
|
||||||
|
if let Some(value) = object.get("source_ip").cloned() {
|
||||||
|
object.insert("client_ip".into(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !object.contains_key("reason") {
|
||||||
|
if let Some(value) = object.get("protocol_error").cloned() {
|
||||||
|
object.insert("reason".into(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !object.contains_key("error") {
|
||||||
|
if let Some(value) = object.get("error_detail").cloned() {
|
||||||
|
object.insert("error".into(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !object.contains_key("sni") {
|
||||||
|
if let Some(value) = object.get("tls_sni").cloned() {
|
||||||
|
object.insert("sni".into(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
object.entry("event").or_insert_with(|| {
|
object.entry("event").or_insert_with(|| {
|
||||||
Value::String(
|
Value::String(
|
||||||
if source.kind == "error" {
|
if source.kind == "error" {
|
||||||
@@ -256,13 +306,28 @@ fn contains_ci(value: Option<&Value>, needle: &str) -> bool {
|
|||||||
.is_some_and(|text| text.to_ascii_lowercase().contains(needle))
|
.is_some_and(|text| text.to_ascii_lowercase().contains(needle))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn append_search_values(value: &Value, output: &mut String) {
|
||||||
|
match value {
|
||||||
|
Value::Null => {}
|
||||||
|
Value::Bool(value) => { output.push_str(if *value { " true" } else { " false" }); }
|
||||||
|
Value::Number(value) => { output.push(' '); output.push_str(&value.to_string()); }
|
||||||
|
Value::String(value) => { output.push(' '); output.push_str(&value.to_ascii_lowercase()); }
|
||||||
|
Value::Array(values) => values.iter().for_each(|value| append_search_values(value, output)),
|
||||||
|
Value::Object(values) => values.values().for_each(|value| append_search_values(value, output)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn record_matches(value: &Value, query: &FileLogQuery, q: &str) -> bool {
|
fn record_matches(value: &Value, query: &FileLogQuery, q: &str) -> bool {
|
||||||
let Some(object) = value.as_object() else {
|
let Some(object) = value.as_object() else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
if !q.is_empty() && !value.to_string().to_ascii_lowercase().contains(q) {
|
if !q.is_empty() {
|
||||||
|
let mut searchable = String::new();
|
||||||
|
append_search_values(value, &mut searchable);
|
||||||
|
if !q.split_whitespace().all(|term| searchable.contains(term)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if let Some(event) = query
|
if let Some(event) = query
|
||||||
.event
|
.event
|
||||||
.as_deref()
|
.as_deref()
|
||||||
@@ -345,7 +410,9 @@ async fn file_logs_query(
|
|||||||
let config = state.config.load().envelope.config.clone();
|
let config = state.config.load().envelope.config.clone();
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let mut sources = configured_file_log_sources(&config);
|
let mut sources = configured_file_log_sources(&config);
|
||||||
|
let log_format = config.logging.format.clone();
|
||||||
let requested = query.source.as_deref().unwrap_or("all");
|
let requested = query.source.as_deref().unwrap_or("all");
|
||||||
|
let mut truncated = false;
|
||||||
if requested != "all" {
|
if requested != "all" {
|
||||||
sources.retain(|source| source.id == requested);
|
sources.retain(|source| source.id == requested);
|
||||||
if sources.is_empty() {
|
if sources.is_empty() {
|
||||||
@@ -356,8 +423,11 @@ async fn file_logs_query(
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
sources.sort_by(|a, b| b.modified_ms.cmp(&a.modified_ms));
|
sources.sort_by(|a, b| b.modified_ms.cmp(&a.modified_ms));
|
||||||
|
if sources.len() > FILE_LOG_MAX_SOURCES {
|
||||||
|
truncated = true;
|
||||||
sources.truncate(FILE_LOG_MAX_SOURCES);
|
sources.truncate(FILE_LOG_MAX_SOURCES);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let q = query.q.clone().unwrap_or_default().to_ascii_lowercase();
|
let q = query.q.clone().unwrap_or_default().to_ascii_lowercase();
|
||||||
let offset = query.offset.unwrap_or(0);
|
let offset = query.offset.unwrap_or(0);
|
||||||
let limit = query.limit.unwrap_or(100).clamp(1, FILE_LOG_MAX_LIMIT);
|
let limit = query.limit.unwrap_or(100).clamp(1, FILE_LOG_MAX_LIMIT);
|
||||||
@@ -365,7 +435,6 @@ async fn file_logs_query(
|
|||||||
let mut scanned_bytes = 0u64;
|
let mut scanned_bytes = 0u64;
|
||||||
let mut remaining_lines = FILE_LOG_SCAN_LINES;
|
let mut remaining_lines = FILE_LOG_SCAN_LINES;
|
||||||
let mut scanned_lines = 0usize;
|
let mut scanned_lines = 0usize;
|
||||||
let mut truncated = false;
|
|
||||||
let mut records = Vec::new();
|
let mut records = Vec::new();
|
||||||
for (index, source) in sources.iter().enumerate() {
|
for (index, source) in sources.iter().enumerate() {
|
||||||
if remaining_bytes == 0 || remaining_lines == 0 {
|
if remaining_bytes == 0 || remaining_lines == 0 {
|
||||||
@@ -385,7 +454,7 @@ async fn file_logs_query(
|
|||||||
if line.is_empty() {
|
if line.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let value = normalized_record(source, &line);
|
let value = normalized_record(source, &line, &log_format);
|
||||||
if record_matches(&value, &query, &q) {
|
if record_matches(&value, &query, &q) {
|
||||||
records.push(value);
|
records.push(value);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
include!("router.rs");
|
include!("router.rs");
|
||||||
include!("access.rs");
|
include!("access.rs");
|
||||||
include!("session.rs");
|
include!("session.rs");
|
||||||
|
include!("preferences.rs");
|
||||||
include!("status.rs");
|
include!("status.rs");
|
||||||
include!("logging_api.rs");
|
include!("logging_api.rs");
|
||||||
include!("file_logs.rs");
|
include!("file_logs.rs");
|
||||||
|
|||||||
+33
-1
@@ -2,7 +2,7 @@
|
|||||||
"openapi": "3.0.3",
|
"openapi": "3.0.3",
|
||||||
"info": {
|
"info": {
|
||||||
"title": "RustProxy Administration API",
|
"title": "RustProxy Administration API",
|
||||||
"version": "0.26.0",
|
"version": "0.26.1",
|
||||||
"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": [
|
||||||
@@ -2909,6 +2909,38 @@
|
|||||||
},
|
},
|
||||||
"description": "Account-scoped Live Logs columns shown in the dashboard. JSON log export still contains every field present in each entry."
|
"description": "Account-scoped Live Logs columns shown in the dashboard. JSON log export still contains every field present in each entry."
|
||||||
},
|
},
|
||||||
|
"file_log_columns": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"maxItems": 11,
|
||||||
|
"uniqueItems": true,
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"time",
|
||||||
|
"event",
|
||||||
|
"status",
|
||||||
|
"client_ip",
|
||||||
|
"vhost",
|
||||||
|
"transport",
|
||||||
|
"matched_by",
|
||||||
|
"sni",
|
||||||
|
"authority",
|
||||||
|
"record",
|
||||||
|
"source"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"default": [
|
||||||
|
"time",
|
||||||
|
"event",
|
||||||
|
"status",
|
||||||
|
"client_ip",
|
||||||
|
"vhost",
|
||||||
|
"record",
|
||||||
|
"source"
|
||||||
|
],
|
||||||
|
"description": "Account-scoped Stored Logs explorer columns shown in the dashboard."
|
||||||
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
const LIVE_LOG_COLUMNS: &[&str] = &[
|
||||||
|
"time",
|
||||||
|
"request",
|
||||||
|
"host",
|
||||||
|
"source_ip",
|
||||||
|
"vhost",
|
||||||
|
"backend",
|
||||||
|
"status",
|
||||||
|
"duration",
|
||||||
|
"cache",
|
||||||
|
"outcome",
|
||||||
|
"transport",
|
||||||
|
"websocket",
|
||||||
|
"upstream_error",
|
||||||
|
];
|
||||||
|
const FILE_LOG_COLUMNS: &[&str] = &[
|
||||||
|
"time", "event", "status", "client_ip", "vhost", "transport", "matched_by", "sni",
|
||||||
|
"authority", "record", "source",
|
||||||
|
];
|
||||||
|
const BACKEND_TABLE_COLUMNS: &[&str] = &[
|
||||||
|
"vhost",
|
||||||
|
"backend",
|
||||||
|
"status",
|
||||||
|
"active",
|
||||||
|
"requests",
|
||||||
|
"errors",
|
||||||
|
"latency",
|
||||||
|
"traffic",
|
||||||
|
"generation",
|
||||||
|
"generationRequests",
|
||||||
|
"generationAge",
|
||||||
|
"rotations",
|
||||||
|
"lastFailure",
|
||||||
|
"lastRecovery",
|
||||||
|
];
|
||||||
|
|
||||||
|
fn default_admin_theme() -> String {
|
||||||
|
"dark".to_owned()
|
||||||
|
}
|
||||||
|
fn default_file_log_columns() -> Vec<String> {
|
||||||
|
["time", "event", "status", "client_ip", "vhost", "record", "source"]
|
||||||
|
.into_iter()
|
||||||
|
.map(str::to_owned)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
fn default_backend_table_columns() -> Vec<String> {
|
||||||
|
BACKEND_TABLE_COLUMNS
|
||||||
|
.iter()
|
||||||
|
.map(|value| (*value).to_owned())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
fn default_backend_table_sort() -> String {
|
||||||
|
"vhost".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
struct AdminPreferences {
|
||||||
|
live_log_columns: Vec<String>,
|
||||||
|
#[serde(default = "default_admin_theme")]
|
||||||
|
theme: String,
|
||||||
|
#[serde(default = "default_file_log_columns")]
|
||||||
|
file_log_columns: Vec<String>,
|
||||||
|
#[serde(default = "default_backend_table_columns")]
|
||||||
|
backend_table_columns: Vec<String>,
|
||||||
|
#[serde(default = "default_backend_table_sort")]
|
||||||
|
backend_table_sort: String,
|
||||||
|
#[serde(default)]
|
||||||
|
backend_table_descending: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for AdminPreferences {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
live_log_columns: LIVE_LOG_COLUMNS
|
||||||
|
.iter()
|
||||||
|
.map(|value| (*value).to_owned())
|
||||||
|
.collect(),
|
||||||
|
theme: default_admin_theme(),
|
||||||
|
file_log_columns: default_file_log_columns(),
|
||||||
|
backend_table_columns: default_backend_table_columns(),
|
||||||
|
backend_table_sort: default_backend_table_sort(),
|
||||||
|
backend_table_descending: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiError> {
|
||||||
|
if preferences.live_log_columns.is_empty()
|
||||||
|
|| preferences.live_log_columns.len() > LIVE_LOG_COLUMNS.len()
|
||||||
|
{
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"at least one live-log column must be visible".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
for column in &preferences.live_log_columns {
|
||||||
|
if !LIVE_LOG_COLUMNS.contains(&column.as_str()) || !seen.insert(column.as_str()) {
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"invalid live-log column preference".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if preferences.file_log_columns.is_empty()
|
||||||
|
|| preferences.file_log_columns.len() > FILE_LOG_COLUMNS.len()
|
||||||
|
{
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"at least one stored-log column must be visible".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
for column in &preferences.file_log_columns {
|
||||||
|
if !FILE_LOG_COLUMNS.contains(&column.as_str()) || !seen.insert(column.as_str()) {
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"invalid stored-log column preference".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matches!(preferences.theme.as_str(), "dark" | "light") {
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"invalid administration theme preference".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if preferences.backend_table_columns.is_empty()
|
||||||
|
|| preferences.backend_table_columns.len() > BACKEND_TABLE_COLUMNS.len()
|
||||||
|
{
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"at least one backend table column must be visible".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut seen = std::collections::HashSet::new();
|
||||||
|
for column in &preferences.backend_table_columns {
|
||||||
|
if !BACKEND_TABLE_COLUMNS.contains(&column.as_str()) || !seen.insert(column.as_str()) {
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"invalid backend table column preference".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !BACKEND_TABLE_COLUMNS.contains(&preferences.backend_table_sort.as_str()) {
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::UNPROCESSABLE_ENTITY,
|
||||||
|
"invalid backend table sort preference".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn preferences(
|
||||||
|
State(state): State<Arc<App>>,
|
||||||
|
Extension(session): Extension<Session>,
|
||||||
|
) -> Result<Json<AdminPreferences>, ApiError> {
|
||||||
|
let stored = state
|
||||||
|
.store
|
||||||
|
.admin_preferences(&session.username)
|
||||||
|
.await
|
||||||
|
.map_err(auth::internal)?;
|
||||||
|
let preferences = match stored {
|
||||||
|
Some(document) => match serde_json::from_str::<AdminPreferences>(&document) {
|
||||||
|
Ok(preferences) if validate_admin_preferences(&preferences).is_ok() => preferences,
|
||||||
|
Ok(_) | Err(_) => {
|
||||||
|
tracing::warn!(user = %session.username, "invalid admin view preferences ignored");
|
||||||
|
AdminPreferences::default()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => AdminPreferences::default(),
|
||||||
|
};
|
||||||
|
Ok(Json(preferences))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_preferences(
|
||||||
|
State(state): State<Arc<App>>,
|
||||||
|
Extension(session): Extension<Session>,
|
||||||
|
input: Result<Json<AdminPreferences>, axum::extract::rejection::JsonRejection>,
|
||||||
|
) -> Result<Json<AdminPreferences>, ApiError> {
|
||||||
|
let Json(preferences) = input.map_err(public::invalid_json)?;
|
||||||
|
validate_admin_preferences(&preferences)?;
|
||||||
|
let document = serde_json::to_string(&preferences).map_err(auth::internal)?;
|
||||||
|
if !state
|
||||||
|
.store
|
||||||
|
.save_admin_preferences(&session.username, &document)
|
||||||
|
.await
|
||||||
|
.map_err(auth::internal)?
|
||||||
|
{
|
||||||
|
return Err(ApiError(
|
||||||
|
StatusCode::UNAUTHORIZED,
|
||||||
|
"auth.invalid_credentials".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Json(preferences))
|
||||||
|
}
|
||||||
@@ -252,173 +252,6 @@ async fn session(
|
|||||||
Json(session_payload(&session, session_seconds))
|
Json(session_payload(&session, session_seconds))
|
||||||
}
|
}
|
||||||
|
|
||||||
const LIVE_LOG_COLUMNS: &[&str] = &[
|
|
||||||
"time",
|
|
||||||
"request",
|
|
||||||
"host",
|
|
||||||
"source_ip",
|
|
||||||
"vhost",
|
|
||||||
"backend",
|
|
||||||
"status",
|
|
||||||
"duration",
|
|
||||||
"cache",
|
|
||||||
"outcome",
|
|
||||||
"transport",
|
|
||||||
"websocket",
|
|
||||||
"upstream_error",
|
|
||||||
];
|
|
||||||
const BACKEND_TABLE_COLUMNS: &[&str] = &[
|
|
||||||
"vhost",
|
|
||||||
"backend",
|
|
||||||
"status",
|
|
||||||
"active",
|
|
||||||
"requests",
|
|
||||||
"errors",
|
|
||||||
"latency",
|
|
||||||
"traffic",
|
|
||||||
"generation",
|
|
||||||
"generationRequests",
|
|
||||||
"generationAge",
|
|
||||||
"rotations",
|
|
||||||
"lastFailure",
|
|
||||||
"lastRecovery",
|
|
||||||
];
|
|
||||||
|
|
||||||
fn default_admin_theme() -> String {
|
|
||||||
"dark".to_owned()
|
|
||||||
}
|
|
||||||
fn default_backend_table_columns() -> Vec<String> {
|
|
||||||
BACKEND_TABLE_COLUMNS
|
|
||||||
.iter()
|
|
||||||
.map(|value| (*value).to_owned())
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
fn default_backend_table_sort() -> String {
|
|
||||||
"vhost".to_owned()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(deny_unknown_fields)]
|
|
||||||
struct AdminPreferences {
|
|
||||||
live_log_columns: Vec<String>,
|
|
||||||
#[serde(default = "default_admin_theme")]
|
|
||||||
theme: String,
|
|
||||||
#[serde(default = "default_backend_table_columns")]
|
|
||||||
backend_table_columns: Vec<String>,
|
|
||||||
#[serde(default = "default_backend_table_sort")]
|
|
||||||
backend_table_sort: String,
|
|
||||||
#[serde(default)]
|
|
||||||
backend_table_descending: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for AdminPreferences {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
live_log_columns: LIVE_LOG_COLUMNS
|
|
||||||
.iter()
|
|
||||||
.map(|value| (*value).to_owned())
|
|
||||||
.collect(),
|
|
||||||
theme: default_admin_theme(),
|
|
||||||
backend_table_columns: default_backend_table_columns(),
|
|
||||||
backend_table_sort: default_backend_table_sort(),
|
|
||||||
backend_table_descending: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiError> {
|
|
||||||
if preferences.live_log_columns.is_empty()
|
|
||||||
|| preferences.live_log_columns.len() > LIVE_LOG_COLUMNS.len()
|
|
||||||
{
|
|
||||||
return Err(ApiError(
|
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
|
||||||
"at least one live-log column must be visible".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let mut seen = std::collections::HashSet::new();
|
|
||||||
for column in &preferences.live_log_columns {
|
|
||||||
if !LIVE_LOG_COLUMNS.contains(&column.as_str()) || !seen.insert(column.as_str()) {
|
|
||||||
return Err(ApiError(
|
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
|
||||||
"invalid live-log column preference".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !matches!(preferences.theme.as_str(), "dark" | "light") {
|
|
||||||
return Err(ApiError(
|
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
|
||||||
"invalid administration theme preference".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if preferences.backend_table_columns.is_empty()
|
|
||||||
|| preferences.backend_table_columns.len() > BACKEND_TABLE_COLUMNS.len()
|
|
||||||
{
|
|
||||||
return Err(ApiError(
|
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
|
||||||
"at least one backend table column must be visible".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let mut seen = std::collections::HashSet::new();
|
|
||||||
for column in &preferences.backend_table_columns {
|
|
||||||
if !BACKEND_TABLE_COLUMNS.contains(&column.as_str()) || !seen.insert(column.as_str()) {
|
|
||||||
return Err(ApiError(
|
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
|
||||||
"invalid backend table column preference".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !BACKEND_TABLE_COLUMNS.contains(&preferences.backend_table_sort.as_str()) {
|
|
||||||
return Err(ApiError(
|
|
||||||
StatusCode::UNPROCESSABLE_ENTITY,
|
|
||||||
"invalid backend table sort preference".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn preferences(
|
|
||||||
State(state): State<Arc<App>>,
|
|
||||||
Extension(session): Extension<Session>,
|
|
||||||
) -> Result<Json<AdminPreferences>, ApiError> {
|
|
||||||
let stored = state
|
|
||||||
.store
|
|
||||||
.admin_preferences(&session.username)
|
|
||||||
.await
|
|
||||||
.map_err(auth::internal)?;
|
|
||||||
let preferences = match stored {
|
|
||||||
Some(document) => match serde_json::from_str::<AdminPreferences>(&document) {
|
|
||||||
Ok(preferences) if validate_admin_preferences(&preferences).is_ok() => preferences,
|
|
||||||
Ok(_) | Err(_) => {
|
|
||||||
tracing::warn!(user = %session.username, "invalid admin view preferences ignored");
|
|
||||||
AdminPreferences::default()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => AdminPreferences::default(),
|
|
||||||
};
|
|
||||||
Ok(Json(preferences))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn update_preferences(
|
|
||||||
State(state): State<Arc<App>>,
|
|
||||||
Extension(session): Extension<Session>,
|
|
||||||
input: Result<Json<AdminPreferences>, axum::extract::rejection::JsonRejection>,
|
|
||||||
) -> Result<Json<AdminPreferences>, ApiError> {
|
|
||||||
let Json(preferences) = input.map_err(public::invalid_json)?;
|
|
||||||
validate_admin_preferences(&preferences)?;
|
|
||||||
let document = serde_json::to_string(&preferences).map_err(auth::internal)?;
|
|
||||||
if !state
|
|
||||||
.store
|
|
||||||
.save_admin_preferences(&session.username, &document)
|
|
||||||
.await
|
|
||||||
.map_err(auth::internal)?
|
|
||||||
{
|
|
||||||
return Err(ApiError(
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
"auth.invalid_credentials".into(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Ok(Json(preferences))
|
|
||||||
}
|
|
||||||
fn logged_out(secure_cookie: bool) -> Response {
|
fn logged_out(secure_cookie: bool) -> Response {
|
||||||
let mut response = Json(json!({"logged_out":true})).into_response();
|
let mut response = Json(json!({"logged_out":true})).into_response();
|
||||||
response.headers_mut().insert(
|
response.headers_mut().insert(
|
||||||
|
|||||||
@@ -20,6 +20,10 @@
|
|||||||
LIVE_LOG_COLUMNS.len()
|
LIVE_LOG_COLUMNS.len()
|
||||||
);
|
);
|
||||||
assert_eq!(initial["theme"], "dark");
|
assert_eq!(initial["theme"], "dark");
|
||||||
|
assert_eq!(
|
||||||
|
initial["file_log_columns"].as_array().unwrap().len(),
|
||||||
|
default_file_log_columns().len()
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
initial["backend_table_columns"].as_array().unwrap().len(),
|
initial["backend_table_columns"].as_array().unwrap().len(),
|
||||||
BACKEND_TABLE_COLUMNS.len()
|
BACKEND_TABLE_COLUMNS.len()
|
||||||
@@ -45,6 +49,7 @@
|
|||||||
let legacy = json_body(response).await;
|
let legacy = json_body(response).await;
|
||||||
assert_eq!(legacy["live_log_columns"], json!(["time", "status"]));
|
assert_eq!(legacy["live_log_columns"], json!(["time", "status"]));
|
||||||
assert_eq!(legacy["theme"], "dark");
|
assert_eq!(legacy["theme"], "dark");
|
||||||
|
assert_eq!(legacy["file_log_columns"], json!(default_file_log_columns()));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
legacy["backend_table_columns"].as_array().unwrap().len(),
|
legacy["backend_table_columns"].as_array().unwrap().len(),
|
||||||
BACKEND_TABLE_COLUMNS.len()
|
BACKEND_TABLE_COLUMNS.len()
|
||||||
@@ -55,6 +60,7 @@
|
|||||||
let selected = json!({
|
let selected = json!({
|
||||||
"live_log_columns":["time","request","status","upstream_error"],
|
"live_log_columns":["time","request","status","upstream_error"],
|
||||||
"theme":"light",
|
"theme":"light",
|
||||||
|
"file_log_columns":["time","event","client_ip","record"],
|
||||||
"backend_table_columns":["vhost","backend","status","requests","latency"],
|
"backend_table_columns":["vhost","backend","status","requests","latency"],
|
||||||
"backend_table_sort":"requests",
|
"backend_table_sort":"requests",
|
||||||
"backend_table_descending":true
|
"backend_table_descending":true
|
||||||
|
|||||||
@@ -55,6 +55,37 @@
|
|||||||
assert!(validate_file_log_query(&query).is_err());
|
assert!(validate_file_log_query(&query).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stored_log_normalizes_mapped_client_ip_and_searches_diagnostics() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let source = source_from_path(
|
||||||
|
"vhost:app".into(),
|
||||||
|
"app.jsonl".into(),
|
||||||
|
"access",
|
||||||
|
dir.path().join("app.jsonl"),
|
||||||
|
true,
|
||||||
|
"JSONL",
|
||||||
|
);
|
||||||
|
let mut format = crate::logging::JsonFormat::default();
|
||||||
|
format.fields.remove("client_ip");
|
||||||
|
format
|
||||||
|
.fields
|
||||||
|
.insert("remote_addr".into(), "client_ip".into());
|
||||||
|
let record = normalized_record(
|
||||||
|
&source,
|
||||||
|
r#"{"event":"protocol_error","remote_addr":"192.0.2.10","reason":"tls_handshake_error","error":"HTTP parser error","sni":"app.example.test","matched_by":"sni","vhost":"app"}"#,
|
||||||
|
&format,
|
||||||
|
);
|
||||||
|
assert_eq!(record["client_ip"], "192.0.2.10");
|
||||||
|
assert_eq!(record["sni"], "app.example.test");
|
||||||
|
|
||||||
|
let query = FileLogQuery::default();
|
||||||
|
assert!(record_matches(&record, &query, "error"));
|
||||||
|
assert!(record_matches(&record, &query, "http parser"));
|
||||||
|
assert!(record_matches(&record, &query, "app.example.test"));
|
||||||
|
assert!(!record_matches(&record, &query, "missing"));
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
#[test]
|
#[test]
|
||||||
fn stored_log_reader_refuses_symlink_targets() {
|
fn stored_log_reader_refuses_symlink_targets() {
|
||||||
|
|||||||
@@ -355,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; $("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 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.rustProxyFileLogs?.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.26.0";
|
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.1";
|
||||||
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();
|
||||||
}
|
}
|
||||||
|
|||||||
+95
-15
@@ -1,7 +1,28 @@
|
|||||||
(() => {
|
(() => {
|
||||||
let inventory = null, offset = 0, lastTotal = 0, busy = false;
|
let inventory = null, offset = 0, lastTotal = 0, busy = false;
|
||||||
|
let currentEntries = [], selectedIndex = null;
|
||||||
|
let preferencesLoaded = false, preferenceSaveRunning = false, preferenceSaveQueued = false;
|
||||||
|
let searchTimer = null;
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{ id: "time", label: "time", value: entry => entryTime(entry) },
|
||||||
|
{ id: "event", label: "fileLogsEvent", value: entry => entry.event || "—" },
|
||||||
|
{ id: "status", label: "status", value: entry => entryStatus(entry) },
|
||||||
|
{ id: "client_ip", label: "clientIp", value: entry => entry.client_ip || entry.source_ip || "—" },
|
||||||
|
{ id: "vhost", label: "vhost", value: entry => entry.vhost || "—" },
|
||||||
|
{ id: "transport", label: "logTransport", value: entry => entry.transport ? `${entry.transport}${entry.http_version ? ` / HTTP ${entry.http_version}` : ""}` : "—" },
|
||||||
|
{ id: "matched_by", label: "fileLogsMatchedBy", value: entry => entry.matched_by || "—" },
|
||||||
|
{ id: "sni", label: "fileLogsSni", value: entry => entry.sni || entry.tls_sni || "—" },
|
||||||
|
{ id: "authority", label: "fileLogsAuthority", value: entry => entry.authority || entry.host || "—" },
|
||||||
|
{ id: "record", label: "fileLogsRecord", value: entry => entrySummary(entry) },
|
||||||
|
{ id: "source", label: "fileLogsSource", value: entry => entry._source || "—" },
|
||||||
|
];
|
||||||
|
const defaultColumnIds = ["time", "event", "status", "client_ip", "vhost", "record", "source"];
|
||||||
|
let visibleColumns = new Set(defaultColumnIds);
|
||||||
|
|
||||||
const sourceSelect = () => $("file-log-source");
|
const sourceSelect = () => $("file-log-source");
|
||||||
const currentLimit = () => Math.max(1, Math.min(500, Number($("file-log-limit").value) || 100));
|
const currentLimit = () => Math.max(1, Math.min(500, Number($("file-log-limit").value) || 100));
|
||||||
|
|
||||||
function sourceOption(source) {
|
function sourceOption(source) {
|
||||||
const label = `${source.label} · ${source.format} · ${bytes(source.size || 0)}${source.enabled ? ` · ${t("enabled")}` : ""}`;
|
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;
|
const option = el("option", label); option.value = source.id; return option;
|
||||||
@@ -26,8 +47,9 @@
|
|||||||
return "—";
|
return "—";
|
||||||
}
|
}
|
||||||
function entrySummary(entry) {
|
function entrySummary(entry) {
|
||||||
if (entry.protocol_error) return entry.protocol_error;
|
if (entry.reason || entry.protocol_error) return entry.reason || entry.protocol_error;
|
||||||
if (entry.method || entry.path) return `${entry.method || ""} ${entry.host || ""}${entry.path || ""}`.trim();
|
if (entry.method || entry.path) return `${entry.method || ""} ${entry.host || entry.authority || ""}${entry.path || ""}`.trim();
|
||||||
|
if (entry.error || entry.error_detail) return entry.error || entry.error_detail;
|
||||||
if (entry.message) return entry.message;
|
if (entry.message) return entry.message;
|
||||||
if (entry.raw) return entry.raw;
|
if (entry.raw) return entry.raw;
|
||||||
return entry.outcome || "—";
|
return entry.outcome || "—";
|
||||||
@@ -36,19 +58,72 @@
|
|||||||
const status = Number(entry.status) || 0;
|
const status = Number(entry.status) || 0;
|
||||||
return status ? String(status) : "—";
|
return status ? String(status) : "—";
|
||||||
}
|
}
|
||||||
function renderRows(entries) {
|
function syncHeaders() {
|
||||||
const rows = entries.map(entry => {
|
document.querySelectorAll("#file-logs-table-head [data-file-log-column]").forEach(header => {
|
||||||
const row = el("tr"); row.tabIndex = 0;
|
header.hidden = !visibleColumns.has(header.dataset.fileLogColumn);
|
||||||
for (const value of [entryTime(entry), entry.event || "—", entryStatus(entry), entry.client_ip || "—", entry.vhost || "—", entrySummary(entry), entry._source || "—"]) {
|
});
|
||||||
|
}
|
||||||
|
function syncColumnPicker() {
|
||||||
|
const picker = $("file-log-column-options");
|
||||||
|
picker.replaceChildren(...columns.map(column => {
|
||||||
|
const label = el("label", undefined, "check log-column-option");
|
||||||
|
const input = document.createElement("input"); input.type = "checkbox"; input.checked = visibleColumns.has(column.id); input.dataset.column = column.id;
|
||||||
|
const text = el("span", t(column.label)); text.dataset.i18n = column.label;
|
||||||
|
input.addEventListener("change", () => {
|
||||||
|
if (!input.checked && visibleColumns.size === 1) { input.checked = true; toast(t("fileLogsColumnsRequired"), true); return; }
|
||||||
|
if (input.checked) visibleColumns.add(column.id); else visibleColumns.delete(column.id);
|
||||||
|
syncHeaders(); renderRows(); void savePreferences();
|
||||||
|
});
|
||||||
|
label.append(input, text); return label;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
function applyPreferences(preferences) {
|
||||||
|
const selected = Array.isArray(preferences?.file_log_columns)
|
||||||
|
? preferences.file_log_columns.filter(id => columns.some(column => column.id === id))
|
||||||
|
: [];
|
||||||
|
visibleColumns = new Set(selected.length ? selected : defaultColumnIds);
|
||||||
|
preferencesLoaded = true;
|
||||||
|
syncColumnPicker(); syncHeaders(); renderRows();
|
||||||
|
}
|
||||||
|
async function savePreferences() {
|
||||||
|
if (!authenticated || !preferencesLoaded) return;
|
||||||
|
if (preferenceSaveRunning) { preferenceSaveQueued = true; return; }
|
||||||
|
preferenceSaveRunning = true;
|
||||||
|
try {
|
||||||
|
do {
|
||||||
|
preferenceSaveQueued = false;
|
||||||
|
const selected = columns.filter(column => visibleColumns.has(column.id)).map(column => column.id);
|
||||||
|
const preferences = await window.rustProxyPreferences.update({ file_log_columns: selected });
|
||||||
|
if (!preferenceSaveQueued) applyPreferences(preferences);
|
||||||
|
} while (preferenceSaveQueued);
|
||||||
|
} catch (error) { toast(error.message, true); }
|
||||||
|
finally { preferenceSaveRunning = false; }
|
||||||
|
}
|
||||||
|
function renderRows(entries = currentEntries) {
|
||||||
|
currentEntries = entries;
|
||||||
|
const nodes = [];
|
||||||
|
entries.forEach((entry, index) => {
|
||||||
|
const row = el("tr"); row.tabIndex = 0; row.classList.toggle("selected", selectedIndex === index);
|
||||||
|
row.setAttribute("aria-expanded", selectedIndex === index ? "true" : "false");
|
||||||
|
for (const column of columns) {
|
||||||
|
if (!visibleColumns.has(column.id)) continue;
|
||||||
|
const value = column.value(entry);
|
||||||
const cell = el("td", String(value)); cell.title = String(value); row.append(cell);
|
const cell = el("td", String(value)); cell.title = String(value); row.append(cell);
|
||||||
}
|
}
|
||||||
const show = () => { $("file-log-detail").textContent = JSON.stringify(entry, null, 2); };
|
const select = () => { selectedIndex = selectedIndex === index ? null : index; renderRows(); };
|
||||||
row.addEventListener("click", show); row.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); show(); } });
|
row.addEventListener("click", select);
|
||||||
return row;
|
row.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); select(); } });
|
||||||
|
nodes.push(row);
|
||||||
|
if (selectedIndex === index) {
|
||||||
|
const detailRow = el("tr", undefined, "file-log-detail-row");
|
||||||
|
const cell = el("td"); cell.colSpan = Math.max(1, visibleColumns.size);
|
||||||
|
const heading = el("strong", t("fileLogsDetails")); heading.dataset.i18n = "fileLogsDetails";
|
||||||
|
const pre = el("pre", JSON.stringify(entry, null, 2), "metrics-example");
|
||||||
|
const detail = el("div", undefined, "file-log-inline-detail"); detail.append(heading, pre); cell.append(detail); detailRow.append(cell); nodes.push(detailRow);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$("file-log-rows").replaceChildren(...rows);
|
$("file-log-rows").replaceChildren(...nodes);
|
||||||
$("file-logs-empty").hidden = rows.length !== 0;
|
$("file-logs-empty").hidden = entries.length !== 0;
|
||||||
if (!rows.length) $("file-log-detail").textContent = t("fileLogsSelectRecord");
|
|
||||||
}
|
}
|
||||||
function queryString() {
|
function queryString() {
|
||||||
const params = new URLSearchParams();
|
const params = new URLSearchParams();
|
||||||
@@ -66,7 +141,8 @@
|
|||||||
try {
|
try {
|
||||||
if (!inventory) await loadInventory();
|
if (!inventory) await loadInventory();
|
||||||
const value = await api(`/api/file-logs/query?${queryString()}`);
|
const value = await api(`/api/file-logs/query?${queryString()}`);
|
||||||
lastTotal = Number(value.total) || 0; renderRows(Array.isArray(value.entries) ? value.entries : []);
|
lastTotal = Number(value.total) || 0; selectedIndex = null;
|
||||||
|
renderRows(Array.isArray(value.entries) ? value.entries : []);
|
||||||
const from = lastTotal ? offset + 1 : 0, to = Math.min(offset + currentLimit(), lastTotal);
|
const from = lastTotal ? offset + 1 : 0, to = Math.min(offset + currentLimit(), lastTotal);
|
||||||
$("file-log-summary").textContent = t("fileLogsSummary")
|
$("file-log-summary").textContent = t("fileLogsSummary")
|
||||||
.replace("{from}", fmt(from)).replace("{to}", fmt(to)).replace("{total}", fmt(lastTotal))
|
.replace("{from}", fmt(from)).replace("{to}", fmt(to)).replace("{total}", fmt(lastTotal))
|
||||||
@@ -78,14 +154,18 @@
|
|||||||
finally { busy = false; $("file-logs-refresh").disabled = false; }
|
finally { busy = false; $("file-logs-refresh").disabled = false; }
|
||||||
}
|
}
|
||||||
function reset() {
|
function reset() {
|
||||||
inventory = null; offset = 0; lastTotal = 0; $("file-log-rows").replaceChildren(); $("file-log-detail").textContent = "";
|
inventory = null; offset = 0; lastTotal = 0; currentEntries = []; selectedIndex = null;
|
||||||
|
preferencesLoaded = false; preferenceSaveRunning = false; preferenceSaveQueued = false;
|
||||||
|
visibleColumns = new Set(defaultColumnIds); $("file-log-rows").replaceChildren(); syncColumnPicker(); syncHeaders();
|
||||||
}
|
}
|
||||||
$("file-logs-open").addEventListener("click", () => setPanel("filelogs"));
|
$("file-logs-open").addEventListener("click", () => setPanel("filelogs"));
|
||||||
$("settings-file-logs-open").addEventListener("click", () => setPanel("filelogs"));
|
$("settings-file-logs-open").addEventListener("click", () => setPanel("filelogs"));
|
||||||
$("file-logs-refresh").addEventListener("click", () => refresh(true));
|
$("file-logs-refresh").addEventListener("click", () => refresh(true));
|
||||||
$("file-logs-filter").addEventListener("submit", event => { event.preventDefault(); void refresh(true); });
|
$("file-logs-filter").addEventListener("submit", event => { event.preventDefault(); void refresh(true); });
|
||||||
|
$("file-log-q").addEventListener("input", () => { clearTimeout(searchTimer); searchTimer = setTimeout(() => void refresh(true), 250); });
|
||||||
$("file-logs-prev").addEventListener("click", () => { offset = Math.max(0, offset - currentLimit()); void refresh(); });
|
$("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(); } });
|
$("file-logs-next").addEventListener("click", () => { if (offset + currentLimit() < lastTotal) { offset += currentLimit(); void refresh(); } });
|
||||||
sourceSelect().addEventListener("change", () => refresh(true));
|
sourceSelect().addEventListener("change", () => refresh(true));
|
||||||
window.rustProxyFileLogs = { refresh: () => refresh(false), reset, inventory: loadInventory };
|
syncColumnPicker(); syncHeaders();
|
||||||
|
window.rustProxyFileLogs = { refresh: () => refresh(false), reset, inventory: loadInventory, applyPreferences };
|
||||||
})();
|
})();
|
||||||
+22
-15
@@ -1539,20 +1539,31 @@
|
|||||||
<option>500</option>
|
<option>500</option>
|
||||||
</select></label>
|
</select></label>
|
||||||
<button class="primary" data-i18n="applyFilters" type="submit">Apply filters</button>
|
<button class="primary" data-i18n="applyFilters" type="submit">Apply filters</button>
|
||||||
|
<details class="log-column-picker file-log-column-picker">
|
||||||
|
<summary data-i18n="logColumns">Columns</summary>
|
||||||
|
<div class="log-column-options" id="file-log-column-options"></div>
|
||||||
|
</details>
|
||||||
</form>
|
</form>
|
||||||
|
<details class="file-log-inventory-details">
|
||||||
|
<summary data-i18n="fileLogsSourcesRead">Configured files</summary>
|
||||||
<p class="fine file-log-inventory" id="file-log-inventory"></p>
|
<p class="fine file-log-inventory" id="file-log-inventory"></p>
|
||||||
|
</details>
|
||||||
<p class="fine" id="file-log-summary" role="status"></p>
|
<p class="fine" id="file-log-summary" role="status"></p>
|
||||||
<div class="table-scroll file-logs-table">
|
<div class="table-scroll file-logs-table">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead id="file-logs-table-head">
|
||||||
<tr>
|
<tr>
|
||||||
<th data-i18n="time">Time</th>
|
<th data-file-log-column="time" data-i18n="time">Time</th>
|
||||||
<th data-i18n="fileLogsEvent">Event</th>
|
<th data-file-log-column="event" data-i18n="fileLogsEvent">Event</th>
|
||||||
<th data-i18n="status">Status</th>
|
<th data-file-log-column="status" data-i18n="status">Status</th>
|
||||||
<th data-i18n="clientIp">Client IP</th>
|
<th data-file-log-column="client_ip" data-i18n="clientIp">Client IP</th>
|
||||||
<th data-i18n="vhost">Vhost</th>
|
<th data-file-log-column="vhost" data-i18n="vhost">Vhost</th>
|
||||||
<th data-i18n="fileLogsRecord">Request / reason</th>
|
<th data-file-log-column="transport" data-i18n="logTransport" hidden>Transport</th>
|
||||||
<th data-i18n="fileLogsSource">Source</th>
|
<th data-file-log-column="matched_by" data-i18n="fileLogsMatchedBy" hidden>Matched by</th>
|
||||||
|
<th data-file-log-column="sni" data-i18n="fileLogsSni" hidden>SNI</th>
|
||||||
|
<th data-file-log-column="authority" data-i18n="fileLogsAuthority" hidden>Authority</th>
|
||||||
|
<th data-file-log-column="record" data-i18n="fileLogsRecord">Request / reason</th>
|
||||||
|
<th data-file-log-column="source" data-i18n="fileLogsSource">Source</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="file-log-rows"></tbody>
|
<tbody id="file-log-rows"></tbody>
|
||||||
@@ -1563,10 +1574,6 @@
|
|||||||
<div class="file-logs-pager"><button class="quiet" data-i18n="previous" disabled
|
<div class="file-logs-pager"><button class="quiet" data-i18n="previous" disabled
|
||||||
id="file-logs-prev" type="button">Previous</button><button class="quiet"
|
id="file-logs-prev" type="button">Previous</button><button class="quiet"
|
||||||
data-i18n="next" disabled id="file-logs-next" type="button">Next</button></div>
|
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>
|
</article>
|
||||||
</section>
|
</section>
|
||||||
<section class="panel" hidden="" id="panel-settings">
|
<section class="panel" hidden="" id="panel-settings">
|
||||||
@@ -1742,7 +1749,7 @@
|
|||||||
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
|
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
|
||||||
</select>
|
</select>
|
||||||
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
|
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
|
||||||
id="server-value" maxlength="256" placeholder="RustProxy/v0.26.0"
|
id="server-value" maxlength="256" placeholder="RustProxy/v0.26.1"
|
||||||
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">
|
||||||
@@ -2541,7 +2548,7 @@
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<footer class="page-footer"><span>RustProxy <b>0.26.0</b></span><span id="footer-status">API - /
|
<footer class="page-footer"><span>RustProxy <b>0.26.1</b></span><span id="footer-status">API - /
|
||||||
dropped: 0</span></footer>
|
dropped: 0</span></footer>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -3252,7 +3259,7 @@ max_execution_time = 30</code></pre>
|
|||||||
<article class="card about-card">
|
<article class="card about-card">
|
||||||
<div class="about-product">
|
<div class="about-product">
|
||||||
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
|
<span aria-hidden="true" class="brand-mark">r<span>/</span>p</span>
|
||||||
<div><span class="small-label">RustProxy</span><strong>0.26.0</strong></div>
|
<div><span class="small-label">RustProxy</span><strong>0.26.1</strong></div>
|
||||||
</div>
|
</div>
|
||||||
<dl class="about-meta">
|
<dl class="about-meta">
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
+49
-11
@@ -8207,7 +8207,7 @@ button.config-history-day.is-selected {
|
|||||||
|
|
||||||
.file-logs-filter {
|
.file-logs-filter {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(7, minmax(120px, 1fr));
|
grid-template-columns: repeat(8, minmax(120px, 1fr));
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: end;
|
align-items: end;
|
||||||
}
|
}
|
||||||
@@ -8220,14 +8220,31 @@ button.config-history-day.is-selected {
|
|||||||
grid-column: span 2;
|
grid-column: span 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-logs-table {
|
.file-log-column-picker {
|
||||||
max-height: 52vh;
|
align-self: end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-logs-table tbody tr {
|
.file-logs-table {
|
||||||
|
max-height: 62vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-table thead th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 4;
|
||||||
|
background: var(--panel);
|
||||||
|
box-shadow: 0 1px 0 var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-logs-table tbody tr:not(.file-log-detail-row) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-logs-table tbody tr.selected > td {
|
||||||
|
background: var(--panel2);
|
||||||
|
box-shadow: inset 3px 0 0 var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
.file-logs-table td {
|
.file-logs-table td {
|
||||||
max-width: 340px;
|
max-width: 340px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -8235,19 +8252,33 @@ button.config-history-day.is-selected {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-log-detail-row td {
|
||||||
|
max-width: none;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--panel2);
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-log-inline-detail {
|
||||||
|
padding: 12px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-log-inline-detail pre {
|
||||||
|
max-height: 32vh;
|
||||||
|
margin: 8px 0 0;
|
||||||
|
overflow: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
.file-logs-pager {
|
.file-logs-pager {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-log-detail-card pre {
|
|
||||||
max-height: 32vh;
|
|
||||||
overflow: auto;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.file-logs-launcher {
|
.file-logs-launcher {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -8255,7 +8286,14 @@ button.config-history-day.is-selected {
|
|||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.file-log-inventory-details > summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.86rem;
|
||||||
|
}
|
||||||
|
|
||||||
.file-log-inventory {
|
.file-log-inventory {
|
||||||
|
margin: 8px 0 0;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -238,6 +238,12 @@ pub struct CompiledFrontend {
|
|||||||
pub pages: crate::pages::VhostPages,
|
pub pages: crate::pages::VhostPages,
|
||||||
pub proxy_timeouts: crate::vhost::ProxyTimeouts,
|
pub proxy_timeouts: crate::vhost::ProxyTimeouts,
|
||||||
}
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ResolvedLogVhost {
|
||||||
|
pub vhost: String,
|
||||||
|
pub matched_by: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Snapshot {
|
pub struct Snapshot {
|
||||||
pub vhost_metrics: std::collections::BTreeMap<String, Arc<crate::metrics::VhostMetrics>>,
|
pub vhost_metrics: std::collections::BTreeMap<String, Arc<crate::metrics::VhostMetrics>>,
|
||||||
pub forwarding: crate::forwarding::Compiled,
|
pub forwarding: crate::forwarding::Compiled,
|
||||||
|
|||||||
@@ -93,6 +93,54 @@ impl Snapshot {
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve a persistent protocol-error record to a configured vhost using
|
||||||
|
/// the same exact/wildcard precedence and listener binding rules as normal routing.
|
||||||
|
/// Client-provided Host/SNI values are never used as file names directly.
|
||||||
|
pub fn resolve_logging_vhost(
|
||||||
|
&self,
|
||||||
|
local: SocketAddr,
|
||||||
|
tls: bool,
|
||||||
|
routed_vhost: Option<&str>,
|
||||||
|
authority: Option<&str>,
|
||||||
|
host: Option<&str>,
|
||||||
|
sni: Option<&str>,
|
||||||
|
) -> Option<ResolvedLogVhost> {
|
||||||
|
if let Some(routed) = routed_vhost.filter(|value| !value.is_empty()) {
|
||||||
|
if self.frontends.values().flatten().any(|frontend| {
|
||||||
|
frontend.id == routed
|
||||||
|
&& frontend
|
||||||
|
.bindings
|
||||||
|
.iter()
|
||||||
|
.any(|binding| binding.matches(local, tls))
|
||||||
|
}) {
|
||||||
|
return Some(ResolvedLogVhost {
|
||||||
|
vhost: routed.to_owned(),
|
||||||
|
matched_by: "router",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (candidate, matched_by) in [
|
||||||
|
(authority, "authority"),
|
||||||
|
(host, "host"),
|
||||||
|
(sni, "sni"),
|
||||||
|
] {
|
||||||
|
let Some(candidate) = candidate else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(normalized) = request_host(candidate) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if let Some(frontend) = self.frontend(&normalized, local, tls) {
|
||||||
|
return Some(ResolvedLogVhost {
|
||||||
|
vhost: frontend.id.clone(),
|
||||||
|
matched_by,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
/// Select a redirect host/rule using the accepted socket, TLS state and normalized path.
|
/// Select a redirect host/rule using the accepted socket, TLS state and normalized path.
|
||||||
/// Exact hostnames win over the longest matching wildcard, matching vhost behavior.
|
/// Exact hostnames win over the longest matching wildcard, matching vhost behavior.
|
||||||
pub fn redirect_on(
|
pub fn redirect_on(
|
||||||
|
|||||||
@@ -187,6 +187,57 @@ fn wildcard_host_patterns_are_normalized_and_exact_hosts_win() {
|
|||||||
"wildcard/main"
|
"wildcard/main"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
#[test]
|
||||||
|
fn logging_vhost_resolution_reuses_router_host_precedence() {
|
||||||
|
let mut config = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||||
|
config.vhosts[0].id = "wildcard".into();
|
||||||
|
config.vhosts[0].frontend.hosts = vec!["*.example.test".into()];
|
||||||
|
let mut exact = config.vhosts[0].clone();
|
||||||
|
exact.id = "exact".into();
|
||||||
|
exact.frontend.hosts = vec!["api.example.test".into()];
|
||||||
|
config.vhosts.push(exact);
|
||||||
|
let snapshot = compile(config, 1, &policy()).unwrap();
|
||||||
|
let local: SocketAddr = "127.0.0.1:8080".parse().unwrap();
|
||||||
|
|
||||||
|
let routed = snapshot
|
||||||
|
.resolve_logging_vhost(
|
||||||
|
local,
|
||||||
|
false,
|
||||||
|
Some("wildcard"),
|
||||||
|
Some("api.example.test"),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(routed.vhost, "wildcard");
|
||||||
|
assert_eq!(routed.matched_by, "router");
|
||||||
|
|
||||||
|
let exact = snapshot
|
||||||
|
.resolve_logging_vhost(local, false, None, Some("API.EXAMPLE.TEST:8080"), None, None)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(exact.vhost, "exact");
|
||||||
|
assert_eq!(exact.matched_by, "authority");
|
||||||
|
|
||||||
|
let wildcard = snapshot
|
||||||
|
.resolve_logging_vhost(local, false, None, None, Some("www.example.test:8080"), None)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(wildcard.vhost, "wildcard");
|
||||||
|
assert_eq!(wildcard.matched_by, "host");
|
||||||
|
|
||||||
|
let sni = snapshot
|
||||||
|
.resolve_logging_vhost(local, false, None, None, None, Some("api.example.test"))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(sni.vhost, "exact");
|
||||||
|
assert_eq!(sni.matched_by, "sni");
|
||||||
|
|
||||||
|
assert!(snapshot
|
||||||
|
.resolve_logging_vhost(local, false, None, None, None, Some("unknown.test"))
|
||||||
|
.is_none());
|
||||||
|
assert!(snapshot
|
||||||
|
.resolve_logging_vhost(local, false, None, None, None, None)
|
||||||
|
.is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reusable_acl_profile_reference_is_compiled_and_unknown_reference_fails() {
|
fn reusable_acl_profile_reference_is_compiled_and_unknown_reference_fails() {
|
||||||
let mut c = demo_config("127.0.0.1:9000".parse().unwrap());
|
let mut c = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||||
|
|||||||
+75
-16
@@ -28,7 +28,6 @@ use tokio::{
|
|||||||
task::JoinSet,
|
task::JoinSet,
|
||||||
};
|
};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[serde(default, deny_unknown_fields)]
|
#[serde(default, deny_unknown_fields)]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
@@ -191,6 +190,63 @@ impl Drop for Lease {
|
|||||||
.fetch_sub(1, Ordering::Relaxed);
|
.fetch_sub(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn log_protocol_error(
|
||||||
|
state: &App,
|
||||||
|
kind: &'static str,
|
||||||
|
detail: impl std::fmt::Display,
|
||||||
|
peer: SocketAddr,
|
||||||
|
local: SocketAddr,
|
||||||
|
status: u16,
|
||||||
|
authority: Option<String>,
|
||||||
|
host: Option<String>,
|
||||||
|
sni: Option<String>,
|
||||||
|
) {
|
||||||
|
let resolved = state.config.load().resolve_logging_vhost(
|
||||||
|
local,
|
||||||
|
true,
|
||||||
|
None,
|
||||||
|
authority.as_deref(),
|
||||||
|
host.as_deref(),
|
||||||
|
sni.as_deref(),
|
||||||
|
);
|
||||||
|
let (vhost, matched_by) = resolved
|
||||||
|
.map(|resolved| (Some(resolved.vhost), resolved.matched_by))
|
||||||
|
.unwrap_or((None, "none"));
|
||||||
|
state.logs.push_protocol_error_attributed(
|
||||||
|
kind,
|
||||||
|
detail,
|
||||||
|
peer,
|
||||||
|
local,
|
||||||
|
"quic",
|
||||||
|
"3",
|
||||||
|
status,
|
||||||
|
sni,
|
||||||
|
authority.or(host),
|
||||||
|
vhost,
|
||||||
|
matched_by,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fn request_identity<B>(request: &http::Request<B>) -> (Option<String>, Option<String>) {
|
||||||
|
let authority = request
|
||||||
|
.uri()
|
||||||
|
.authority()
|
||||||
|
.map(|value| value.as_str().to_owned());
|
||||||
|
let host = request
|
||||||
|
.headers()
|
||||||
|
.get(http::header::HOST)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.map(str::to_owned);
|
||||||
|
(authority, host)
|
||||||
|
}
|
||||||
|
fn connection_sni(connection: &quinn::Connection) -> Option<String> {
|
||||||
|
connection
|
||||||
|
.handshake_data()?
|
||||||
|
.downcast::<quinn::crypto::rustls::HandshakeData>()
|
||||||
|
.ok()?
|
||||||
|
.server_name
|
||||||
|
.and_then(|name| crate::config::request_host(&name).ok())
|
||||||
|
}
|
||||||
pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duration) -> Result<()> {
|
pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duration) -> Result<()> {
|
||||||
let slots = Arc::new(Semaphore::new(
|
let slots = Arc::new(Semaphore::new(
|
||||||
bound.settings.max_concurrent_connections as usize,
|
bound.settings.max_concurrent_connections as usize,
|
||||||
@@ -258,15 +314,16 @@ pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duratio
|
|||||||
None if !address.ip().is_unspecified()=>address,
|
None if !address.ip().is_unspecified()=>address,
|
||||||
None=>{connection.close(0x102u32.into(),b"local destination unavailable");return;},
|
None=>{connection.close(0x102u32.into(),b"local destination unavailable");return;},
|
||||||
};
|
};
|
||||||
|
let tls_sni=connection_sni(&connection);
|
||||||
state.metrics.connections.fetch_add(1,Ordering::Relaxed);
|
state.metrics.connections.fetch_add(1,Ordering::Relaxed);
|
||||||
state.metrics.active_connections.fetch_add(1,Ordering::Relaxed);
|
state.metrics.active_connections.fetch_add(1,Ordering::Relaxed);
|
||||||
state.metrics.http3_connections.fetch_add(1,Ordering::Relaxed);
|
state.metrics.http3_connections.fetch_add(1,Ordering::Relaxed);
|
||||||
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.clone(),limit,grace).await {
|
if let Err(error)=connection_loop(connection,local,router,state.clone(),limit,grace,tls_sni.clone()).await {
|
||||||
state.logs.push_protocol_error(
|
log_protocol_error(
|
||||||
"http3_connection_error", &error, peer, local, "quic", "3", 0, None,
|
&state, "http3_connection_error", &error, peer, local, 0, None, None, tls_sni,
|
||||||
);
|
);
|
||||||
tracing::debug!(%error,"HTTP/3 connection ended");
|
tracing::debug!(%error,"HTTP/3 connection ended");
|
||||||
}
|
}
|
||||||
@@ -290,6 +347,7 @@ async fn connection_loop(
|
|||||||
state: Arc<App>,
|
state: Arc<App>,
|
||||||
limit: usize,
|
limit: usize,
|
||||||
grace: Duration,
|
grace: Duration,
|
||||||
|
tls_sni: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let mut builder = h3::server::builder();
|
let mut builder = h3::server::builder();
|
||||||
builder.max_field_section_size(32 * 1024);
|
builder.max_field_section_size(32 * 1024);
|
||||||
@@ -316,27 +374,27 @@ async fn connection_loop(
|
|||||||
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 {
|
if requests.len()>=limit {
|
||||||
state.logs.push_protocol_error(
|
log_protocol_error(
|
||||||
"http3_stream_limit_rejected", "HTTP/3 concurrent request stream limit reached",
|
&state, "http3_stream_limit_rejected", "HTTP/3 concurrent request stream limit reached",
|
||||||
connection.remote_address(), local, "quic", "3", 0, None,
|
connection.remote_address(), local, 0, None, None, tls_sni.clone(),
|
||||||
);
|
);
|
||||||
drop(resolver);continue;
|
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();let tls_sni=tls_sni.clone();
|
||||||
requests.spawn(async move {
|
requests.spawn(async move {
|
||||||
let resolved=tokio::time::timeout(state.header_timeout,resolver.resolve_request()).await;
|
let resolved=tokio::time::timeout(state.header_timeout,resolver.resolve_request()).await;
|
||||||
let (mut request,stream)=match resolved {
|
let (mut request,stream)=match resolved {
|
||||||
Ok(Ok(request))=>request,
|
Ok(Ok(request))=>request,
|
||||||
Ok(Err(error))=>{
|
Ok(Err(error))=>{
|
||||||
state.logs.push_protocol_error(
|
log_protocol_error(
|
||||||
"http3_request_error", error, peer, local, "quic", "3", 400, None,
|
&state, "http3_request_error", error, peer, local, 400, None, None, tls_sni.clone(),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
Err(_)=>{
|
Err(_)=>{
|
||||||
state.logs.push_protocol_error(
|
log_protocol_error(
|
||||||
"http3_header_timeout", "HTTP/3 request header timeout",
|
&state, "http3_header_timeout", "HTTP/3 request header timeout",
|
||||||
peer, local, "quic", "3", 408, None,
|
peer, local, 408, None, None, tls_sni.clone(),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
@@ -347,9 +405,10 @@ async fn connection_loop(
|
|||||||
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(
|
let (authority, host) = request_identity(&request);
|
||||||
"http3_invalid_headers", "HTTP/3 request contains forbidden connection-specific headers",
|
log_protocol_error(
|
||||||
peer, local, "quic", "3", 400, None,
|
&state, "http3_invalid_headers", "HTTP/3 request contains forbidden connection-specific headers",
|
||||||
|
peer, local, 400, authority, host, tls_sni.clone(),
|
||||||
);
|
);
|
||||||
send.stop_stream(Code::H3_MESSAGE_ERROR);return;
|
send.stop_stream(Code::H3_MESSAGE_ERROR);return;
|
||||||
}
|
}
|
||||||
|
|||||||
+74
-2
@@ -2,9 +2,14 @@
|
|||||||
pub struct LogEntry {
|
pub struct LogEntry {
|
||||||
pub event: &'static str,
|
pub event: &'static str,
|
||||||
pub protocol_error: Option<&'static str>,
|
pub protocol_error: Option<&'static str>,
|
||||||
|
pub reason: Option<&'static str>,
|
||||||
pub error_detail: Option<String>,
|
pub error_detail: Option<String>,
|
||||||
|
pub error: Option<String>,
|
||||||
pub local_addr: Option<String>,
|
pub local_addr: Option<String>,
|
||||||
pub tls_sni: Option<String>,
|
pub tls_sni: Option<String>,
|
||||||
|
pub sni: Option<String>,
|
||||||
|
pub authority: Option<String>,
|
||||||
|
pub matched_by: Option<&'static str>,
|
||||||
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>,
|
||||||
@@ -85,6 +90,21 @@ fn protocol_error_detail(detail: impl std::fmt::Display) -> String {
|
|||||||
detail
|
detail
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn protocol_identity_field(value: Option<String>) -> Option<String> {
|
||||||
|
value.map(|value| {
|
||||||
|
let mut value: String = value.chars().filter(|ch| !ch.is_control()).collect();
|
||||||
|
if value.len() > 512 {
|
||||||
|
let mut end = 512;
|
||||||
|
while !value.is_char_boundary(end) {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
value.truncate(end);
|
||||||
|
value.push_str("...");
|
||||||
|
}
|
||||||
|
value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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);
|
||||||
@@ -143,18 +163,47 @@ impl Logs {
|
|||||||
http_version: &'static str,
|
http_version: &'static str,
|
||||||
status: u16,
|
status: u16,
|
||||||
tls_sni: Option<String>,
|
tls_sni: Option<String>,
|
||||||
|
) {
|
||||||
|
self.push_protocol_error_attributed(
|
||||||
|
kind, detail, client, local, transport, http_version, status, tls_sni, None, None,
|
||||||
|
"none",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn push_protocol_error_attributed(
|
||||||
|
&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>,
|
||||||
|
authority: Option<String>,
|
||||||
|
vhost: Option<String>,
|
||||||
|
matched_by: &'static str,
|
||||||
) {
|
) {
|
||||||
if !self.settings.load().enabled() {
|
if !self.settings.load().enabled() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let detail = protocol_error_detail(detail);
|
let detail = protocol_error_detail(detail);
|
||||||
|
let tls_sni = protocol_identity_field(tls_sni);
|
||||||
|
let authority = protocol_identity_field(authority);
|
||||||
|
let sni = tls_sni.clone();
|
||||||
self.push_sink(LogEntry {
|
self.push_sink(LogEntry {
|
||||||
event: "protocol_error",
|
event: "protocol_error",
|
||||||
protocol_error: Some(kind),
|
protocol_error: Some(kind),
|
||||||
error_detail: Some(detail),
|
reason: Some(kind),
|
||||||
|
error_detail: Some(detail.clone()),
|
||||||
|
error: Some(detail),
|
||||||
local_addr: Some(local.to_string()),
|
local_addr: Some(local.to_string()),
|
||||||
tls_sni,
|
tls_sni,
|
||||||
vhost: None,
|
sni,
|
||||||
|
authority,
|
||||||
|
matched_by: Some(matched_by),
|
||||||
|
vhost,
|
||||||
route: None,
|
route: None,
|
||||||
cache_effective_enabled: None,
|
cache_effective_enabled: None,
|
||||||
transform_effective_enabled: None,
|
transform_effective_enabled: None,
|
||||||
@@ -221,11 +270,20 @@ fn value_for(entry: &LogEntry, name: &str) -> Value {
|
|||||||
.protocol_error
|
.protocol_error
|
||||||
.map(|v| Value::String(v.into()))
|
.map(|v| Value::String(v.into()))
|
||||||
.unwrap_or(Value::Null),
|
.unwrap_or(Value::Null),
|
||||||
|
"reason" => entry
|
||||||
|
.reason
|
||||||
|
.map(|v| Value::String(v.into()))
|
||||||
|
.unwrap_or(Value::Null),
|
||||||
"error_detail" => entry
|
"error_detail" => entry
|
||||||
.error_detail
|
.error_detail
|
||||||
.clone()
|
.clone()
|
||||||
.map(Value::String)
|
.map(Value::String)
|
||||||
.unwrap_or(Value::Null),
|
.unwrap_or(Value::Null),
|
||||||
|
"error" => entry
|
||||||
|
.error
|
||||||
|
.clone()
|
||||||
|
.map(Value::String)
|
||||||
|
.unwrap_or(Value::Null),
|
||||||
"local_addr" => entry
|
"local_addr" => entry
|
||||||
.local_addr
|
.local_addr
|
||||||
.clone()
|
.clone()
|
||||||
@@ -236,6 +294,20 @@ fn value_for(entry: &LogEntry, name: &str) -> Value {
|
|||||||
.clone()
|
.clone()
|
||||||
.map(Value::String)
|
.map(Value::String)
|
||||||
.unwrap_or(Value::Null),
|
.unwrap_or(Value::Null),
|
||||||
|
"sni" => entry
|
||||||
|
.sni
|
||||||
|
.clone()
|
||||||
|
.map(Value::String)
|
||||||
|
.unwrap_or(Value::Null),
|
||||||
|
"authority" => entry
|
||||||
|
.authority
|
||||||
|
.clone()
|
||||||
|
.map(Value::String)
|
||||||
|
.unwrap_or(Value::Null),
|
||||||
|
"matched_by" => entry
|
||||||
|
.matched_by
|
||||||
|
.map(|v| Value::String(v.into()))
|
||||||
|
.unwrap_or(Value::Null),
|
||||||
"vhost" => entry
|
"vhost" => entry
|
||||||
.vhost
|
.vhost
|
||||||
.clone()
|
.clone()
|
||||||
|
|||||||
@@ -231,9 +231,14 @@ fn source_value_name(name: &str) -> bool {
|
|||||||
name,
|
name,
|
||||||
"event"
|
"event"
|
||||||
| "protocol_error"
|
| "protocol_error"
|
||||||
|
| "reason"
|
||||||
| "error_detail"
|
| "error_detail"
|
||||||
|
| "error"
|
||||||
| "local_addr"
|
| "local_addr"
|
||||||
| "tls_sni"
|
| "tls_sni"
|
||||||
|
| "sni"
|
||||||
|
| "authority"
|
||||||
|
| "matched_by"
|
||||||
| "vhost"
|
| "vhost"
|
||||||
| "route"
|
| "route"
|
||||||
| "cache_effective_enabled"
|
| "cache_effective_enabled"
|
||||||
|
|||||||
+10
-18
@@ -19,11 +19,17 @@ fn format_entry(entry: &LogEntry, format: &JsonFormat) -> Value {
|
|||||||
for name in [
|
for name in [
|
||||||
"event",
|
"event",
|
||||||
"protocol_error",
|
"protocol_error",
|
||||||
|
"reason",
|
||||||
"error_detail",
|
"error_detail",
|
||||||
|
"error",
|
||||||
"time_ms",
|
"time_ms",
|
||||||
"client_ip",
|
"client_ip",
|
||||||
"local_addr",
|
"local_addr",
|
||||||
"tls_sni",
|
"tls_sni",
|
||||||
|
"sni",
|
||||||
|
"authority",
|
||||||
|
"matched_by",
|
||||||
|
"vhost",
|
||||||
"transport",
|
"transport",
|
||||||
"http_version",
|
"http_version",
|
||||||
"status",
|
"status",
|
||||||
@@ -63,24 +69,10 @@ struct FileState {
|
|||||||
const PER_VHOST_OPEN_FILES: usize = 64;
|
const PER_VHOST_OPEN_FILES: usize = 64;
|
||||||
|
|
||||||
fn vhost_log_name(vhost: Option<&str>) -> String {
|
fn vhost_log_name(vhost: Option<&str>) -> String {
|
||||||
let raw = vhost
|
vhost
|
||||||
.filter(|value| !value.is_empty())
|
.filter(|value| crate::config::valid_id(value))
|
||||||
.unwrap_or("_unmatched");
|
.unwrap_or("_unmatched")
|
||||||
let mut safe = String::with_capacity(raw.len().min(96));
|
.to_owned()
|
||||||
for ch in raw.chars().take(96) {
|
|
||||||
safe.push(
|
|
||||||
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
|
|
||||||
ch
|
|
||||||
} else {
|
|
||||||
'_'
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if safe.is_empty() {
|
|
||||||
"_unmatched".into()
|
|
||||||
} else {
|
|
||||||
safe
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
struct LogstashState {
|
struct LogstashState {
|
||||||
address: String,
|
address: String,
|
||||||
|
|||||||
+25
-1
@@ -5,9 +5,14 @@ mod tests {
|
|||||||
LogEntry {
|
LogEntry {
|
||||||
event: "request",
|
event: "request",
|
||||||
protocol_error: None,
|
protocol_error: None,
|
||||||
|
reason: None,
|
||||||
error_detail: None,
|
error_detail: None,
|
||||||
|
error: None,
|
||||||
local_addr: Some("192.0.2.1:443".into()),
|
local_addr: Some("192.0.2.1:443".into()),
|
||||||
tls_sni: Some("app.example".into()),
|
tls_sni: Some("app.example".into()),
|
||||||
|
sni: Some("app.example".into()),
|
||||||
|
authority: None,
|
||||||
|
matched_by: None,
|
||||||
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),
|
||||||
@@ -95,7 +100,7 @@ mod tests {
|
|||||||
fn per_vhost_file_mode_has_safe_names_and_requires_directory() {
|
fn per_vhost_file_mode_has_safe_names_and_requires_directory() {
|
||||||
assert_eq!(vhost_log_name(Some("app-1")), "app-1");
|
assert_eq!(vhost_log_name(Some("app-1")), "app-1");
|
||||||
assert_eq!(vhost_log_name(None), "_unmatched");
|
assert_eq!(vhost_log_name(None), "_unmatched");
|
||||||
assert_eq!(vhost_log_name(Some("bad/name")), "bad_name");
|
assert_eq!(vhost_log_name(Some("bad/name")), "_unmatched");
|
||||||
let mut settings = Settings::default();
|
let mut settings = Settings::default();
|
||||||
settings.file.enabled = true;
|
settings.file.enabled = true;
|
||||||
settings.file.per_vhost = true;
|
settings.file.per_vhost = true;
|
||||||
@@ -124,7 +129,12 @@ mod tests {
|
|||||||
let mut item = entry();
|
let mut item = entry();
|
||||||
item.event = "protocol_error";
|
item.event = "protocol_error";
|
||||||
item.protocol_error = Some("tls_handshake_error");
|
item.protocol_error = Some("tls_handshake_error");
|
||||||
|
item.reason = Some("tls_handshake_error");
|
||||||
item.error_detail = Some("invalid handshake".into());
|
item.error_detail = Some("invalid handshake".into());
|
||||||
|
item.error = Some("invalid handshake".into());
|
||||||
|
item.sni = Some("app.example".into());
|
||||||
|
item.authority = Some("app.example:443".into());
|
||||||
|
item.matched_by = Some("sni");
|
||||||
item.outcome = "protocol_error";
|
item.outcome = "protocol_error";
|
||||||
item.status = 0;
|
item.status = 0;
|
||||||
item.transport = "tls";
|
item.transport = "tls";
|
||||||
@@ -135,7 +145,12 @@ mod tests {
|
|||||||
assert_eq!(value["site"], "app");
|
assert_eq!(value["site"], "app");
|
||||||
assert_eq!(value["event"], "protocol_error");
|
assert_eq!(value["event"], "protocol_error");
|
||||||
assert_eq!(value["protocol_error"], "tls_handshake_error");
|
assert_eq!(value["protocol_error"], "tls_handshake_error");
|
||||||
|
assert_eq!(value["reason"], "tls_handshake_error");
|
||||||
assert_eq!(value["error_detail"], "invalid handshake");
|
assert_eq!(value["error_detail"], "invalid handshake");
|
||||||
|
assert_eq!(value["error"], "invalid handshake");
|
||||||
|
assert_eq!(value["sni"], "app.example");
|
||||||
|
assert_eq!(value["authority"], "app.example:443");
|
||||||
|
assert_eq!(value["matched_by"], "sni");
|
||||||
assert_eq!(value["client_ip"], "192.0.2.10");
|
assert_eq!(value["client_ip"], "192.0.2.10");
|
||||||
assert_eq!(value["transport"], "tls");
|
assert_eq!(value["transport"], "tls");
|
||||||
assert_eq!(value["outcome"], "protocol_error");
|
assert_eq!(value["outcome"], "protocol_error");
|
||||||
@@ -170,6 +185,15 @@ mod tests {
|
|||||||
assert!(detail.ends_with("..."));
|
assert!(detail.ends_with("..."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn protocol_identity_fields_are_bounded_and_strip_controls() {
|
||||||
|
let value = protocol_identity_field(Some(format!("host\n{}", "€".repeat(300)))).unwrap();
|
||||||
|
assert!(!value.contains('\n'));
|
||||||
|
assert!(value.is_char_boundary(value.len()));
|
||||||
|
assert!(value.len() <= 515);
|
||||||
|
assert!(value.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.26.0"
|
about = "RustProxy - independent Rust reverse proxy and static Web Server 0.26.1"
|
||||||
)]
|
)]
|
||||||
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.26.0 - all vhosts. Type 'help' for interactive filters; Ctrl-C or 'quit' exits.");
|
println!("RustProxy live logs 0.26.1 - 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();
|
||||||
|
|||||||
@@ -204,9 +204,14 @@ impl Drop for Exchange {
|
|||||||
self.logs.push(LogEntry {
|
self.logs.push(LogEntry {
|
||||||
event: "request",
|
event: "request",
|
||||||
protocol_error: None,
|
protocol_error: None,
|
||||||
|
reason: None,
|
||||||
error_detail: None,
|
error_detail: None,
|
||||||
|
error: None,
|
||||||
local_addr: None,
|
local_addr: None,
|
||||||
tls_sni: None,
|
tls_sni: None,
|
||||||
|
sni: None,
|
||||||
|
authority: None,
|
||||||
|
matched_by: 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,
|
||||||
|
|||||||
+57
-10
@@ -88,6 +88,61 @@ pub struct Transport {
|
|||||||
pub tls: bool,
|
pub tls: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn log_protocol_error_with_identity(
|
||||||
|
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,
|
||||||
|
authority: Option<String>,
|
||||||
|
host: Option<String>,
|
||||||
|
tls_sni: Option<String>,
|
||||||
|
) {
|
||||||
|
if let Some(state) = state {
|
||||||
|
let resolved = state.config.load().resolve_logging_vhost(
|
||||||
|
local,
|
||||||
|
matches!(transport, "tls" | "quic"),
|
||||||
|
None,
|
||||||
|
authority.as_deref(),
|
||||||
|
host.as_deref(),
|
||||||
|
tls_sni.as_deref(),
|
||||||
|
);
|
||||||
|
let (vhost, matched_by) = resolved
|
||||||
|
.map(|resolved| (Some(resolved.vhost), resolved.matched_by))
|
||||||
|
.unwrap_or((None, "none"));
|
||||||
|
state.logs.push_protocol_error_attributed(
|
||||||
|
kind,
|
||||||
|
detail,
|
||||||
|
peer,
|
||||||
|
local,
|
||||||
|
transport,
|
||||||
|
http_version,
|
||||||
|
status,
|
||||||
|
tls_sni,
|
||||||
|
authority.or(host),
|
||||||
|
vhost,
|
||||||
|
matched_by,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_log_identity<B>(request: &Request<B>) -> (Option<String>, Option<String>) {
|
||||||
|
let authority = request
|
||||||
|
.uri()
|
||||||
|
.authority()
|
||||||
|
.map(|value| value.as_str().to_owned());
|
||||||
|
let host = request
|
||||||
|
.headers()
|
||||||
|
.get(http::header::HOST)
|
||||||
|
.and_then(|value| value.to_str().ok())
|
||||||
|
.map(str::to_owned);
|
||||||
|
(authority, host)
|
||||||
|
}
|
||||||
|
|
||||||
fn log_protocol_error(
|
fn log_protocol_error(
|
||||||
state: Option<&Arc<App>>,
|
state: Option<&Arc<App>>,
|
||||||
kind: &'static str,
|
kind: &'static str,
|
||||||
@@ -99,19 +154,11 @@ fn log_protocol_error(
|
|||||||
status: u16,
|
status: u16,
|
||||||
tls_sni: Option<String>,
|
tls_sni: Option<String>,
|
||||||
) {
|
) {
|
||||||
if let Some(state) = state {
|
log_protocol_error_with_identity(
|
||||||
state.logs.push_protocol_error(
|
state, kind, detail, peer, local, transport, http_version, status, None, None,
|
||||||
kind,
|
|
||||||
detail,
|
|
||||||
peer,
|
|
||||||
local,
|
|
||||||
transport,
|
|
||||||
http_version,
|
|
||||||
status,
|
|
||||||
tls_sni,
|
tls_sni,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ServerShutdown(pub CancellationToken);
|
pub struct ServerShutdown(pub CancellationToken);
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|||||||
+3
-2
@@ -5,6 +5,7 @@ async fn http2_connection<I>(
|
|||||||
peer: SocketAddr,
|
peer: SocketAddr,
|
||||||
local: SocketAddr,
|
local: SocketAddr,
|
||||||
transport: Transport,
|
transport: Transport,
|
||||||
|
tls_sni: Option<String>,
|
||||||
extended_connect: bool,
|
extended_connect: bool,
|
||||||
frontend_state: Option<Arc<App>>,
|
frontend_state: Option<Arc<App>>,
|
||||||
lease: Arc<ConnectionLease>,
|
lease: Arc<ConnectionLease>,
|
||||||
@@ -69,7 +70,7 @@ async fn http2_connection<I>(
|
|||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
log_protocol_error(
|
log_protocol_error(
|
||||||
frontend_state.as_ref(), "http2_connection_error", &error, peer, local,
|
frontend_state.as_ref(), "http2_connection_error", &error, peer, local,
|
||||||
if transport.tls { "tls" } else { "tcp" }, "2", 0, None,
|
if transport.tls { "tls" } else { "tcp" }, "2", 0, tls_sni.clone(),
|
||||||
);
|
);
|
||||||
tracing::debug!(%error, "HTTP/2 connection closed");
|
tracing::debug!(%error, "HTTP/2 connection closed");
|
||||||
}
|
}
|
||||||
@@ -108,7 +109,7 @@ async fn http2_connection<I>(
|
|||||||
if transport.tls { "tls" } else { "tcp" },
|
if transport.tls { "tls" } else { "tcp" },
|
||||||
"2",
|
"2",
|
||||||
0,
|
0,
|
||||||
None,
|
tls_sni.clone(),
|
||||||
);
|
);
|
||||||
tracing::debug!(?cause, %error, "HTTP/2 graceful shutdown ended with protocol error")
|
tracing::debug!(?cause, %error, "HTTP/2 graceful shutdown ended with protocol error")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,7 +100,9 @@ mod h2c_tests {
|
|||||||
peer,
|
peer,
|
||||||
local,
|
local,
|
||||||
Transport { tls: false },
|
Transport { tls: false },
|
||||||
|
None,
|
||||||
false,
|
false,
|
||||||
|
None,
|
||||||
lease,
|
lease,
|
||||||
server_stop,
|
server_stop,
|
||||||
Duration::from_secs(5),
|
Duration::from_secs(5),
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ async fn serve_inner(
|
|||||||
peer,
|
peer,
|
||||||
local,
|
local,
|
||||||
Transport { tls: true },
|
Transport { tls: true },
|
||||||
|
host.clone(),
|
||||||
h2,
|
h2,
|
||||||
extended_connect,
|
extended_connect,
|
||||||
false,
|
false,
|
||||||
@@ -168,6 +169,7 @@ async fn serve_inner(
|
|||||||
peer,
|
peer,
|
||||||
local,
|
local,
|
||||||
Transport { tls: false },
|
Transport { tls: false },
|
||||||
|
None,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -197,6 +199,7 @@ async fn serve_inner(
|
|||||||
peer,
|
peer,
|
||||||
local,
|
local,
|
||||||
Transport { tls: true },
|
Transport { tls: true },
|
||||||
|
None,
|
||||||
h2,
|
h2,
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
@@ -245,6 +248,7 @@ async fn serve_inner(
|
|||||||
peer,
|
peer,
|
||||||
local,
|
local,
|
||||||
Transport { tls: false },
|
Transport { tls: false },
|
||||||
|
None,
|
||||||
prior_h2,
|
prior_h2,
|
||||||
extended_connect,
|
extended_connect,
|
||||||
h2c && !prior_h2,
|
h2c && !prior_h2,
|
||||||
@@ -276,6 +280,7 @@ async fn connection<I>(
|
|||||||
peer: SocketAddr,
|
peer: SocketAddr,
|
||||||
local: SocketAddr,
|
local: SocketAddr,
|
||||||
transport: Transport,
|
transport: Transport,
|
||||||
|
tls_sni: Option<String>,
|
||||||
h2: bool,
|
h2: bool,
|
||||||
extended_connect: bool,
|
extended_connect: bool,
|
||||||
h2c_upgrade: bool,
|
h2c_upgrade: bool,
|
||||||
@@ -294,6 +299,7 @@ async fn connection<I>(
|
|||||||
peer,
|
peer,
|
||||||
local,
|
local,
|
||||||
transport,
|
transport,
|
||||||
|
tls_sni.clone(),
|
||||||
extended_connect,
|
extended_connect,
|
||||||
frontend_state.clone(),
|
frontend_state.clone(),
|
||||||
lease,
|
lease,
|
||||||
@@ -322,7 +328,8 @@ async fn connection<I>(
|
|||||||
|
|
||||||
if h2c_attempt(&request) {
|
if h2c_attempt(&request) {
|
||||||
if !h2c_upgrade {
|
if !h2c_upgrade {
|
||||||
log_protocol_error(
|
let (authority, host) = request_log_identity(&request);
|
||||||
|
log_protocol_error_with_identity(
|
||||||
upgrade_state.as_ref(),
|
upgrade_state.as_ref(),
|
||||||
"h2c_upgrade_rejected",
|
"h2c_upgrade_rejected",
|
||||||
"h2c upgrade is disabled on this listener",
|
"h2c upgrade is disabled on this listener",
|
||||||
@@ -331,6 +338,8 @@ async fn connection<I>(
|
|||||||
"tcp",
|
"tcp",
|
||||||
"1.1",
|
"1.1",
|
||||||
400,
|
400,
|
||||||
|
authority,
|
||||||
|
host,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
return Ok::<_, Infallible>(
|
return Ok::<_, Infallible>(
|
||||||
@@ -344,7 +353,8 @@ 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(
|
let (authority, host) = request_log_identity(&request);
|
||||||
|
log_protocol_error_with_identity(
|
||||||
upgrade_state.as_ref(),
|
upgrade_state.as_ref(),
|
||||||
"h2c_upgrade_invalid",
|
"h2c_upgrade_invalid",
|
||||||
message,
|
message,
|
||||||
@@ -353,6 +363,8 @@ async fn connection<I>(
|
|||||||
"tcp",
|
"tcp",
|
||||||
"1.1",
|
"1.1",
|
||||||
400,
|
400,
|
||||||
|
authority,
|
||||||
|
host,
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
return Ok::<_, Infallible>(
|
return Ok::<_, Infallible>(
|
||||||
@@ -389,6 +401,7 @@ async fn connection<I>(
|
|||||||
peer,
|
peer,
|
||||||
local,
|
local,
|
||||||
Transport { tls: false },
|
Transport { tls: false },
|
||||||
|
None,
|
||||||
extended_connect,
|
extended_connect,
|
||||||
upgrade_state_for_task.clone(),
|
upgrade_state_for_task.clone(),
|
||||||
upgrade_lease,
|
upgrade_lease,
|
||||||
@@ -448,7 +461,7 @@ async fn connection<I>(
|
|||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
log_protocol_error(
|
log_protocol_error(
|
||||||
frontend_state.as_ref(), "http1_connection_error", &error, peer, local,
|
frontend_state.as_ref(), "http1_connection_error", &error, peer, local,
|
||||||
if transport.tls { "tls" } else { "tcp" }, "1.1", 400, None,
|
if transport.tls { "tls" } else { "tcp" }, "1.1", 400, tls_sni.clone(),
|
||||||
);
|
);
|
||||||
tracing::debug!(%error, "HTTP/1 connection closed");
|
tracing::debug!(%error, "HTTP/1 connection closed");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ pub(super) fn fastcgi_params(
|
|||||||
if context.tls { "on" } else { "off" }.into(),
|
if context.tls { "on" } else { "off" }.into(),
|
||||||
);
|
);
|
||||||
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
|
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
|
||||||
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.26.0".into());
|
params.insert("SERVER_SOFTWARE".into(), "RustProxy/0.26.1".into());
|
||||||
params.insert("REDIRECT_STATUS".into(), "200".into());
|
params.insert("REDIRECT_STATUS".into(), "200".into());
|
||||||
if let Some(value) = headers
|
if let Some(value) = headers
|
||||||
.get(CONTENT_TYPE)
|
.get(CONTENT_TYPE)
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ pub(super) fn directory_listing(path: &str, entries: &[DirectoryEntry]) -> Vec<u
|
|||||||
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
|
"<a class=\"{class}\" href=\"{href}\"><span class=\"type\">{type_label}</span><span class=\"name\">{label}{slash}</span><span class=\"kind\">{kind}</span></a>"
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
html.push_str(r#"</div></section><footer class="page-footer"><span>RustProxy <b>0.26.0</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.1</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.26.0</b>"));
|
assert!(body.contains("RustProxy <b>0.26.1</b>"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
Reference in new issue
Block a user