v0.26.1
This commit is contained in:
1 parent
ddc5c6593f
commit
b9d137eee1
63 files changed
+1232
-445
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
|
||||
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
|
||||
compatibility. Current source version: 0.26.0.
|
||||
compatibility. Current source version: 0.26.1.
|
||||
|
||||
## Delivery
|
||||
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
> test-only changes and packaging implementation notes were moved to
|
||||
> [`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
|
||||
|
||||
- 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]]
|
||||
name = "rust-proxy"
|
||||
version = "0.26.0"
|
||||
version = "0.26.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.26.0"
|
||||
version = "0.26.1"
|
||||
edition = "2021"
|
||||
rust-version = "1.88"
|
||||
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,
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
|
||||
## 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.
|
||||
- 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 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.
|
||||
- Keep file-backed Logs independent from Live Logs: existing configured files can be viewed even when their writers are currently disabled.
|
||||
- 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.
|
||||
- Extend standard structured records with `event`, `protocol_error`, `error_detail`, `local_addr` and `tls_sni`.
|
||||
- Keep SQLite schema 8 and the existing saved configuration format; upgrading from 0.25.7 requires no data migration.
|
||||
- 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`.
|
||||
- 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.
|
||||
- Reuse the normal exact/wildcard hostname resolver for logging attribution instead of maintaining protocol-specific hostname matching.
|
||||
- Improve the **Logs** workspace with selectable account-scoped columns, a sticky table header, highlighted selected rows and inline record details.
|
||||
- Collapse the configured-file inventory by default so large per-vhost installations do not dominate the page.
|
||||
- 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 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
|
||||
|
||||
@@ -99,7 +99,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \
|
||||
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
|
||||
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
|
||||
@@ -338,7 +338,7 @@ python3 scripts/release_smoke.py
|
||||
# Runs those checks first, then the Rust suite when Cargo is available:
|
||||
bash dev.sh test
|
||||
# Source-only package, with no Git checkout required:
|
||||
python3 make_zip.py /tmp/RustProxy-0.26.0.zip
|
||||
python3 make_zip.py /tmp/RustProxy-0.26.1.zip
|
||||
```
|
||||
|
||||
## New management and test-origin guide
|
||||
|
||||
+12
-1
@@ -1,8 +1,19 @@
|
||||
# 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
|
||||
|
||||
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] 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
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
4a5f6bf0b46f663a37e8759e8e8962e081d154b47e859f1faed7b603726c01de AGENTS.md
|
||||
d951a8cc95cb902e8d1ec6d702198784458465d36d65a4b90d1df165fb2135d5 CHANGELOG.md
|
||||
1af885bbc3120d440ea6c2025f4d1cb2ee29a59f62230f5202f587158864f171 Cargo.lock
|
||||
1845fd0b917d70a6b092176a211d49577b81d5c6a75ddb4b33eee790ef299089 Cargo.toml
|
||||
ef0142e762f044784ceb61c15c8bf16333250886963850c58e3af45b1419cf5d AGENTS.md
|
||||
ffce7ba9ca03095bb1aee69eb3e806e5849ae6d1e1f24c0a105230fd27162737 CHANGELOG.md
|
||||
eabd3b41a3f62cc93b6803ba759643bb8b9a7cc1de43b9587ca2154bcc04ff09 Cargo.lock
|
||||
f2cb130695ac4eb7f4e5255589e282e4d1a5fd269996a74090a6275f464733b1 Cargo.toml
|
||||
d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile
|
||||
a33a2aab100f87a4f3a868ca32276dbf1e122b3294711c8543ea1ad3e15ab869 README.md
|
||||
a49f373ddac5ba6a0ad778fb545a7ec82dec6b0c436c5f00b3f94c2ebfacd994 ROADMAP.md
|
||||
d3f596b1e2a8caa7129bca66f13b5c6f26cf3513de495c96f20a64a6763cac26 README.md
|
||||
6065f28237d57a21f58fdfa83050781b118fdef3dedf961b1adabc8e734bb788 ROADMAP.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs
|
||||
e6787f5b31d63a6c7b997562855ac7349680268d6f9a7d55d550f33a702b7ad1 debian/README.md
|
||||
1fa83ad64f747e27822815de2d85221ac4bb20e5003a72112fe9b11d7b95cad8 debian/build-package.sh
|
||||
fe2a113daf65042c84838b1e1b7268a74775288ff3fc9bb1570f973d5f8106b7 debian/README.md
|
||||
2690b8c6a61383a2b1752736e44822c8cfc6119beeacbcba326fcc9f02fc162a debian/build-package.sh
|
||||
0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh
|
||||
228319d6ad21c699dc5cc6104686d02d26f41c5d7d8df73fea021896389b314a debian/changelog
|
||||
0e4306f33dd4a04100af9b3932d2cc264aa901a149cec0d3f95e8aeb4c639152 debian/changelog
|
||||
c90457b19b1b2f7998d3db59a656d215eb0fab23769197405960457b66d95cf7 debian/control
|
||||
dff2ecb9c14623416981f444bc6df1c4e2bf55d330260f36a312d8d42245400f debian/rules
|
||||
c537d54fd92a9b97c5f6e6ec63bc1276cda324513e2946b341b2193f63b62e6a debian/rustproxy.environment
|
||||
@@ -24,7 +24,7 @@ cb0db03a2c1b06fa04a11f7690755a0ca90b17f19ed8fdb228810f505c48c78a debian/rustpro
|
||||
e8e18df40bcd69d6aa404282679186d9b8256ac63a5b4e80d7f081e8a0095a2e debian/source/format
|
||||
385ac7970fa94f043d0bcf04a91184e6e2c807587b85e55517ed064bb4bddba6 deploy/README.md
|
||||
7b7fdf383bbaea4f37b5c120e98a24e9a075fba56ae7838a31a5152c7edcde02 deploy/rustproxy.service
|
||||
162551960700a3a1c68dba29f37248f6a31d6a0d39144559e9653f6657ca4d94 dev.sh
|
||||
cf4180779795cf06416d34a55372ff7d258f0ce6a1e7208bf7912b62b682aa7f dev.sh
|
||||
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
|
||||
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
|
||||
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
|
||||
@@ -33,14 +33,14 @@ bf687a851374d8f1fa59d050f3f76532f7c12f16a6daa7d3c36102c2d9a8ec9c docker-agent/d
|
||||
9d77c90767fa929ee8338734d72ce83284d3d965673270f62cb2b1613d587a33 docker-agent/install.sh
|
||||
b68234472bec5cedd67c7cfe1de7d8806a965dfd4101f944aa6320132f18c14e docker-agent/main.go
|
||||
3d4051a1f805d145df3877476e4b87773e74be8e123045065955a5133afdc68c docker-agent/main_test.go
|
||||
9de427ff54b2fba7a8ed5ff356b1b0069bda81f3953b0252638e84efc79b73fb docker-agent/model.go
|
||||
d4fa829176c8997a8fc42d59d7c45e252efa8a6f05c818d511fd24146da39a01 docker-agent/model.go
|
||||
e52b31d0abf65ae3034f6cb367cca113633014ce8d1d799ad4dd5624d2c7b8bb docker-agent/network.go
|
||||
634790f35ff4627df2a78cec2ed371f6e9ec9df00afb386075595fc7f3ed72eb docker-agent/rustproxy-docker-agent.service
|
||||
5220d669171ec3ad65f50e0bc8de5178dadaa4904def8e1a4f61002aa9e9a55b docker-agent/server.go
|
||||
802a30b793a6ae6a5046489612a23659f33d25e41d0794551381db202daac904 docs/ACME_PROVIDERS.md
|
||||
ec15163f5f51c9212003b7edd1fc380321311c2496fbaa9b45c838281b9af475 docs/ACME_RFC2136.md
|
||||
d699f2261a0d7854e49e9b8bae44fa37008a7d2f6340a73cdc37e840722ea211 docs/API.md
|
||||
e386c03dba02287a864329b9b1993cbfd131764d0755c8d9f87b8b198940abcf docs/API_ACCESS.md
|
||||
9b66a67392803805d0d28c7719f263288b5e6ef16f06b9b8c13a3afdf90271ed docs/API_ACCESS.md
|
||||
0f7f159ad5b4b5277f80c19f3c47f4e7823ea439ef02e5913e1391f1b34d129f docs/ARCHITECTURE.md
|
||||
cd2597465af031cdb47db802f8ad6d3af34ef9e79f679df906a7b84220ea3260 docs/CACHE.md
|
||||
9cb338ed07ac1f92ad491e2099a7e3af63e66262417b1fef24d98c6a791d13fa docs/CACHE_DIAGNOSTICS.md
|
||||
@@ -49,20 +49,20 @@ db5534427ff0f004940fd6960906d3eaa75347a110e6322827c51c5d63f20adb docs/CONTAINER
|
||||
9538a52d508d8d7053c1387c42eef60e6d5dc1879c300b6fe951b3374c16884c docs/CUSTOM_LOCATIONS.md
|
||||
7db65a96f696aff1d7f74e2041cfc6ecdf437b1412ece90cee26da1ceff4cf7e docs/DOCKER_DISCOVERY.md
|
||||
86bb62f288f2e999994c302716fc9986dba1ac7a26e598dc97cb56a7bc21cfb5 docs/ERROR_LOGGING.md
|
||||
363e46aa4d6ca133a21e47d5a530154fbd1784eb43593b36022c85c66c0e787b docs/FILE_LOGS.md
|
||||
a1150f7525a20db8e8399eb04ce679e1699fd1e574485e27bdf2f565436fc508 docs/FILE_LOGS.md
|
||||
df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md
|
||||
5299c5f58056fea84e88d6d86857fb593d0a7458af3be559476de2bb57939b4d docs/INSTALL.md
|
||||
1b120d86a65137319234129ce78af39733e59af4b764869a9d7fdd01b9312382 docs/INSTALL.md
|
||||
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
|
||||
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
|
||||
7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md
|
||||
ba3cbe1f80271326d6e92e98e035ff280bb8ddbf9d602273684dca147e09d172 docs/MIGRATION.md
|
||||
526e24e7a9b76941efccf64f4bacab5529e08489add316ec44b33da2756b4dd5 docs/MIGRATION.md
|
||||
5f090a190000c6badab2a74e5d4968e70c34de43b59c1452f5b05503b5c5c18a docs/MOCK_AUTH.md
|
||||
1f85fac0e06e432ef6f744f0b5f9c1b7a27a1e9e1ad30dc1a218eab96b1caffe docs/MOCK_SERVER.md
|
||||
e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND_HOSTS.md
|
||||
3da047768f32064c5ef0ad7a4c73c4b14930fe7317c54f5650f426af9f484593 docs/NPM_MIGRATION.md
|
||||
712cb1cbf84515384b081fd2929e86e40af7c10f65bc69919b78b7b3e2fd12b5 docs/PHP_FPM.md
|
||||
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
|
||||
a31cce4c476ce9d449a5083510660a569d97dcf165f36e24d26b100a29d66944 docs/README.md
|
||||
087ce068b693b9a0b5fba4b884f82d9e1ece056e5b083e4d2f8cab41062f9bd7 docs/README.md
|
||||
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
|
||||
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
|
||||
ce5afb4f531bb5fb4c453deb49f643f2a563f32f7908c995e5bc8e0d7de995b7 docs/RENEWAL.md
|
||||
@@ -72,7 +72,7 @@ a699fd663bfbf001525f22f0fbacdf03a58c5e825232d8d144e51cbb8b1008b2 docs/SECURITY.
|
||||
620b69e566d27af04f0604f4ca3f88cf95cdfea7fbc6ae9dd92446d2b2b5d9be docs/SECURITY_AUDIT.md
|
||||
4245408286d8bdc3bed585c2f0928ecd4c69a3e48dfacbac9b952079ec2075b1 docs/SETTINGS.md
|
||||
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
|
||||
d9153240be9cca05079893e10b1649c3d5c9a3cf8517890afe0e3aff1cdd6fad docs/TEST_REPORT.md
|
||||
cc678073533ef97b5b9397a1698e7a33a3d7156cc933d754adbf748443e70693 docs/TEST_REPORT.md
|
||||
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
||||
8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md
|
||||
a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md
|
||||
@@ -100,6 +100,7 @@ d0e609febbcd1cfdb8ea0b00b4fa6f081eac01e2d43a81e38a53ae25783152cc docs/V0.24.8_A
|
||||
9dbb66e408bcd4d32267724e557fa9f7f44ef9b70db4ab170c61a7a839b7b157 docs/V0.25.3_AUDIT.md
|
||||
583d548b4a5db9f35a2bbf8ec00ec22397764c5850a8e883a3d12680da517df7 docs/V0.25.4_AUDIT.md
|
||||
bf9b4cf4fec2c8ce287e1b3c2e7d4828e101dafe4d6c173f099819f22493d144 docs/V0.25.5_AUDIT.md
|
||||
05e0b4cc074b07ff37d45fabc20c7272bada217286b3db3d7997cae25c1f6f11 docs/V0.26.1_IMPLEMENTATION.md
|
||||
ce259052c023e90a7bfb6e39fe82e52b845a8b2a051bfa29883d29e87a9301a2 docs/VHOSTS.md
|
||||
b979afd3d56731eeb06e9dc38d37730693e7fbf3aa78467932842c0e712e7b26 docs/VHOST_ACCESS.md
|
||||
f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md
|
||||
@@ -114,14 +115,14 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock-
|
||||
5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json
|
||||
d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json
|
||||
af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json
|
||||
a25dbbfb5f6ebdf23898cfc50828e965d14bbf9f2a397463010a07840cd84f49 grafana/README.md
|
||||
8909eeefb8186c6c8a140b460501db96a7891cd062b7ad3d4ca754ea2185387c grafana/rustproxy-overview.json
|
||||
b14d30a7785545a50a057f020b3df02a2f46a65c75fca0e5aba1dcb959852e57 grafana/README.md
|
||||
7ba863b4731bb3240cc55fb3e97e6bb2492e43718980a0cfbddd98da70bef2c5 grafana/rustproxy-overview.json
|
||||
30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh
|
||||
858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh
|
||||
29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md
|
||||
e034c5376b65bf23d49f652d5315e87d0d3f28b02c91955055227235121d78a3 langs/en_US.json
|
||||
47680dc4c306623890febef47d847f50ed5e8780550c15391f13c3c15ca1c770 langs/pl_pl.json
|
||||
d5df6eade5883206f52d6e015ddfd4cac0c904faaa7616c1c5c49ff5bd05035a make_zip.py
|
||||
1b2f1e7127ccddb4f9c7cbca48282b5cb5f8fd38dbcdbc2c0a5695679821ee73 langs/en_US.json
|
||||
822c1630aea00228118981a4dffdda05a0dc49dff715c111a0760b0ae50e1f4c langs/pl_pl.json
|
||||
fca0b6e43ee1d1b970d9dd33eba93000c452e0380d9f0c3bbd9ee03b4868cfc1 make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql
|
||||
@@ -140,11 +141,11 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p
|
||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py
|
||||
a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py
|
||||
20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py
|
||||
6a0425b289abfca1bc67d43ce8a586b20ffcb9d839e020ddbb1205c4a6336eb2 scripts/check_source.py
|
||||
3e66e89b51f8b1328a68a68b9a87f865d4034c08f01e8ccbf37fe9fdb7e39d76 scripts/check_source.py
|
||||
187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py
|
||||
d2e954c80b229f8b4a37b2b401b0ac7611507cb76c03866cf688453c531b903e scripts/deploy_core.py
|
||||
aafb8042851ae43b64fc51c793ed2134a308d1d155574ae79c1889afc226680a scripts/deploy_core.py
|
||||
f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py
|
||||
948d910904143a98574c7fc256dfa2fac17572c6606876e5b64b0a8baf56eaba scripts/docs_check.py
|
||||
64c746a3d225d810ee8aa9055cbafe80011fba49dbfe0f6857d631d171046ee9 scripts/docs_check.py
|
||||
6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py
|
||||
2e35b7c78887fd56f83ca9017caa168630963a9f47da71dfc53b1a8f9ad436c2 scripts/memory_contracts.py
|
||||
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py
|
||||
@@ -154,8 +155,8 @@ d1418f8e574a7390fd45a2aa879bdf5720b773162d65e295ab0350506aa5220a scripts/mock_a
|
||||
2d3ff92300b09ce9cecbbaaa9309b4d33160fa4b2291c9952b784e8cfb599ab5 scripts/mock_server.py
|
||||
2d4c859aae5ba357a6d30024c8487ae8f59f01b192711f13f9c2f8fee0f48b50 scripts/os-acme-client/README.md
|
||||
c73b814a42bb4ff703441dbd177ee625055eeb115f9b5bb0c2150aede573a159 scripts/os-acme-client/opnsense-rustproxy-cert-deploy.sh
|
||||
f9f8a13e3a8689a45db81c785acaa8c1a78757d3b101e430340f0a881424f9d3 scripts/preflight.py
|
||||
7fdadf0f973aca3c811deb018f1191eee1bcea06446c1e641a80b7096c452bc2 scripts/release_smoke.py
|
||||
6a314bfaf04760a0ae5da298a016400dab705c36769b49412400e6811d679b16 scripts/preflight.py
|
||||
78e9f37c2b31314f582a232b3c45f5f620158094ca24619e47997a820ccf09fb scripts/release_smoke.py
|
||||
45be151cd85fc2ef2f1fdc3003ace63381af262e73af8a685ab48cb697cc0de1 scripts/routeros-oci-build/.env.routeros.example
|
||||
3b48cff8506e5a6ae7d3876f47721f5de7d63e2e02264a78cf5b6ddd52d9fbc6 scripts/routeros-oci-build/Dockerfile.routeros
|
||||
7c7a3e51f43b43e1c962a4b0bb591f99c1f06977bf77f23719f7f867dfe7eb8e scripts/routeros-oci-build/Dockerfile.routeros.dockerignore
|
||||
@@ -167,11 +168,11 @@ ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_s
|
||||
1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py
|
||||
295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py
|
||||
a65010f828194885ffc7f0f733937957f28e6049cd978bec1380a2b8bfbd0113 scripts/source_module_contracts.py
|
||||
41fe5df23e28d4b13d5e998906a8c3a18a312fbeb6cb856237cbae58291fa91e scripts/swagger_assets.py
|
||||
e7790bf216ce8897d29df92c1b44b056644c3cf32417ead21197734fcc3d0f34 scripts/swagger_assets.py
|
||||
102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py
|
||||
c4097fecc34a7b545ffba7203b928f5edf4ed932936df903d90673fc23fac766 scripts/upgrade.py
|
||||
8e5e828c8e18e78678664bdf8eea58adb7cfb736a9464cea1988fdf77c824850 scripts/upgrade.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
|
||||
d8e2b51c39252dbb4305512fa9b1ef508ec610968aed316b10514fd160298762 src/acl.rs
|
||||
4e3be1ea1be0a81529c7c9021fb9bb77c24e5044acd54094972745443ad2c26a src/acl/tests.rs
|
||||
@@ -184,30 +185,31 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi
|
||||
55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs
|
||||
85f00f8157e83a152cc531f4cfecd0b1472ce269be9c8eb1ac17ad0693748b42 src/admin/certificates.rs
|
||||
1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs
|
||||
c9e4237e64a11e7252d0b0b65e12adc2513b8232e9d5fbc81ef22ec81671df7a src/admin/documentation/documents.rs
|
||||
89b6d6a0c2ddef5e77a8f9383105fd8a9b0e195de0ff46c012dd9d1ef57ee2a4 src/admin/documentation/documents.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
|
||||
aeb8351f4d8455cbc81f363862195ce3fda09a07291df7b1eba55cff64861a68 src/admin/mod.rs
|
||||
ae136f17c60ea724906d170799866ef998e90418d53497696ded7424b8beb1b4 src/admin/openapi.json
|
||||
fb079609d182aa63d32b7f2bfc7cf1a4229d7d500266e981fb0fde4fdd032961 src/admin/mod.rs
|
||||
b40aae24eaf99ed8086951dc30d06d75fa0ae48fc2fee9745491337336b129c0 src/admin/openapi.json
|
||||
a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs
|
||||
ee3e9c39eb2d645e55356b99c1af7d6a2ec14acd622e0e0287dea2fc47628fb4 src/admin/preferences.rs
|
||||
b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs
|
||||
d246389975c87d125ebcb8c24282a3729d509cc70e13e0f3b56b4008d80d76b4 src/admin/router.rs
|
||||
9e2981f9c96140bd6d9ca54e0d03b51b0783360e534a13e3b5cd1bec8c9440fd src/admin/session.rs
|
||||
333d5f9f4c27941c535ac689e5826264ffa2bb86b00832b597a9bb9f811f01e8 src/admin/session.rs
|
||||
f7938b202919a49fd2ff86ecdf09efadae12e0ffc0a4246a72a76218db11aa88 src/admin/status.rs
|
||||
cadd7dca29b9aad8c77c7bc23dd249ea37a15adfa4d636a8ef00273a91ce35dc src/admin/tail.rs
|
||||
e5f973c6fbddc3897bc0e6d9f8e70cbe9279acea6161ffb46e013792e845e1e4 src/admin/tests.rs
|
||||
5df98246c46135a176b4b8b705c92fd328719e82eb7342f088256abb976d5ae7 src/admin/tests/part_01.rs
|
||||
6f8c1e30babb2cc4cb7accf4bfa28338651a469b16341f6e7eaec881dd9abf2b src/admin/tests/part_02.rs
|
||||
77608ad33da348dbea41d2a7ceadb75b0bcf05336cb5a20b7f2a47502f1e4842 src/admin/tests/part_03.rs
|
||||
bf3cc7a41c2575003515cc8212111395aaa0f2169b7d04b6bbd0908a6bc2af0f src/admin/tests/part_04.rs
|
||||
60ad5c76db3cd6fdd144d504a7b50153e133ab357e49c8ebadb99c1c8bce562c src/admin/tests/part_04.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
|
||||
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
|
||||
2bfe0dc64d4386133b04bf71502ea9ed8ff346ab0023dc588c68496f6abf3ba3 src/admin/web/app/settings.js
|
||||
cada294084cfa8cb7080143e1ad21d46136bb644ff5f15bb6d457dc034fe11ff src/admin/web/app/settings.js
|
||||
d0be71555de13f732fd6ee44438a0110eb08c91e78bbc1aa36a1a84182780210 src/admin/web/cache-panel.js
|
||||
50d1ebcbaf4d0b2968da23bb943fc3f67131c5d32df3f61d23ff37aad9da5b81 src/admin/web/demo.html
|
||||
3b84a7a08f9f68e2c78fed4523b594c31a251addc5480a69c5f6949908230539 src/admin/web/docker.js
|
||||
@@ -221,9 +223,9 @@ f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/
|
||||
386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html
|
||||
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
|
||||
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
|
||||
c3d38fbf046e287a77c51a5b0182e377a90d7dce7b3fb23c2e42a712e1347d80 src/admin/web/index.html
|
||||
97444c4ab0441af6767e7383bf70c9f3c163b846614ce9b3623ab40e05731118 src/admin/web/index.html
|
||||
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
|
||||
cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json
|
||||
a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html
|
||||
@@ -232,7 +234,7 @@ e41619e5cb0ba01a3d287748f28eb38bbe7641f9bedade252d286c18e619c400 src/admin/web/
|
||||
22318f39f7bb1ae98e65737d3c2ad9e63065b8e94ad59bed560c0e80d3a46949 src/admin/web/redirects.js
|
||||
668f5162fa9f8bbe2fefce4115644a6469e01aa7defc9c7f760c28afd8e2afc7 src/admin/web/settings.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
|
||||
d818580e61bbb70da1f758b9231c0667639cd947d875a55adc21dcf9ed1319c2 src/admin/web/transform-panel.js
|
||||
ac79a0f5471f110850bf5ceaaee9a3903bb92fb2edc10e969d8fda81f66571c0 src/admin/web/vhosts/core.js
|
||||
@@ -308,11 +310,11 @@ ad47db6519e471ec53a408129bb10a794033430e28fcd3d9a4cf273669900829 src/compressio
|
||||
7921ae451ccd65d647165b02919bd97399399da471454a6a1a1f63e682b76fc8 src/config.rs
|
||||
36c8774fe6fdc44d13286b7688726ceaa9b55cee4c63113689df63456cbecaac src/config/backend.rs
|
||||
8b39e92594133c32b6e113c67849fc10f51ceec9e3bbf69211779f6628d0e001 src/config/compile.rs
|
||||
66b3061200c06c4fffe3350e3807bd9948b9b05aa91410da5d05edef1e0d791d src/config/model.rs
|
||||
bb1059f07a0e7bbffeb2575334bbe5e95d1f3167c9fe0769696f585523a42471 src/config/model.rs
|
||||
4d2a4a078e890a9d1582e2cc6f9b8f0e8c2c676c4c94bc927bffdbe55e932610 src/config/routing.rs
|
||||
9bc22e19b7cfd3b7555faac8497c7410ef48e8c6b4eb3515f77f6a0e2f251c5d src/config/snapshot.rs
|
||||
9662674d17740df4999f28bec42a2fbd3453a93e591c88f0796edc396bb23a18 src/config/snapshot.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
|
||||
fa4e955ad23c0b5b3fb25af9f144c5c483f116fb79568300543b6501575d9e18 src/db.rs
|
||||
2ff744223934685439374572b83fd639bb3eabdbac168bea4a6501a6e9ce0ee0 src/db/history.rs
|
||||
@@ -330,23 +332,23 @@ fe571c85a924c827097f4be01b3a827a16bc5dd17a55fbf991ee3ad016641305 src/docker/tes
|
||||
e40171e72327bc4350ba95cedacde2f3c9148c26a5a232d9f790f4449f20a685 src/geoip/tests.rs
|
||||
650818b60f76e1ae1a26c6fbf71d7ba63bc85dcb88754a592e8da87b5938e5af src/headers.rs
|
||||
01cb98570ec388170420883e4ba18b2bf85ad1af97854dbc3a12b135db511c5c src/headers/tests.rs
|
||||
1988f116779d725d324fe7b8af0eed071916efaffb38636b55e6276966a36056 src/http3.rs
|
||||
d0cd8589321595309834690fa9ddb98488886bc98f34ea45aa0ccb3035412fb5 src/http3.rs
|
||||
2165a0a4b27d621985e2030326ad52b8bbd59d7b9e4f092f72f7e92b4e58b497 src/http3/body.rs
|
||||
68bc11160bc5275a904e48af8698b37433096fb6d592364d8281d59bb9e2b3f4 src/http3/tests.rs
|
||||
3dab8fac75263120e48ee869ad55a3e2ccc2a19fc9f6d6a6fe055657ec55b1e3 src/lib.rs
|
||||
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
|
||||
d8abc31f130e85b38c760b96e3369639464bca513991532e025485489a0af70e src/logging/error_runtime.rs
|
||||
2ee000906a56219cda4124b5bafad4af326a70ff0baa160d8f920ad90c5ebef9 src/logging/format.rs
|
||||
15f88b956b2a4804ba82f493ab20e8bd1ca57920a9fd218e9e3d85867c796d81 src/logging/model.rs
|
||||
c4c2f77e4e64943ac7e5d26688743a19e1349d2c984fec20dcff70373af1c254 src/logging/runtime.rs
|
||||
a978bb38c87336145395d598213718ee9ede41e82e3ab89883fdb8e3b024d944 src/logging/tests.rs
|
||||
a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/format.rs
|
||||
83e7bd1517ede4a2d1696ba0b22bdd5d34a4c29fcc8eb5c30c454dc4eba66853 src/logging/model.rs
|
||||
7c0d8e8f46f96cbaf3a5cb5fd47953f9185d7558dd71e599676bc63578b7ca99 src/logging/runtime.rs
|
||||
17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs
|
||||
3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs
|
||||
46732ed6d9cf2a33031624f941b8318d2676fb1b6bb5142e96820301f9fa1ac4 src/main/admin_runtime.rs
|
||||
86d62387fb73c221aeed3d0136b4b4954e5feac8533f8b3490b585d17fb743ac src/main/bootstrap.rs
|
||||
7d1ed0a639ed79b914d609d913b3004d5da7ad60695a487799ef42bcc4d74c9c src/main/bootstrap.rs
|
||||
5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs
|
||||
4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs
|
||||
0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs
|
||||
7110171b265945d44a45ca09a02c9860e7bbae4e9aec2bc9583c7a65a3fea907 src/main/tasks.rs
|
||||
d2b53a7681476f368b169a1c3bb2e4646af0c27084e52cd45cc7c37a2451cfcb src/main/tasks.rs
|
||||
49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs
|
||||
201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs
|
||||
49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs
|
||||
@@ -362,7 +364,7 @@ d3c6b08088df0003144544936a55e2a3661aac85292821b594f00adfd19a2951 src/prometheus
|
||||
064b2be7fed2f6acf526b1cce070c1bb1b943d36f08fa50bbfcbdf3cf1c9318c src/prometheus/tests.rs
|
||||
028e1dd9f2ec2afaf699a6b21c2d4b0d96e76f11f5e4ed009c233dadf0f3be51 src/prometheus/traffic.rs
|
||||
e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus/vhosts_backends.rs
|
||||
b4ab7010056ff095e4c4d2f878d2301b8ce626bd2059ef4fd54c3647c16d4f61 src/proxy/body.rs
|
||||
b5856fd38a957b2ca9c7e3606d84d13d5cfcf3a45b487b15ae1d2dc248cbda57 src/proxy/body.rs
|
||||
16b9d4db3b20b2ab14c47ac14fcd18d3d8ca77c3ad9a1f37866ebfa60f379862 src/proxy/body/tests.rs
|
||||
00e50a914511754266b4eae2626244587174bfa71fff91509c530bcde7f0d397 src/proxy/cache_helpers.rs
|
||||
cfb0910af8cd868bde3f6c9992319e46d5328d6d8f69c50701df0ea12bef2108 src/proxy/forward/cache_lookup.rs
|
||||
@@ -399,11 +401,11 @@ b4c17b304166592e63c81884076589fbaf725c3f57043f8924c46167b061ac5b src/secret_sto
|
||||
6fa1d8c352bce7a16ff0208bc4cdbe80404715df0aff4ed26910a5df57cbbcd4 src/security.rs
|
||||
0e5fc2aff856bf6fd959cba12b5ddd1e3b3cbeb0fbaadde906c811cdfafdaae7 src/security/tests.rs
|
||||
53a4bed4d1233aa434712b946857ea2050acaaf5de011c6dadca5b5c22155a83 src/server.rs
|
||||
a5495ac40a75d8c94b6f1c47b7e9c0946f836d40735464d4d18ce6705541d063 src/server/http.rs
|
||||
f2f6196632b2fdb2444189624f961edaf5bdfff57f700b0207b57e2491c1daa5 src/server/http.rs
|
||||
8c6ca5eaffb9acab57199d0349722703b903cfff3752baad2cf2596bd51188de src/server/listeners.rs
|
||||
c362593f2feb7cb3b22b2a4fb22f8afa128e72ca8185111ceb9bd0f8125abe55 src/server/serve.rs
|
||||
b209c1135aa30c5a2d1edb3f8a80b9b95398b2c2e4c03139b59b8a4955f1eb99 src/server/serve/tests.rs
|
||||
c123c2e1bfc127621a5c8915ad3e0a32f57d557b2fd8760f1cfc537d703e1a02 src/server/tls_runtime.rs
|
||||
71899a9b1897fa22ab9e8eb6a670054a6d382bad78714006b9b218d07ba99902 src/server/serve.rs
|
||||
e9e478cf45b88994eaffd115f51764b6b07852ee3c941cafdabb20584d3dabc0 src/server/serve/tests.rs
|
||||
048f2c7a8ef698a172ba7ed844558f4118b367b2cf340748c5c28da7723d8e85 src/server/tls_runtime.rs
|
||||
1be88858215d393d4f62f04f36a77ee64ce476be1b642c8f37e059c33423fea7 src/settings.rs
|
||||
b8b3150abe1c6f61d3f593e169795aae397b578607710ea6fd887b90f37b872e src/settings/tests.rs
|
||||
566daf9c0e1a30da2dc18e51090541a85ced3320a0fc00ed1d1de3e2b4da5072 src/stream.rs
|
||||
@@ -453,7 +455,7 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
|
||||
9c1c8d6eedecb95eadbfb23c3caea562f6f7a995606ef988f25b58c2d03bcdd5 src/web_server/config/model.rs
|
||||
6191f4d541476c4a3ae296e7dd28bdb9f8a123fc509b47619f6f44519c44d278 src/web_server/config/validation.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
|
||||
8186363315bd998919c6c783e6f7501528f257a5c0da77c5d60f423e8670db5e src/web_server/filesystem.rs
|
||||
0aafd7f37c4fe784b047646e4ce5ddcb5fe821231a41ed0a9f519529385288a8 src/web_server/htaccess.rs
|
||||
@@ -463,14 +465,14 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali
|
||||
80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs
|
||||
67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.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
|
||||
2268a6aeed2f565f9791af33e1cd21c56c228c8873b031725b39a6c57184f2d1 src/web_server/routing.rs
|
||||
400e1c8aa2fe857d96036a64499fbc3de44c1e3a53811c5b25101fb843359792 src/web_server/security.rs
|
||||
3e0b4e92600a7d5abcec92efed532b3a01e0781cb4f2681daac6d6ae13b0a6e9 src/web_server/serve.rs
|
||||
cf86c4692080723bbdf8f4105e1abfae33d7fcf020d2ad9e13ebae3943bd0cbe src/web_server/tests.rs
|
||||
c62752fe2fa6b74ddc9d94d36f8d191376b1575b15d21fc434c5a5da4c932692 src/web_server/tests/part_01.rs
|
||||
8c15c626e879016d5d3e3f0c30af42aa9acd18ce55b6965976ca6895e0af9028 src/web_server/tests/part_02.rs
|
||||
070141f7e6f0a5659ac18f90e03fd73a73767aded81d752c342440dbee80b5f0 src/web_server/tests/part_02.rs
|
||||
e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs
|
||||
f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json
|
||||
1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
# 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:
|
||||
|
||||
- `/usr/bin/rust-proxy`
|
||||
|
||||
Vendored
+1
-1
@@ -21,6 +21,6 @@ cargo fetch --locked
|
||||
dpkg-buildpackage -us -uc -b
|
||||
|
||||
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/ \;
|
||||
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
|
||||
|
||||
* 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
|
||||
help|-h|--help)
|
||||
cat <<'HELP'
|
||||
RustProxy 0.26.0
|
||||
RustProxy 0.26.1
|
||||
./dev.sh Build and start proxy + local panel
|
||||
./dev.sh run [serve flags] Same, with extra server options
|
||||
./dev.sh admin setup-token Generate/print a one-time setup capability before first login
|
||||
@@ -139,7 +139,7 @@ if [[ "$mode" == "test" ]]; then
|
||||
printf 'Checking all Rust targets before tests...\n' >&2
|
||||
cargo check --locked --all-targets
|
||||
fi
|
||||
printf 'Building RustProxy 0.26.0...\n' >&2
|
||||
printf 'Building RustProxy 0.26.1...\n' >&2
|
||||
cargo build --locked
|
||||
BIN="$CARGO_TARGET_DIR/debug/rust-proxy"
|
||||
case "$mode" in
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package main
|
||||
|
||||
const version = "0.26.0"
|
||||
const version = "0.26.1"
|
||||
|
||||
type config struct {
|
||||
Listen string
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Administration API access - 0.26.0
|
||||
# Administration API access - 0.26.1
|
||||
|
||||
## Sign-in boundary
|
||||
|
||||
|
||||
+45
-19
@@ -1,44 +1,72 @@
|
||||
# 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
|
||||
|
||||
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 combined structured access-log path, or JSONL files directly inside the configured per-vhost directory;
|
||||
- the configured optional error-log file.
|
||||
|
||||
Existing files remain browsable after their writer is disabled. Disabling file logging therefore stops new persistent access/protocol records but does not remove old files.
|
||||
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
|
||||
{
|
||||
"event": "protocol_error",
|
||||
"reason": "tls_handshake_error",
|
||||
"protocol_error": "tls_handshake_error",
|
||||
"error": "...",
|
||||
"error_detail": "...",
|
||||
"client_ip": "192.0.2.10",
|
||||
"local_addr": "192.0.2.20:443",
|
||||
"tls_sni": "app.example",
|
||||
"transport": "tls",
|
||||
"http_version": "",
|
||||
"status": 0,
|
||||
"outcome": "protocol_error"
|
||||
"vhost": "app",
|
||||
"sni": "app.example",
|
||||
"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`.
|
||||
|
||||
@@ -46,16 +74,14 @@ The reader is bounded:
|
||||
|
||||
- up to 2 MiB scanned from the tail of one source;
|
||||
- up to 8 MiB across one query;
|
||||
- up to 20,000 lines across the complete query, shared fairly across selected sources;
|
||||
- up to 20,000 lines across the complete query, shared across selected sources;
|
||||
- at most 32 sources in an `all` query;
|
||||
- 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.
|
||||
|
||||
The endpoints are private administration routes and inherit the existing panel session/Host/Origin access controls. There is no public-proxy route to the file reader.
|
||||
|
||||
## Filters
|
||||
|
||||
The workspace supports source, case-insensitive text, event class, HTTP status class, client IP and vhost filters. Results are paged from the bounded scanned tail. Selecting a row shows the complete parsed JSON object. RAW error-log lines are wrapped as a record with `event: "error"` and a `raw` field.
|
||||
|
||||
The explorer is diagnostic rather than a retention system. RustProxy does not add rollover, indexing or compression; use the existing operating-system log rotation policy for retention.
|
||||
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.
|
||||
+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
|
||||
verified production build**: Rust/Cargo, real CA operations, live systemd startup
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# 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
|
||||
|
||||
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.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.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.
|
||||
|
||||
## Functional documentation
|
||||
@@ -60,7 +61,7 @@ This is the canonical index for the current project documentation. The authentic
|
||||
|
||||
## Historical or version-labelled context
|
||||
|
||||
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone; 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.
|
||||
|
||||
|
||||
+17
-32
@@ -1,55 +1,40 @@
|
||||
# Verification report - RustProxy 0.26.0
|
||||
# Verification report - RustProxy 0.26.1
|
||||
|
||||
## 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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## Verification status in this workspace
|
||||
|
||||
| Check | Status |
|
||||
|---|---|
|
||||
| `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/web_server_contracts.py` | PASS |
|
||||
| `python3 scripts/security_contracts.py` | PASS |
|
||||
| `python3 scripts/security_dependency_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 |
|
||||
| `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 |
|
||||
| `python3 scripts/check_source.py` | PASS; explicitly reports Rust parsing/types/runtime not verified |
|
||||
| `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 test` | Rust-independent gates PASS; exits 127 when the Rust stage is reached because Cargo is unavailable |
|
||||
| `./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.
|
||||
|
||||
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
|
||||
will simply show no series.
|
||||
|
||||
|
||||
@@ -4529,9 +4529,9 @@
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "browser",
|
||||
"title": "RustProxy 0.26.0 - Operations",
|
||||
"title": "RustProxy 0.26.1 - Operations",
|
||||
"uid": "rustproxy-0170",
|
||||
"version": 5,
|
||||
"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...",
|
||||
"fileLogsTruncated": "scan limit reached",
|
||||
"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",
|
||||
"all": "All",
|
||||
"clientIp": "Client IP",
|
||||
|
||||
@@ -1995,6 +1995,11 @@
|
||||
"fileLogsSearchPlaceholder": "IP, ścieżka, błąd, request id...",
|
||||
"fileLogsTruncated": "osiągnięto limit skanowania",
|
||||
"fileLogsSummary": "{from}–{to} z {total} wyników · przeskanowano {bytes} / {lines} linii",
|
||||
"fileLogsMatchedBy": "Dopasowanie przez",
|
||||
"fileLogsSni": "SNI",
|
||||
"fileLogsAuthority": "Authority / Host",
|
||||
"fileLogsColumnsRequired": "Co najmniej jedna kolumna zapisanych logów musi pozostać widoczna.",
|
||||
"fileLogsSourcesRead": "Skonfigurowane pliki",
|
||||
"search": "Szukaj",
|
||||
"all": "Wszystkie",
|
||||
"clientIp": "IP klienta",
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@ def add_file(archive: zipfile.ZipFile, rel: Path, data: bytes, timestamp: tuple[
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.0.zip')
|
||||
parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.1.zip')
|
||||
output = parser.parse_args().output.resolve()
|
||||
paths = files()
|
||||
checksum = ROOT / 'SHA256SUMS'
|
||||
|
||||
+10
-10
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
|
||||
|
||||
preflight(root)
|
||||
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"
|
||||
h3dep = manifest["dependencies"]["h3"]
|
||||
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"
|
||||
if lock_path.exists():
|
||||
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"
|
||||
for release_doc, marker in {
|
||||
"README.md": "# RustProxy 0.26.0",
|
||||
"CHANGELOG.md": "## 0.26.0",
|
||||
"ROADMAP.md": "## 0.26.0",
|
||||
"AGENTS.md": "Current source version: 0.26.0.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.0",
|
||||
"README.md": "# RustProxy 0.26.1",
|
||||
"CHANGELOG.md": "## 0.26.1",
|
||||
"ROADMAP.md": "## 0.26.1",
|
||||
"AGENTS.md": "Current source version: 0.26.1.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.1",
|
||||
}.items():
|
||||
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"
|
||||
@@ -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 '"backend_header"' in openapi and '"backend_timing_header"' 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
|
||||
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
|
||||
@@ -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 '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 '"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.
|
||||
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"]
|
||||
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.")
|
||||
@@ -97,7 +97,7 @@ def digest(path: Path) -> str:
|
||||
def fetch_https(url: str, max_bytes: int) -> bytes:
|
||||
if urllib.parse.urlparse(url).scheme != 'https':
|
||||
raise ValueError('GeoIP downloads require HTTPS.')
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.0'})
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.1'})
|
||||
with urllib.request.urlopen(request, timeout=30) as response:
|
||||
if urllib.parse.urlparse(response.geturl()).scheme != 'https':
|
||||
raise ValueError('GeoIP download redirected away from HTTPS.')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/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
|
||||
import re
|
||||
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")
|
||||
roadmap = (ROOT / "ROADMAP.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 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 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 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 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
|
||||
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}"
|
||||
|
||||
@@ -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)}")
|
||||
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():
|
||||
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")
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ def upgrade_smoke(tmp: Path) -> None:
|
||||
assert backup.is_dir()
|
||||
assert (backup / "src/obsolete.rs").is_file()
|
||||
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 / ".env").read_text(encoding="utf-8") == "RUST_PROXY_DEMO=0\n"
|
||||
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/server.go").is_file()
|
||||
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 not legacy.exists()
|
||||
assert not (backup / "legacy-data").exists()
|
||||
@@ -554,7 +554,7 @@ def source_contract_smoke() -> None:
|
||||
assert marker in admin, marker
|
||||
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 '"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
|
||||
for marker in ['id="settings-tab-logging"', 'id="settings-page-logging"', 'id="logging-form"', 'id="error-log-form"']:
|
||||
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
|
||||
openapi = (ROOT / "src/admin/openapi.json").read_text(encoding="utf-8")
|
||||
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 '"software_update"' in openapi and '"update_available"' in openapi
|
||||
for marker in ['"/api/certificates/deploy"', '"CertificateDeploy"', '"CertificateDeployResult"', '"/api/certificates/reload"']:
|
||||
@@ -1186,7 +1186,7 @@ def main() -> None:
|
||||
source_contract_smoke()
|
||||
container_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__":
|
||||
|
||||
@@ -85,7 +85,7 @@ def download(destination: Path) -> None:
|
||||
"""TLS verification uses Python's default trust store; never executes a shell."""
|
||||
started = time.monotonic()
|
||||
opener = build_opener(HttpsRedirects())
|
||||
request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.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:
|
||||
length = response.headers.get("Content-Length")
|
||||
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:
|
||||
if (target / name).is_symlink():
|
||||
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)
|
||||
installed: list[str] = []
|
||||
moved: list[str] = []
|
||||
@@ -84,7 +84,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None
|
||||
remove_legacy = legacy.exists()
|
||||
if remove_legacy and (legacy.is_symlink() or not legacy.is_file()):
|
||||
raise ValueError("Obsolete renewal observation sidecar must be a regular file.")
|
||||
(backup / "upgrade.json").write_text(json.dumps({"from_directory":str(target), "to_version":"0.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:
|
||||
legacy.unlink()
|
||||
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)
|
||||
except (OSError, ValueError) as error:
|
||||
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(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh")
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ need(web,
|
||||
'HeaderValue::from_static("no-store")',
|
||||
"builtin_error_response(StatusCode::FORBIDDEN, &method)",
|
||||
"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",
|
||||
"try_files_rewrites_cache_rules_headers_and_downloads_work",
|
||||
"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.26.1_IMPLEMENTATION.md",
|
||||
"technical",
|
||||
include_str!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/docs/V0.26.1_IMPLEMENTATION.md"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"docs/V0.25.5_AUDIT.md",
|
||||
"audits",
|
||||
|
||||
+75
-6
@@ -229,12 +229,62 @@ fn read_tail(
|
||||
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}));
|
||||
if !value.is_object() {
|
||||
value = json!({"value": value});
|
||||
}
|
||||
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(|| {
|
||||
Value::String(
|
||||
if source.kind == "error" {
|
||||
@@ -256,12 +306,27 @@ fn contains_ci(value: Option<&Value>, needle: &str) -> bool {
|
||||
.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 {
|
||||
let Some(object) = value.as_object() else {
|
||||
return false;
|
||||
};
|
||||
if !q.is_empty() && !value.to_string().to_ascii_lowercase().contains(q) {
|
||||
return false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
if let Some(event) = query
|
||||
.event
|
||||
@@ -345,7 +410,9 @@ async fn file_logs_query(
|
||||
let config = state.config.load().envelope.config.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
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 mut truncated = false;
|
||||
if requested != "all" {
|
||||
sources.retain(|source| source.id == requested);
|
||||
if sources.is_empty() {
|
||||
@@ -356,7 +423,10 @@ async fn file_logs_query(
|
||||
}
|
||||
} else {
|
||||
sources.sort_by(|a, b| b.modified_ms.cmp(&a.modified_ms));
|
||||
sources.truncate(FILE_LOG_MAX_SOURCES);
|
||||
if sources.len() > FILE_LOG_MAX_SOURCES {
|
||||
truncated = true;
|
||||
sources.truncate(FILE_LOG_MAX_SOURCES);
|
||||
}
|
||||
}
|
||||
let q = query.q.clone().unwrap_or_default().to_ascii_lowercase();
|
||||
let offset = query.offset.unwrap_or(0);
|
||||
@@ -365,7 +435,6 @@ async fn file_logs_query(
|
||||
let mut scanned_bytes = 0u64;
|
||||
let mut remaining_lines = FILE_LOG_SCAN_LINES;
|
||||
let mut scanned_lines = 0usize;
|
||||
let mut truncated = false;
|
||||
let mut records = Vec::new();
|
||||
for (index, source) in sources.iter().enumerate() {
|
||||
if remaining_bytes == 0 || remaining_lines == 0 {
|
||||
@@ -385,7 +454,7 @@ async fn file_logs_query(
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let value = normalized_record(source, &line);
|
||||
let value = normalized_record(source, &line, &log_format);
|
||||
if record_matches(&value, &query, &q) {
|
||||
records.push(value);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
include!("router.rs");
|
||||
include!("access.rs");
|
||||
include!("session.rs");
|
||||
include!("preferences.rs");
|
||||
include!("status.rs");
|
||||
include!("logging_api.rs");
|
||||
include!("file_logs.rs");
|
||||
|
||||
+33
-1
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"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."
|
||||
},
|
||||
"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."
|
||||
},
|
||||
"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": {
|
||||
"type": "string",
|
||||
"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))
|
||||
}
|
||||
|
||||
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 {
|
||||
let mut response = Json(json!({"logged_out":true})).into_response();
|
||||
response.headers_mut().insert(
|
||||
|
||||
@@ -20,6 +20,10 @@
|
||||
LIVE_LOG_COLUMNS.len()
|
||||
);
|
||||
assert_eq!(initial["theme"], "dark");
|
||||
assert_eq!(
|
||||
initial["file_log_columns"].as_array().unwrap().len(),
|
||||
default_file_log_columns().len()
|
||||
);
|
||||
assert_eq!(
|
||||
initial["backend_table_columns"].as_array().unwrap().len(),
|
||||
BACKEND_TABLE_COLUMNS.len()
|
||||
@@ -45,6 +49,7 @@
|
||||
let legacy = json_body(response).await;
|
||||
assert_eq!(legacy["live_log_columns"], json!(["time", "status"]));
|
||||
assert_eq!(legacy["theme"], "dark");
|
||||
assert_eq!(legacy["file_log_columns"], json!(default_file_log_columns()));
|
||||
assert_eq!(
|
||||
legacy["backend_table_columns"].as_array().unwrap().len(),
|
||||
BACKEND_TABLE_COLUMNS.len()
|
||||
@@ -55,6 +60,7 @@
|
||||
let selected = json!({
|
||||
"live_log_columns":["time","request","status","upstream_error"],
|
||||
"theme":"light",
|
||||
"file_log_columns":["time","event","client_ip","record"],
|
||||
"backend_table_columns":["vhost","backend","status","requests","latency"],
|
||||
"backend_table_sort":"requests",
|
||||
"backend_table_descending":true
|
||||
|
||||
@@ -55,6 +55,37 @@
|
||||
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)]
|
||||
#[test]
|
||||
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); }
|
||||
}
|
||||
|
||||
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); } }
|
||||
let tunnelRefreshTimer = null;
|
||||
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")) {
|
||||
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
|
||||
$("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];
|
||||
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
|
||||
}
|
||||
|
||||
+95
-15
@@ -1,7 +1,28 @@
|
||||
(() => {
|
||||
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 currentLimit = () => Math.max(1, Math.min(500, Number($("file-log-limit").value) || 100));
|
||||
|
||||
function sourceOption(source) {
|
||||
const label = `${source.label} · ${source.format} · ${bytes(source.size || 0)}${source.enabled ? ` · ${t("enabled")}` : ""}`;
|
||||
const option = el("option", label); option.value = source.id; return option;
|
||||
@@ -26,8 +47,9 @@
|
||||
return "—";
|
||||
}
|
||||
function entrySummary(entry) {
|
||||
if (entry.protocol_error) return entry.protocol_error;
|
||||
if (entry.method || entry.path) return `${entry.method || ""} ${entry.host || ""}${entry.path || ""}`.trim();
|
||||
if (entry.reason || entry.protocol_error) return entry.reason || entry.protocol_error;
|
||||
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.raw) return entry.raw;
|
||||
return entry.outcome || "—";
|
||||
@@ -36,19 +58,72 @@
|
||||
const status = Number(entry.status) || 0;
|
||||
return status ? String(status) : "—";
|
||||
}
|
||||
function renderRows(entries) {
|
||||
const rows = entries.map(entry => {
|
||||
const row = el("tr"); row.tabIndex = 0;
|
||||
for (const value of [entryTime(entry), entry.event || "—", entryStatus(entry), entry.client_ip || "—", entry.vhost || "—", entrySummary(entry), entry._source || "—"]) {
|
||||
function syncHeaders() {
|
||||
document.querySelectorAll("#file-logs-table-head [data-file-log-column]").forEach(header => {
|
||||
header.hidden = !visibleColumns.has(header.dataset.fileLogColumn);
|
||||
});
|
||||
}
|
||||
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 show = () => { $("file-log-detail").textContent = JSON.stringify(entry, null, 2); };
|
||||
row.addEventListener("click", show); row.addEventListener("keydown", event => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); show(); } });
|
||||
return row;
|
||||
const select = () => { selectedIndex = selectedIndex === index ? null : index; renderRows(); };
|
||||
row.addEventListener("click", select);
|
||||
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-logs-empty").hidden = rows.length !== 0;
|
||||
if (!rows.length) $("file-log-detail").textContent = t("fileLogsSelectRecord");
|
||||
$("file-log-rows").replaceChildren(...nodes);
|
||||
$("file-logs-empty").hidden = entries.length !== 0;
|
||||
}
|
||||
function queryString() {
|
||||
const params = new URLSearchParams();
|
||||
@@ -66,7 +141,8 @@
|
||||
try {
|
||||
if (!inventory) await loadInventory();
|
||||
const value = await api(`/api/file-logs/query?${queryString()}`);
|
||||
lastTotal = Number(value.total) || 0; renderRows(Array.isArray(value.entries) ? value.entries : []);
|
||||
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);
|
||||
$("file-log-summary").textContent = t("fileLogsSummary")
|
||||
.replace("{from}", fmt(from)).replace("{to}", fmt(to)).replace("{total}", fmt(lastTotal))
|
||||
@@ -78,14 +154,18 @@
|
||||
finally { busy = false; $("file-logs-refresh").disabled = false; }
|
||||
}
|
||||
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"));
|
||||
$("settings-file-logs-open").addEventListener("click", () => setPanel("filelogs"));
|
||||
$("file-logs-refresh").addEventListener("click", () => refresh(true));
|
||||
$("file-logs-filter").addEventListener("submit", event => { event.preventDefault(); void refresh(true); });
|
||||
$("file-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-next").addEventListener("click", () => { if (offset + currentLimit() < lastTotal) { offset += currentLimit(); void refresh(); } });
|
||||
sourceSelect().addEventListener("change", () => refresh(true));
|
||||
window.rustProxyFileLogs = { refresh: () => refresh(false), reset, inventory: loadInventory };
|
||||
syncColumnPicker(); syncHeaders();
|
||||
window.rustProxyFileLogs = { refresh: () => refresh(false), reset, inventory: loadInventory, applyPreferences };
|
||||
})();
|
||||
+23
-16
@@ -1539,20 +1539,31 @@
|
||||
<option>500</option>
|
||||
</select></label>
|
||||
<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>
|
||||
<p class="fine file-log-inventory" id="file-log-inventory"></p>
|
||||
<details class="file-log-inventory-details">
|
||||
<summary data-i18n="fileLogsSourcesRead">Configured files</summary>
|
||||
<p class="fine file-log-inventory" id="file-log-inventory"></p>
|
||||
</details>
|
||||
<p class="fine" id="file-log-summary" role="status"></p>
|
||||
<div class="table-scroll file-logs-table">
|
||||
<table>
|
||||
<thead>
|
||||
<thead id="file-logs-table-head">
|
||||
<tr>
|
||||
<th data-i18n="time">Time</th>
|
||||
<th data-i18n="fileLogsEvent">Event</th>
|
||||
<th data-i18n="status">Status</th>
|
||||
<th data-i18n="clientIp">Client IP</th>
|
||||
<th data-i18n="vhost">Vhost</th>
|
||||
<th data-i18n="fileLogsRecord">Request / reason</th>
|
||||
<th data-i18n="fileLogsSource">Source</th>
|
||||
<th data-file-log-column="time" data-i18n="time">Time</th>
|
||||
<th data-file-log-column="event" data-i18n="fileLogsEvent">Event</th>
|
||||
<th data-file-log-column="status" data-i18n="status">Status</th>
|
||||
<th data-file-log-column="client_ip" data-i18n="clientIp">Client IP</th>
|
||||
<th data-file-log-column="vhost" data-i18n="vhost">Vhost</th>
|
||||
<th data-file-log-column="transport" data-i18n="logTransport" hidden>Transport</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>
|
||||
</thead>
|
||||
<tbody id="file-log-rows"></tbody>
|
||||
@@ -1563,10 +1574,6 @@
|
||||
<div class="file-logs-pager"><button class="quiet" data-i18n="previous" disabled
|
||||
id="file-logs-prev" type="button">Previous</button><button class="quiet"
|
||||
data-i18n="next" disabled id="file-logs-next" type="button">Next</button></div>
|
||||
<details open class="file-log-detail-card">
|
||||
<summary data-i18n="fileLogsDetails">Record details</summary>
|
||||
<pre class="metrics-example" id="file-log-detail"></pre>
|
||||
</details>
|
||||
</article>
|
||||
</section>
|
||||
<section class="panel" hidden="" id="panel-settings">
|
||||
@@ -1742,7 +1749,7 @@
|
||||
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
|
||||
</select>
|
||||
<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" />
|
||||
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
|
||||
<div class="switch-grid">
|
||||
@@ -2541,7 +2548,7 @@
|
||||
</section>
|
||||
</div>
|
||||
</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>
|
||||
</div>
|
||||
</main>
|
||||
@@ -3252,7 +3259,7 @@ max_execution_time = 30</code></pre>
|
||||
<article class="card about-card">
|
||||
<div class="about-product">
|
||||
<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>
|
||||
<dl class="about-meta">
|
||||
<div>
|
||||
|
||||
+49
-11
@@ -8207,7 +8207,7 @@ button.config-history-day.is-selected {
|
||||
|
||||
.file-logs-filter {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(120px, 1fr));
|
||||
grid-template-columns: repeat(8, minmax(120px, 1fr));
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
}
|
||||
@@ -8220,14 +8220,31 @@ button.config-history-day.is-selected {
|
||||
grid-column: span 2;
|
||||
}
|
||||
|
||||
.file-logs-table {
|
||||
max-height: 52vh;
|
||||
.file-log-column-picker {
|
||||
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;
|
||||
}
|
||||
|
||||
.file-logs-table tbody tr.selected > td {
|
||||
background: var(--panel2);
|
||||
box-shadow: inset 3px 0 0 var(--accent);
|
||||
}
|
||||
|
||||
.file-logs-table td {
|
||||
max-width: 340px;
|
||||
overflow: hidden;
|
||||
@@ -8235,19 +8252,33 @@ button.config-history-day.is-selected {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-log-detail-card pre {
|
||||
max-height: 32vh;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.file-logs-launcher {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -8255,7 +8286,14 @@ button.config-history-day.is-selected {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.file-log-inventory-details > summary {
|
||||
cursor: pointer;
|
||||
color: var(--muted);
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.file-log-inventory {
|
||||
margin: 8px 0 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
|
||||
@@ -238,6 +238,12 @@ pub struct CompiledFrontend {
|
||||
pub pages: crate::pages::VhostPages,
|
||||
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 vhost_metrics: std::collections::BTreeMap<String, Arc<crate::metrics::VhostMetrics>>,
|
||||
pub forwarding: crate::forwarding::Compiled,
|
||||
|
||||
@@ -93,6 +93,54 @@ impl Snapshot {
|
||||
}
|
||||
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.
|
||||
/// Exact hostnames win over the longest matching wildcard, matching vhost behavior.
|
||||
pub fn redirect_on(
|
||||
|
||||
@@ -187,6 +187,57 @@ fn wildcard_host_patterns_are_normalized_and_exact_hosts_win() {
|
||||
"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]
|
||||
fn reusable_acl_profile_reference_is_compiled_and_unknown_reference_fails() {
|
||||
let mut c = demo_config("127.0.0.1:9000".parse().unwrap());
|
||||
|
||||
+75
-16
@@ -28,7 +28,6 @@ use tokio::{
|
||||
task::JoinSet,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
@@ -191,6 +190,63 @@ impl Drop for Lease {
|
||||
.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<()> {
|
||||
let slots = Arc::new(Semaphore::new(
|
||||
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=>{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.active_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.tls_connections.fetch_add(1,Ordering::Relaxed);
|
||||
let _lease=Lease {_permit:permit,metrics:state.metrics.clone()};
|
||||
if let Err(error)=connection_loop(connection,local,router,state.clone(),limit,grace).await {
|
||||
state.logs.push_protocol_error(
|
||||
"http3_connection_error", &error, peer, local, "quic", "3", 0, None,
|
||||
if let Err(error)=connection_loop(connection,local,router,state.clone(),limit,grace,tls_sni.clone()).await {
|
||||
log_protocol_error(
|
||||
&state, "http3_connection_error", &error, peer, local, 0, None, None, tls_sni,
|
||||
);
|
||||
tracing::debug!(%error,"HTTP/3 connection ended");
|
||||
}
|
||||
@@ -290,6 +347,7 @@ async fn connection_loop(
|
||||
state: Arc<App>,
|
||||
limit: usize,
|
||||
grace: Duration,
|
||||
tls_sni: Option<String>,
|
||||
) -> Result<()> {
|
||||
let mut builder = h3::server::builder();
|
||||
builder.max_field_section_size(32 * 1024);
|
||||
@@ -316,27 +374,27 @@ async fn connection_loop(
|
||||
Ok(Some(resolver))=>{
|
||||
// Completed JoinSet entries are drained before accepting more.
|
||||
if requests.len()>=limit {
|
||||
state.logs.push_protocol_error(
|
||||
"http3_stream_limit_rejected", "HTTP/3 concurrent request stream limit reached",
|
||||
connection.remote_address(), local, "quic", "3", 0, None,
|
||||
log_protocol_error(
|
||||
&state, "http3_stream_limit_rejected", "HTTP/3 concurrent request stream limit reached",
|
||||
connection.remote_address(), local, 0, None, None, tls_sni.clone(),
|
||||
);
|
||||
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 {
|
||||
let resolved=tokio::time::timeout(state.header_timeout,resolver.resolve_request()).await;
|
||||
let (mut request,stream)=match resolved {
|
||||
Ok(Ok(request))=>request,
|
||||
Ok(Err(error))=>{
|
||||
state.logs.push_protocol_error(
|
||||
"http3_request_error", error, peer, local, "quic", "3", 400, None,
|
||||
log_protocol_error(
|
||||
&state, "http3_request_error", error, peer, local, 400, None, None, tls_sni.clone(),
|
||||
);
|
||||
return;
|
||||
},
|
||||
Err(_)=>{
|
||||
state.logs.push_protocol_error(
|
||||
"http3_header_timeout", "HTTP/3 request header timeout",
|
||||
peer, local, "quic", "3", 408, None,
|
||||
log_protocol_error(
|
||||
&state, "http3_header_timeout", "HTTP/3 request header timeout",
|
||||
peer, local, 408, None, None, tls_sni.clone(),
|
||||
);
|
||||
return;
|
||||
},
|
||||
@@ -347,9 +405,10 @@ async fn connection_loop(
|
||||
let websocket=extended_connect && protocol.as_deref()==Some("websocket");
|
||||
let (mut send,receive)=stream.split();
|
||||
if invalid_headers(request.headers()) {
|
||||
state.logs.push_protocol_error(
|
||||
"http3_invalid_headers", "HTTP/3 request contains forbidden connection-specific headers",
|
||||
peer, local, "quic", "3", 400, None,
|
||||
let (authority, host) = request_identity(&request);
|
||||
log_protocol_error(
|
||||
&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;
|
||||
}
|
||||
|
||||
+74
-2
@@ -2,9 +2,14 @@
|
||||
pub struct LogEntry {
|
||||
pub event: &'static str,
|
||||
pub protocol_error: Option<&'static str>,
|
||||
pub reason: Option<&'static str>,
|
||||
pub error_detail: Option<String>,
|
||||
pub error: Option<String>,
|
||||
pub local_addr: 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 route: Option<String>,
|
||||
pub cache_effective_enabled: Option<bool>,
|
||||
@@ -85,6 +90,21 @@ fn protocol_error_detail(detail: impl std::fmt::Display) -> String {
|
||||
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 {
|
||||
pub fn start(settings: Settings, shutdown: CancellationToken) -> Arc<Self> {
|
||||
let (tx, rx) = mpsc::channel(SINK_QUEUE);
|
||||
@@ -143,18 +163,47 @@ impl Logs {
|
||||
http_version: &'static str,
|
||||
status: u16,
|
||||
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() {
|
||||
return;
|
||||
}
|
||||
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 {
|
||||
event: "protocol_error",
|
||||
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()),
|
||||
tls_sni,
|
||||
vhost: None,
|
||||
sni,
|
||||
authority,
|
||||
matched_by: Some(matched_by),
|
||||
vhost,
|
||||
route: None,
|
||||
cache_effective_enabled: None,
|
||||
transform_effective_enabled: None,
|
||||
@@ -221,11 +270,20 @@ fn value_for(entry: &LogEntry, name: &str) -> Value {
|
||||
.protocol_error
|
||||
.map(|v| Value::String(v.into()))
|
||||
.unwrap_or(Value::Null),
|
||||
"reason" => entry
|
||||
.reason
|
||||
.map(|v| Value::String(v.into()))
|
||||
.unwrap_or(Value::Null),
|
||||
"error_detail" => entry
|
||||
.error_detail
|
||||
.clone()
|
||||
.map(Value::String)
|
||||
.unwrap_or(Value::Null),
|
||||
"error" => entry
|
||||
.error
|
||||
.clone()
|
||||
.map(Value::String)
|
||||
.unwrap_or(Value::Null),
|
||||
"local_addr" => entry
|
||||
.local_addr
|
||||
.clone()
|
||||
@@ -236,6 +294,20 @@ fn value_for(entry: &LogEntry, name: &str) -> Value {
|
||||
.clone()
|
||||
.map(Value::String)
|
||||
.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
|
||||
.clone()
|
||||
|
||||
@@ -231,9 +231,14 @@ fn source_value_name(name: &str) -> bool {
|
||||
name,
|
||||
"event"
|
||||
| "protocol_error"
|
||||
| "reason"
|
||||
| "error_detail"
|
||||
| "error"
|
||||
| "local_addr"
|
||||
| "tls_sni"
|
||||
| "sni"
|
||||
| "authority"
|
||||
| "matched_by"
|
||||
| "vhost"
|
||||
| "route"
|
||||
| "cache_effective_enabled"
|
||||
|
||||
+10
-18
@@ -19,11 +19,17 @@ fn format_entry(entry: &LogEntry, format: &JsonFormat) -> Value {
|
||||
for name in [
|
||||
"event",
|
||||
"protocol_error",
|
||||
"reason",
|
||||
"error_detail",
|
||||
"error",
|
||||
"time_ms",
|
||||
"client_ip",
|
||||
"local_addr",
|
||||
"tls_sni",
|
||||
"sni",
|
||||
"authority",
|
||||
"matched_by",
|
||||
"vhost",
|
||||
"transport",
|
||||
"http_version",
|
||||
"status",
|
||||
@@ -63,24 +69,10 @@ struct FileState {
|
||||
const PER_VHOST_OPEN_FILES: usize = 64;
|
||||
|
||||
fn vhost_log_name(vhost: Option<&str>) -> String {
|
||||
let raw = vhost
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("_unmatched");
|
||||
let mut safe = String::with_capacity(raw.len().min(96));
|
||||
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
|
||||
}
|
||||
vhost
|
||||
.filter(|value| crate::config::valid_id(value))
|
||||
.unwrap_or("_unmatched")
|
||||
.to_owned()
|
||||
}
|
||||
struct LogstashState {
|
||||
address: String,
|
||||
|
||||
+25
-1
@@ -5,9 +5,14 @@ mod tests {
|
||||
LogEntry {
|
||||
event: "request",
|
||||
protocol_error: None,
|
||||
reason: None,
|
||||
error_detail: None,
|
||||
error: None,
|
||||
local_addr: Some("192.0.2.1:443".into()),
|
||||
tls_sni: Some("app.example".into()),
|
||||
sni: Some("app.example".into()),
|
||||
authority: None,
|
||||
matched_by: None,
|
||||
vhost: Some("app".into()),
|
||||
route: Some("root".into()),
|
||||
cache_effective_enabled: Some(true),
|
||||
@@ -95,7 +100,7 @@ mod tests {
|
||||
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(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();
|
||||
settings.file.enabled = true;
|
||||
settings.file.per_vhost = true;
|
||||
@@ -124,7 +129,12 @@ mod tests {
|
||||
let mut item = entry();
|
||||
item.event = "protocol_error";
|
||||
item.protocol_error = Some("tls_handshake_error");
|
||||
item.reason = Some("tls_handshake_error");
|
||||
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.status = 0;
|
||||
item.transport = "tls";
|
||||
@@ -135,7 +145,12 @@ mod tests {
|
||||
assert_eq!(value["site"], "app");
|
||||
assert_eq!(value["event"], "protocol_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"], "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["transport"], "tls");
|
||||
assert_eq!(value["outcome"], "protocol_error");
|
||||
@@ -170,6 +185,15 @@ mod tests {
|
||||
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]
|
||||
fn error_log_defaults_off_and_redacts_common_secret_shapes() {
|
||||
let settings = ErrorLogSettings::default();
|
||||
|
||||
@@ -41,7 +41,7 @@ use zeroize::Zeroizing;
|
||||
#[command(
|
||||
name = "rust-proxy",
|
||||
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 {
|
||||
#[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();
|
||||
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()?; }
|
||||
let mut filters = LiveLogFilters::default();
|
||||
let mut history = std::collections::BTreeMap::<u64, serde_json::Value>::new();
|
||||
|
||||
@@ -204,9 +204,14 @@ impl Drop for Exchange {
|
||||
self.logs.push(LogEntry {
|
||||
event: "request",
|
||||
protocol_error: None,
|
||||
reason: None,
|
||||
error_detail: None,
|
||||
error: None,
|
||||
local_addr: None,
|
||||
tls_sni: None,
|
||||
sni: None,
|
||||
authority: None,
|
||||
matched_by: None,
|
||||
cache_reason: *self.cache_reason.lock().unwrap_or_else(|e| e.into_inner()),
|
||||
vhost: self.vhost.clone(),
|
||||
route,
|
||||
|
||||
+59
-12
@@ -88,6 +88,61 @@ pub struct Transport {
|
||||
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(
|
||||
state: Option<&Arc<App>>,
|
||||
kind: &'static str,
|
||||
@@ -99,18 +154,10 @@ fn log_protocol_error(
|
||||
status: u16,
|
||||
tls_sni: Option<String>,
|
||||
) {
|
||||
if let Some(state) = state {
|
||||
state.logs.push_protocol_error(
|
||||
kind,
|
||||
detail,
|
||||
peer,
|
||||
local,
|
||||
transport,
|
||||
http_version,
|
||||
status,
|
||||
tls_sni,
|
||||
);
|
||||
}
|
||||
log_protocol_error_with_identity(
|
||||
state, kind, detail, peer, local, transport, http_version, status, None, None,
|
||||
tls_sni,
|
||||
);
|
||||
}
|
||||
#[derive(Clone)]
|
||||
pub struct ServerShutdown(pub CancellationToken);
|
||||
|
||||
+3
-2
@@ -5,6 +5,7 @@ async fn http2_connection<I>(
|
||||
peer: SocketAddr,
|
||||
local: SocketAddr,
|
||||
transport: Transport,
|
||||
tls_sni: Option<String>,
|
||||
extended_connect: bool,
|
||||
frontend_state: Option<Arc<App>>,
|
||||
lease: Arc<ConnectionLease>,
|
||||
@@ -69,7 +70,7 @@ async fn http2_connection<I>(
|
||||
if let Err(error) = result {
|
||||
log_protocol_error(
|
||||
frontend_state.as_ref(), "http2_connection_error", &error, peer, local,
|
||||
if transport.tls { "tls" } else { "tcp" }, "2", 0, None,
|
||||
if transport.tls { "tls" } else { "tcp" }, "2", 0, tls_sni.clone(),
|
||||
);
|
||||
tracing::debug!(%error, "HTTP/2 connection closed");
|
||||
}
|
||||
@@ -108,7 +109,7 @@ async fn http2_connection<I>(
|
||||
if transport.tls { "tls" } else { "tcp" },
|
||||
"2",
|
||||
0,
|
||||
None,
|
||||
tls_sni.clone(),
|
||||
);
|
||||
tracing::debug!(?cause, %error, "HTTP/2 graceful shutdown ended with protocol error")
|
||||
}
|
||||
|
||||
@@ -100,7 +100,9 @@ mod h2c_tests {
|
||||
peer,
|
||||
local,
|
||||
Transport { tls: false },
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
lease,
|
||||
server_stop,
|
||||
Duration::from_secs(5),
|
||||
|
||||
@@ -125,6 +125,7 @@ async fn serve_inner(
|
||||
peer,
|
||||
local,
|
||||
Transport { tls: true },
|
||||
host.clone(),
|
||||
h2,
|
||||
extended_connect,
|
||||
false,
|
||||
@@ -168,6 +169,7 @@ async fn serve_inner(
|
||||
peer,
|
||||
local,
|
||||
Transport { tls: false },
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -197,6 +199,7 @@ async fn serve_inner(
|
||||
peer,
|
||||
local,
|
||||
Transport { tls: true },
|
||||
None,
|
||||
h2,
|
||||
false,
|
||||
false,
|
||||
@@ -245,6 +248,7 @@ async fn serve_inner(
|
||||
peer,
|
||||
local,
|
||||
Transport { tls: false },
|
||||
None,
|
||||
prior_h2,
|
||||
extended_connect,
|
||||
h2c && !prior_h2,
|
||||
@@ -276,6 +280,7 @@ async fn connection<I>(
|
||||
peer: SocketAddr,
|
||||
local: SocketAddr,
|
||||
transport: Transport,
|
||||
tls_sni: Option<String>,
|
||||
h2: bool,
|
||||
extended_connect: bool,
|
||||
h2c_upgrade: bool,
|
||||
@@ -294,6 +299,7 @@ async fn connection<I>(
|
||||
peer,
|
||||
local,
|
||||
transport,
|
||||
tls_sni.clone(),
|
||||
extended_connect,
|
||||
frontend_state.clone(),
|
||||
lease,
|
||||
@@ -322,7 +328,8 @@ async fn connection<I>(
|
||||
|
||||
if h2c_attempt(&request) {
|
||||
if !h2c_upgrade {
|
||||
log_protocol_error(
|
||||
let (authority, host) = request_log_identity(&request);
|
||||
log_protocol_error_with_identity(
|
||||
upgrade_state.as_ref(),
|
||||
"h2c_upgrade_rejected",
|
||||
"h2c upgrade is disabled on this listener",
|
||||
@@ -331,6 +338,8 @@ async fn connection<I>(
|
||||
"tcp",
|
||||
"1.1",
|
||||
400,
|
||||
authority,
|
||||
host,
|
||||
None,
|
||||
);
|
||||
return Ok::<_, Infallible>(
|
||||
@@ -344,7 +353,8 @@ async fn connection<I>(
|
||||
let injected_headers = match h2_headers_frame(&request) {
|
||||
Ok(frame) => frame,
|
||||
Err(message) => {
|
||||
log_protocol_error(
|
||||
let (authority, host) = request_log_identity(&request);
|
||||
log_protocol_error_with_identity(
|
||||
upgrade_state.as_ref(),
|
||||
"h2c_upgrade_invalid",
|
||||
message,
|
||||
@@ -353,6 +363,8 @@ async fn connection<I>(
|
||||
"tcp",
|
||||
"1.1",
|
||||
400,
|
||||
authority,
|
||||
host,
|
||||
None,
|
||||
);
|
||||
return Ok::<_, Infallible>(
|
||||
@@ -389,6 +401,7 @@ async fn connection<I>(
|
||||
peer,
|
||||
local,
|
||||
Transport { tls: false },
|
||||
None,
|
||||
extended_connect,
|
||||
upgrade_state_for_task.clone(),
|
||||
upgrade_lease,
|
||||
@@ -448,7 +461,7 @@ async fn connection<I>(
|
||||
if let Err(error) = result {
|
||||
log_protocol_error(
|
||||
frontend_state.as_ref(), "http1_connection_error", &error, peer, local,
|
||||
if transport.tls { "tls" } else { "tcp" }, "1.1", 400, None,
|
||||
if transport.tls { "tls" } else { "tcp" }, "1.1", 400, tls_sni.clone(),
|
||||
);
|
||||
tracing::debug!(%error, "HTTP/1 connection closed");
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ pub(super) fn fastcgi_params(
|
||||
if context.tls { "on" } else { "off" }.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());
|
||||
if let Some(value) = headers
|
||||
.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>"
|
||||
));
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ async fn empty_index_can_go_directly_to_directory_listing() {
|
||||
let body = std::str::from_utf8(&body).unwrap();
|
||||
assert!(body.contains("file.txt"));
|
||||
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]
|
||||
|
||||
Reference in new issue
Block a user