From 32e6875980a2592d9c3a47d7c67bc89751029190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 5 Oct 2026 22:29:54 +0200 Subject: [PATCH] v0.26.2 --- AGENTS.md | 2 +- CHANGELOG.md | 11 + Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 27 +- ROADMAP.md | 12 + SHA256SUMS | 98 +++---- debian/README.md | 2 +- debian/build-package.sh | 2 +- debian/changelog | 2 +- dev.sh | 4 +- docker-agent/model.go | 2 +- docs/API_ACCESS.md | 2 +- docs/FILE_LOGS.md | 121 ++++----- docs/INSTALL.md | 2 +- docs/MIGRATION.md | 4 + docs/README.md | 5 +- docs/TEST_REPORT.md | 12 +- docs/V0.26.2_IMPLEMENTATION.md | 71 +++++ grafana/README.md | 2 +- grafana/rustproxy-overview.json | 4 +- langs/en_US.json | 13 +- langs/pl_pl.json | 13 +- make_zip.py | 2 +- scripts/check_source.py | 20 +- scripts/deploy_core.py | 2 +- scripts/docs_check.py | 10 +- scripts/preflight.py | 4 +- scripts/release_smoke.py | 10 +- scripts/swagger_assets.py | 2 +- scripts/upgrade.py | 6 +- scripts/web_server_contracts.py | 2 +- src/admin/documentation/documents.rs | 8 + src/admin/file_log_search.rs | 384 +++++++++++++++++++++++++++ src/admin/file_logs.rs | 79 +++--- src/admin/mod.rs | 1 + src/admin/openapi.json | 13 +- src/admin/preferences.rs | 29 +- src/admin/tests/part_06.rs | 234 +++++++++------- src/admin/web/app/core.js | 4 +- src/admin/web/app/settings.js | 2 +- src/admin/web/file-logs.js | 168 ++++++++---- src/admin/web/index.html | 110 ++++---- src/admin/web/management.js | 2 +- src/admin/web/style.css | 188 ++++++++++++- src/admin/web/vhosts/render.js | 2 +- src/main/bootstrap.rs | 2 +- src/main/tasks.rs | 2 +- src/web_server/fastcgi/protocol.rs | 2 +- src/web_server/response/file.rs | 2 +- src/web_server/tests/part_02.rs | 2 +- 51 files changed, 1255 insertions(+), 452 deletions(-) create mode 100644 docs/V0.26.2_IMPLEMENTATION.md create mode 100644 src/admin/file_log_search.rs diff --git a/AGENTS.md b/AGENTS.md index 160f4b9..eebe4f6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes. The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for -compatibility. Current source version: 0.26.1. +compatibility. Current source version: 0.26.2. ## Delivery diff --git a/CHANGELOG.md b/CHANGELOG.md index a222f46..6c483ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ > test-only changes and packaging implementation notes were moved to > [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md). +## 0.26.2 + +- Combine Stored Logs and Live Logs under one top-bar **Logs** menu. +- Replace the Stored Logs filter matrix with an optional source selector plus a single field-query search supporting `field:value`, explicit `AND`, `!` negation, quoted values, wildcards and numeric comparisons. +- Build selectable Stored Logs columns dynamically from JSON fields instead of fixed `matched_by`/SNI protocol columns; saved account preferences can retain discovered fields. +- Parse RAW error-log lines into timestamp, level, target, message and detectable `key=value` fields, while preserving the original raw line. +- Show parsed field/value details for a selected record with raw JSON available on demand, and move the column picker to the upper-right table toolbar. +- Remove the configured-file inventory while retaining an optional source selector; bounded searches scan configured sources together by default or one selected source on demand. +- Replace backend sort-direction text with `↑`/`↓` symbols. +- Keep SQLite schema 8 and the existing saved proxy configuration format; no data migration is required from 0.26.1. + ## 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`. diff --git a/Cargo.lock b/Cargo.lock index bc20441..07639b3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "rust-proxy" -version = "0.26.1" +version = "0.26.2" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index 56dcd26..6c0c45d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rust-proxy" -version = "0.26.1" +version = "0.26.2" edition = "2021" rust-version = "1.88" description = "RustProxy - independent reverse proxy with local administration" diff --git a/README.md b/README.md index 3b08e9a..c5d55f0 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ -# RustProxy 0.26.1 +# RustProxy 0.26.2 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.1 release source.** +**0.26.2 release source.** ## Layer-4 streams @@ -11,17 +11,18 @@ Raw TCP/UDP forwarding has its own **Streams** page in the primary navigation, a Named HTTP/HTTPS listener bind addresses use the same chip-style editor as vhost hostnames, including multi-address listeners. -## What changed in 0.26.1 +## What changed in 0.26.2 -- 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. +- Merge the Stored Logs and Live Logs top-bar actions into one **Logs** menu. +- Replace the Stored Logs filter row with an optional log-source selector, one query field and page-size control. Search accepts Kibana-style field clauses such as `req: GET AND !country: Poland`, quoted values, wildcards and numeric comparisons. +- Parse JSON/JSONL records into dynamic selectable columns discovered from returned fields instead of maintaining fixed protocol-specific columns. `matched_by` and `sni` are no longer hard-coded table columns, but remain available when present in a record. +- Parse the built-in RAW error-log format into timestamp, level, target, message and detected `key=value` fields while retaining the original raw line. +- Expand selected records as parsed field/value rows with optional raw JSON, and move the column picker to the upper-right edge of the result table. +- Remove the Configured files inventory while keeping an optional source selector; the explorer searches all bounded configured log sources by default and narrows to one selected source when requested. +- Replace backend sort text (`A-Z` / `Z-A`) with direction arrows (`↑` / `↓`). +- Keep SQLite schema 8 and the saved proxy configuration format unchanged; upgrading from 0.26.1 requires no data migration. -The detailed 0.25.5 implementation audit remains available as historical baseline evidence. Current 0.26.1 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.2 verification status is maintained in [docs/TEST_REPORT.md](docs/TEST_REPORT.md). ## Documentation @@ -99,7 +100,7 @@ CARGO_BUILD_JOBS=1 CARGO_PROFILE_RELEASE_LTO=false \ CARGO_PROFILE_RELEASE_CODEGEN_UNITS=8 cargo build --release --locked ``` -The 0.26.1 source archive preserves the resolved `Cargo.lock` supplied with the source tree and +The 0.26.2 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 +339,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.1.zip +python3 make_zip.py /tmp/RustProxy-0.26.2.zip ``` ## New management and test-origin guide diff --git a/ROADMAP.md b/ROADMAP.md index edbe271..2e6d2f7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,17 @@ # RustProxy roadmap +## 0.26.2 - Unified logs search and dynamic fields + +Current direction: 0.26.1 -> 0.26.2 + +- [x] Merge Stored Logs and Live Logs into one top-bar Logs menu. +- [x] Replace fixed Stored Logs filters with one bounded field-query syntax supporting AND, negation, quotes, wildcards and numeric comparisons. +- [x] Discover selectable Stored Logs columns from parsed record fields and keep arbitrary safe field choices in account preferences. +- [x] Parse the built-in RAW error-log line format into basic structured fields while retaining the raw record. +- [x] Remove the configured-file inventory/source selector from the Logs workspace and keep bounded all-source scanning by default. +- [x] Move Stored Logs column configuration to the table toolbar and use arrow symbols for backend sort direction. +- [ ] Run compiler-enabled `cargo fmt --all`, `cargo check --locked --all-targets` and `cargo test --locked --all-targets` on a Rust release host. + ## 0.26.1 - Vhost-aware protocol diagnostics and Logs Explorer UX Current direction: 0.26.0 -> 0.26.1 diff --git a/SHA256SUMS b/SHA256SUMS index 810fa90..7ac3133 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,19 +1,19 @@ 61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore 4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example 9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore -ef0142e762f044784ceb61c15c8bf16333250886963850c58e3af45b1419cf5d AGENTS.md -ffce7ba9ca03095bb1aee69eb3e806e5849ae6d1e1f24c0a105230fd27162737 CHANGELOG.md -eabd3b41a3f62cc93b6803ba759643bb8b9a7cc1de43b9587ca2154bcc04ff09 Cargo.lock -f2cb130695ac4eb7f4e5255589e282e4d1a5fd269996a74090a6275f464733b1 Cargo.toml +f927259ddc3f564e17884b2f2cdf0e060b2d8e921efc41739bff39c7c8fd186b AGENTS.md +1b2622333d04fbf440aef8cdadbb59fc133d98a1ed5a12f6d2df7ecfffcea526 CHANGELOG.md +4a2e8f63e0c5d72f18f636c6e8eea4018cadd14340bdd44e0aeaab8f931488b7 Cargo.lock +4fee895e239bcfe93b945101dc556cf06189e960610da2ba4520c54eeea5b040 Cargo.toml d6d22ba63f4bc3d16a13012ecaabff8a156be57c824d2f20f66976502fb27d1f Dockerfile -d3f596b1e2a8caa7129bca66f13b5c6f26cf3513de495c96f20a64a6763cac26 README.md -6065f28237d57a21f58fdfa83050781b118fdef3dedf961b1adabc8e734bb788 ROADMAP.md +7142091e55af5998d73cab966b50563f0304c7bd13bdd94c7d24208075240922 README.md +0c0e9cb5583e389b393aac6d5fcf9269ee3c0dd8ef41fd6c02f74aff9c9090dc ROADMAP.md da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml 928ba6a504bc431ae100c6e587861e5088b28822d2cb19da5d437b39d26c864a build.rs -fe2a113daf65042c84838b1e1b7268a74775288ff3fc9bb1570f973d5f8106b7 debian/README.md -2690b8c6a61383a2b1752736e44822c8cfc6119beeacbcba326fcc9f02fc162a debian/build-package.sh +e42acfb9e917d6e04ea6692f2a8512c3856d1aabdf96d4a17efaa0b2d49e187b debian/README.md +bcf9fba9f796a7051a47bace3b6ff6d090f72dffe02642181bd828692e208988 debian/build-package.sh 0763a4bd8b429f7922b23a7276ff694f3a751b83fb127f6ad7ec654e5e507ecd debian/build-repository.sh -0e4306f33dd4a04100af9b3932d2cc264aa901a149cec0d3f95e8aeb4c639152 debian/changelog +b2579151f48cd68a751d8d99043de2a1b78cfaa51514fc6becf1223f2decb144 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 -cf4180779795cf06416d34a55372ff7d258f0ce6a1e7208bf7912b62b682aa7f dev.sh +999c0141133436be1ca8757a7a7570c406c63e89af8ecf1b587ba86a84b23695 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 -d4fa829176c8997a8fc42d59d7c45e252efa8a6f05c818d511fd24146da39a01 docker-agent/model.go +7706c216c99152c5c707ebe8c7bc9e9d900208e79cfd4f29dcade9549f24b791 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 -9b66a67392803805d0d28c7719f263288b5e6ef16f06b9b8c13a3afdf90271ed docs/API_ACCESS.md +3056b3db4b69d65db8392b2f6328b52bc3dcdb5962cea677c3e66f347b99653d 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 -a1150f7525a20db8e8399eb04ce679e1699fd1e574485e27bdf2f565436fc508 docs/FILE_LOGS.md +613e844c12e32be23c46e8d88c081bf8d2c0395ea53dbd4d725f9d07e21b7a60 docs/FILE_LOGS.md df6ecd4a50796640243676ad4b3a87789f6e5d20cbebea6255e92fbfff1c7131 docs/HTTP3.md -1b120d86a65137319234129ce78af39733e59af4b764869a9d7fdd01b9312382 docs/INSTALL.md +0e8e7c9291555b0f31a7af0d4826cc2dfbf33cbc42332e69360caeb4bc349768 docs/INSTALL.md 3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md 4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md 7c35c4cdbe7bbb81d94f3bf6aeb22b8a866af3e7f0d62bbedc299e4d6f264e0d docs/MANAGEMENT.md -526e24e7a9b76941efccf64f4bacab5529e08489add316ec44b33da2756b4dd5 docs/MIGRATION.md +5f98c4006e89b432904e0b65ece6aa5df4befd6dc8e00024eb67b4ad901ba14d 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 -087ce068b693b9a0b5fba4b884f82d9e1ece056e5b083e4d2f8cab41062f9bd7 docs/README.md +3f8169931302a4abf0a65109ff98c2b9154f06ac39d4c08e2ab89dfeacfb39bc 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 -cc678073533ef97b5b9397a1698e7a33a3d7156cc933d754adbf748443e70693 docs/TEST_REPORT.md +ac2815cb3cd746a228cc489699d0b2031056750cfd744bb30e48aa550b3b515e docs/TEST_REPORT.md cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md 8ee6187a5460ca45643b7fd789e27bffd264449e3a92ff70e9359edee011bbbb docs/TRANSFORM.md a1e9c6d38d21067d1ce7115ca3395ae8ff1f6a4c40fb0ebf00a6940ef79df439 docs/TRANSPORT.md @@ -101,6 +101,7 @@ d0e609febbcd1cfdb8ea0b00b4fa6f081eac01e2d43a81e38a53ae25783152cc docs/V0.24.8_A 583d548b4a5db9f35a2bbf8ec00ec22397764c5850a8e883a3d12680da517df7 docs/V0.25.4_AUDIT.md bf9b4cf4fec2c8ce287e1b3c2e7d4828e101dafe4d6c173f099819f22493d144 docs/V0.25.5_AUDIT.md 05e0b4cc074b07ff37d45fabc20c7272bada217286b3db3d7997cae25c1f6f11 docs/V0.26.1_IMPLEMENTATION.md +91774f2c23f587a502c65fe2ffaf009aba81b221cb3ede4861ea79abb4d4a528 docs/V0.26.2_IMPLEMENTATION.md ce259052c023e90a7bfb6e39fe82e52b845a8b2a051bfa29883d29e87a9301a2 docs/VHOSTS.md b979afd3d56731eeb06e9dc38d37730693e7fbf3aa78467932842c0e712e7b26 docs/VHOST_ACCESS.md f0e76167944861517d1d42b9186ff0b2f184be0371b9e8597827e60895de4a76 docs/WEB_SERVER.md @@ -115,14 +116,14 @@ b26833cb4251f9390a44192ecc827ff7f052fd19016a5ccf7498c94503ab4ce3 examples/mock- 5a8f3a8a054abc89c6e78896b9fe2f903212199a24dc3d2d2bcbd0696a31bcde examples/transport.json d242de9614fbb52933b4accd32f4715ca94107abe4a3b1d7e088362b07a6ba60 examples/vhosts-balanced.json af2bdb1343a8141069e94ccd2c7f92574ad848437e2f9fdb5c7576a9a18ef4a5 examples/vhosts-private.json -b14d30a7785545a50a057f020b3df02a2f46a65c75fca0e5aba1dcb959852e57 grafana/README.md -7ba863b4731bb3240cc55fb3e97e6bb2492e43718980a0cfbddd98da70bef2c5 grafana/rustproxy-overview.json +ca2142afb74a3f2434815f6f0ba8b0859f67c8740d98e25e15ec3997a74d0220 grafana/README.md +6baa1cf4a0a1894a09214a591472aedb5880e6311d6df41083d9c71f68bbbbb1 grafana/rustproxy-overview.json 30763fc61c38992a44bc24ad6932b88fd3b322724fce799db1f17f9a747eb59a install-debian13.sh 858b39fa6886b71d20690bf369e76580922364b3cc7f0cf6ba9b040df9cd76e3 install.sh 29cada0936762f26495064d1524a668262ed729b21f20aebddc8b6e5cfea363b langs/README.md -1b2f1e7127ccddb4f9c7cbca48282b5cb5f8fd38dbcdbc2c0a5695679821ee73 langs/en_US.json -822c1630aea00228118981a4dffdda05a0dc49dff715c111a0760b0ae50e1f4c langs/pl_pl.json -fca0b6e43ee1d1b970d9dd33eba93000c452e0380d9f0c3bbd9ee03b4868cfc1 make_zip.py +71af021055dd5affbc353f51126342c81b6ac91180e94300cf989e9c1032da22 langs/en_US.json +efce73aa41a21cae9debe335d7fb89245f73d132d2dfe4c97421d7241a7a77f9 langs/pl_pl.json +24c8fe7bfb42652a63c4414e38fb8ef0c89656b9045e1a3c404856de1f77efbe make_zip.py e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql 20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql 5c62da30fa9db594a4d656c77ae676d3c388072b6a85f91170ec7d911d7cde43 migrations/0004_admin_preferences.sql @@ -141,11 +142,11 @@ edb6ee1445b01cd6f3a74a3a607f39fb107d411b49df028d2e0f4679e093de31 scripts/acme_p e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/acme_rfc2136.py a5b4451e45ce975685772aa8409b64a57a20a7548466584c3ea7fb975e64729c scripts/admin_audit.py 20e83f8b6347b819d1d1ea3de5c691c36f0816954a4210b0d3f39557fefe1931 scripts/backup.py -3e66e89b51f8b1328a68a68b9a87f865d4034c08f01e8ccbf37fe9fdb7e39d76 scripts/check_source.py +80261fe4b453c1fa7724700fa6ff8b838bc63379b193356e3cc8053739fc95ce scripts/check_source.py 187ee92c8029cdd59e89b4447cf4d170e856017a2091ceb85ae7c22dd1a79c72 scripts/deploy.py -aafb8042851ae43b64fc51c793ed2134a308d1d155574ae79c1889afc226680a scripts/deploy_core.py +5b87be7439dd561eb8dd45681a60f63a2f752325db8eb7ee35258de0e0543236 scripts/deploy_core.py f014472588e889488976970dbaa95a8745b6907516aa81a7cca21d0f81eaaad3 scripts/deploy_runtime.py -64c746a3d225d810ee8aa9055cbafe80011fba49dbfe0f6857d631d171046ee9 scripts/docs_check.py +674968e62b14f818c1e68c49b9c9bde4fa75f885ac7e1f4fe269e542c52538bf scripts/docs_check.py 6facb2d65bfab234f09855097d12da8f72669a38ebceb4b1cbd03ab42b8d2469 scripts/langs_check.py 2e35b7c78887fd56f83ca9017caa168630963a9f47da71dfc53b1a8f9ad436c2 scripts/memory_contracts.py 2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migrate_npm.py @@ -155,8 +156,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 -6a314bfaf04760a0ae5da298a016400dab705c36769b49412400e6811d679b16 scripts/preflight.py -78e9f37c2b31314f582a232b3c45f5f620158094ca24619e47997a820ccf09fb scripts/release_smoke.py +b65c46669e1d07bdb64ddaff2c9a07b61de3c34db1591f2e024ac97c60f227ba scripts/preflight.py +705311b16b803bcaa7644478c03f163f31c4ec08b34efd140fada249b20f2a4b 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 @@ -168,11 +169,11 @@ ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed scripts/rust_s 1c83054336b3508d7ad72cd259f039394682642439877fe69639c0b76be09154 scripts/security_contracts.py 295b6f2625b1875950ea3eecccd142f49be327afdd00c92641286e6de530ec51 scripts/security_dependency_contracts.py a65010f828194885ffc7f0f733937957f28e6049cd978bec1380a2b8bfbd0113 scripts/source_module_contracts.py -e7790bf216ce8897d29df92c1b44b056644c3cf32417ead21197734fcc3d0f34 scripts/swagger_assets.py +c5d1a2c36bb8649d93ad9c40bfd542d21109234ebc87405b7d81632c85f71c84 scripts/swagger_assets.py 102b23de19a88a8e60de392fb83aefd3dfb33e90cc97ed1690d9f3d4a33d8866 scripts/test_compression.py -8e5e828c8e18e78678664bdf8eea58adb7cfb736a9464cea1988fdf77c824850 scripts/upgrade.py +dc6d4cdfbd08150d45c04ed2c17f90f72cc2bae6f0a40a5917b4072fc60c8605 scripts/upgrade.py 43db35e7264b0345592172f2d1f6a6e4919fc4cbde2a9ff787acd7fff1d031d6 scripts/web_assets.py -4016a00b0d503163e01f9dae35067d813e588989a442e41e0d44a758c52a7a63 scripts/web_server_contracts.py +29abb682a3dad39c4df3d9d6f0874e95c631144eb2e246970d54c5f15bbe4c61 scripts/web_server_contracts.py b8ce3e8fd08865015ce312fae9c0457612af18d6ef4513a6281b9e3212f8d458 scripts/web_service_ui_test.mjs d8e2b51c39252dbb4305512fa9b1ef508ec610968aed316b10514fd160298762 src/acl.rs 4e3be1ea1be0a81529c7c9021fb9bb77c24e5044acd54094972745443ad2c26a src/acl/tests.rs @@ -185,14 +186,15 @@ b9e57db4bd80ef37564dfc533a96897d8a494671d53acea9fb4bf28d23ba370d src/acme/provi 55ef7ceb44c25c7a8ddcbfd5e0d4b647526f9bdcde901ee08db5314e4d055b39 src/admin/access.rs 85f00f8157e83a152cc531f4cfecd0b1472ce269be9c8eb1ac17ad0693748b42 src/admin/certificates.rs 1de4c7c116eeab2452ccccbaba3f2b7b63c6eb1c1e146ab870d19c5a5ca16f80 src/admin/documentation.rs -89b6d6a0c2ddef5e77a8f9383105fd8a9b0e195de0ff46c012dd9d1ef57ee2a4 src/admin/documentation/documents.rs +c96ddc9befca3834d1bf29ffc7b92b9bab9e36abec42020df59196d960d6884a src/admin/documentation/documents.rs 67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs -8ff4e0a57487c4244189f78bfeea0bfc509dcb996ed740000e3fa288fb8657bd src/admin/file_logs.rs +002922c91aa07cbea17cb0ee11127bb6e7e015de108ee032761b34507dcfa06d src/admin/file_log_search.rs +d3210e037481e1f95e840550428e088ffd83ec9a42f1810e4c976298fb555a9a src/admin/file_logs.rs beb8be241e4f1a8cea7b6c4e02b42f0359fb1fff10ab5ce2c70ccd0e63a554b0 src/admin/logging_api.rs -fb079609d182aa63d32b7f2bfc7cf1a4229d7d500266e981fb0fde4fdd032961 src/admin/mod.rs -b40aae24eaf99ed8086951dc30d06d75fa0ae48fc2fee9745491337336b129c0 src/admin/openapi.json +58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs +1c2176d3db666e1780372534cdc0c83aa29c665265d7e3de2aaf1d42bc5d5aee src/admin/openapi.json a53309daf118381031122ca70764f7a2f92adc44af3fc91df5f946e6856f76ae src/admin/operations.rs -ee3e9c39eb2d645e55356b99c1af7d6a2ec14acd622e0e0287dea2fc47628fb4 src/admin/preferences.rs +f8ced8092063785faa64c744625412f103447ead249d8f50f5388591841b5a56 src/admin/preferences.rs b4fa393b780b2676b5d72e0f90cd7290292f424b8062d4b5983e722d6ca72c32 src/admin/public.rs d246389975c87d125ebcb8c24282a3729d509cc70e13e0f3b56b4008d80d76b4 src/admin/router.rs 333d5f9f4c27941c535ac689e5826264ffa2bb86b00832b597a9bb9f811f01e8 src/admin/session.rs @@ -204,12 +206,12 @@ e5f973c6fbddc3897bc0e6d9f8e70cbe9279acea6161ffb46e013792e845e1e4 src/admin/test 77608ad33da348dbea41d2a7ceadb75b0bcf05336cb5a20b7f2a47502f1e4842 src/admin/tests/part_03.rs 60ad5c76db3cd6fdd144d504a7b50153e133ab357e49c8ebadb99c1c8bce562c src/admin/tests/part_04.rs 5f0062abc42a3b81c79088dc0824ab4c81a22c6ea765e77d2527d07d13bffd58 src/admin/tests/part_05.rs -d6479bcf439621fb8ddc8e9ee71b599cf6d6cf72780422c490ed48b16684074d src/admin/tests/part_06.rs +f869e4da5de149fc04ce1aab658befeaf3208c862f149ca976235d15832c3cdb src/admin/tests/part_06.rs 23c55f23deeec844a9abc2289440c9e0583aaeb72416fc5f3632a43a864c0efd src/admin/web/acme.js 78e7d54d6536cd600dcbe66f0fc06ec109e121dfee1e57f3ca6e61cc6ae04a6a src/admin/web/app/config-history.js -058c1aec00109237a3b5d88134db85cdd4197c45d0335f652b661400edf0fec5 src/admin/web/app/core.js +b47ce2fe8d139c1c7007a539e120324ccb3c8fdd3cd3fd39d0ac365f7372edaf src/admin/web/app/core.js 19abc332a1009bcb087fe4ebb58d458739b27480ab50715b03d07fd408302942 src/admin/web/app/runtime.js -cada294084cfa8cb7080143e1ad21d46136bb644ff5f15bb6d457dc034fe11ff src/admin/web/app/settings.js +5c4256e97517d7cb38e0c985da150631f6ca1bed9c2030f2d21a02bd28436f5f 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 @@ -223,23 +225,23 @@ f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/ 386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html 0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg -bd5b8789d03df1e0fd0c4da31f7980e74870c26485311f566e511433f9c09e2e src/admin/web/file-logs.js +efefd94e021e7846cc0d9cd98a8ca0c605bb00e209aa33ecd54201ccdb3bef4b src/admin/web/file-logs.js dee3b113907f77138cdf919efdfb097e45b889cff3893bfc6f02ed998643bbae src/admin/web/host404.js -97444c4ab0441af6767e7383bf70c9f3c163b846614ce9b3623ab40e05731118 src/admin/web/index.html +9321d491cede2438b9a9e9beb82f5886e7e0459d36fba64a20cda3d5779f64e1 src/admin/web/index.html 24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js cdab0c11a28deebdf401c0453fbc76b675e9e643722106fb20e812bb9648ccde src/admin/web/login-keys.json a253d202dc9a147f2add34c64e0f35c38fdc7f667f9908933ce5f959b1a92a2e src/admin/web/login.html dc092f7120b6502e7d322b5f0953f34a131b7af7d1476207a934605ee65f3968 src/admin/web/login.js -e41619e5cb0ba01a3d287748f28eb38bbe7641f9bedade252d286c18e619c400 src/admin/web/management.js +44939a885a00d6a10d5be23b03c9552567010ec66c9e015b2bc7dc28a7e53000 src/admin/web/management.js 22318f39f7bb1ae98e65737d3c2ad9e63065b8e94ad59bed560c0e80d3a46949 src/admin/web/redirects.js 668f5162fa9f8bbe2fefce4115644a6469e01aa7defc9c7f760c28afd8e2afc7 src/admin/web/settings.js 1a30affd6a717095a1763998a5850a79ae0f86b7b694806f2aba2b880735dd1a src/admin/web/streams.js -c6f8dcdf07f0bf27036d9b2f37bdec56c0144b7f7365e6f8b968f700047697b4 src/admin/web/style.css +ed3b9b327cde32bfd2b7861977b28b5d76298badd70ffb4407db7b6ca794b557 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 d4938e0bf8cf41f79e9b49cf9b5cf2c8340668e792b73d5350cb1872e7ef1edf src/admin/web/vhosts/editor.js -c815bcf24a75608669232fe5ff447d0b755de586093addca3758aae739f9febc src/admin/web/vhosts/render.js +91efb2d4f02d12bbbb22818f1215486329a91293d85a84c789dae8d77632059f src/admin/web/vhosts/render.js 0de35a0671f3ac8fed62260d63ae43d04efce4947428ebdb9254cff0295295f9 src/admin/web/web-server.js 98c00d99822f9be54fc58afb6d4b577901712effd2c5eddce014312681864625 src/auth/files.rs b2f948bcc3dbd24952c7b1b02d41d06757f49f0c38a78caf51f8f8a687d8480d src/auth/files/tests.rs @@ -344,11 +346,11 @@ a16613114273c54b6d2a16a94fa8748550e91468585469642622bd2c86087baa src/logging/fo 17425ebbca6bbbb0dcaeb65ebc44b1e51488a4092b7b6add556e764c6d486d6a src/logging/tests.rs 3836ed14b6710e183f048ee4d7807a35366c27a27338057fe4a2811129b63302 src/main.rs 46732ed6d9cf2a33031624f941b8318d2676fb1b6bb5142e96820301f9fa1ac4 src/main/admin_runtime.rs -7d1ed0a639ed79b914d609d913b3004d5da7ad60695a487799ef42bcc4d74c9c src/main/bootstrap.rs +8678b85fb46fa6813c9483e1ceb59a25458cae655418571bfa491b1e391f7ed5 src/main/bootstrap.rs 5a98d0e249338e73aa57ae2e352dc16db2f79c2d042c1544ce1c2022da76a31c src/main/proxy_runtime.rs 4dd7c1084a6214c0f2513ebf86fe8ef144470695557772bfe25979edd64491ab src/main/shutdown.rs 0a6578a0f4023b9f8d73043bf942dc5e096ef9afb8c0d2f131c14f65d08ac01d src/main/shutdown/tests.rs -d2b53a7681476f368b169a1c3bb2e4646af0c27084e52cd45cc7c37a2451cfcb src/main/tasks.rs +97f8dea981ea0b54f64144724f565ed0fa3d42dfcc043ce88f6f851f4ea458fe src/main/tasks.rs 49d7e104585c8791ac9a203dddd95802784b68c0ba65d6f90f3d57154b4c7871 src/metrics.rs 201ab702722d6d21585fe8bb49939150d61bfac7ed8dc198b98c2cc6098ffb5f src/metrics/global.rs 49b11df65b4fb0b5b727f9f867436584680b45e3f4fbd53f74bc0cab8b9a88ec src/metrics/vhost.rs @@ -455,7 +457,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 -4cd184d73f00bc7abc967e0fe574e16f84de1e9d999e599de6f4a873050f9e61 src/web_server/fastcgi/protocol.rs +87c20f3a28ea6cfde496f05a381ee3b4be784bae4d6bf65580ab6c0306bb46fb 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 @@ -465,14 +467,14 @@ f0479524d66bfee7a6eed5eea03601630ba82064fb944902deb9166f90fe17cf src/vhost/vali 80f946570c14c8915fd21bfdb1a84dfb6a14c633ab6060acb828fab4c7c21daa src/web_server/mod.rs 67ab8bef567452f123c4b9b46a942b39c93f1f765c128edca7ee808ff1c5e304 src/web_server/request.rs c5cb561b0f276e8082f904b9446fa5885f6b3c0608f25ea2ef425061e9f7f6b4 src/web_server/response.rs -9dd6c0861cfd11a278002c665e81440a302d0a91f2e486f9437d0185bbc40542 src/web_server/response/file.rs +4c3490918e10074226d75971c743249482d3e174dddc80699f8cd8da7d90d9df 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 -070141f7e6f0a5659ac18f90e03fd73a73767aded81d752c342440dbee80b5f0 src/web_server/tests/part_02.rs +9bd3acd815604a4ce349c7b2d41cede676ebafc33183dd9d32fb3193ef63d68f src/web_server/tests/part_02.rs e54f44f6ca1ed2c41e620de9195d32c4f8bb6936c3881745316a07160f87b4fa src/web_server/tests/part_03.rs f00743af2cf4a5952caef7a1dcf68e7817b5599a252a9973e1c22045517c0b0e tests/fixtures/argon2id.json 1725c10331745dc04a07675784d385260da7b14567b8dba3cbae5e891eb5dadf tests/http2.rs diff --git a/debian/README.md b/debian/README.md index d9b07aa..86b3dbf 100644 --- a/debian/README.md +++ b/debian/README.md @@ -1,6 +1,6 @@ # Debian packaging -This directory builds the `rustproxy` binary package for RustProxy 0.26.1. +This directory builds the `rustproxy` binary package for RustProxy 0.26.2. The package installs: - `/usr/bin/rust-proxy` diff --git a/debian/build-package.sh b/debian/build-package.sh index b301594..8430222 100755 --- a/debian/build-package.sh +++ b/debian/build-package.sh @@ -21,6 +21,6 @@ cargo fetch --locked dpkg-buildpackage -us -uc -b mkdir -p dist -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' \) \ +find .. -maxdepth 1 -type f \( -name 'rustproxy_0.26.2_*.deb' -o -name 'rustproxy_0.26.2_*.changes' -o -name 'rustproxy_0.26.2_*.buildinfo' \) \ -exec cp -f {} dist/ \; echo "Packages copied to $ROOT/dist" diff --git a/debian/changelog b/debian/changelog index 1e30a17..fc34548 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -rustproxy (0.26.1) stable; urgency=medium +rustproxy (0.26.2) 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. diff --git a/dev.sh b/dev.sh index fc76834..4624be7 100755 --- a/dev.sh +++ b/dev.sh @@ -18,7 +18,7 @@ if [[ $# -gt 0 ]]; then shift; fi case "$mode" in help|-h|--help) cat <<'HELP' -RustProxy 0.26.1 +RustProxy 0.26.2 ./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.1...\n' >&2 +printf 'Building RustProxy 0.26.2...\n' >&2 cargo build --locked BIN="$CARGO_TARGET_DIR/debug/rust-proxy" case "$mode" in diff --git a/docker-agent/model.go b/docker-agent/model.go index 37faf79..b100f88 100644 --- a/docker-agent/model.go +++ b/docker-agent/model.go @@ -1,6 +1,6 @@ package main -const version = "0.26.1" +const version = "0.26.2" type config struct { Listen string diff --git a/docs/API_ACCESS.md b/docs/API_ACCESS.md index 10de324..6f57a7c 100644 --- a/docs/API_ACCESS.md +++ b/docs/API_ACCESS.md @@ -1,4 +1,4 @@ -# Administration API access - 0.26.1 +# Administration API access - 0.26.2 ## Sign-in boundary diff --git a/docs/FILE_LOGS.md b/docs/FILE_LOGS.md index dcaf97a..2307b4a 100644 --- a/docs/FILE_LOGS.md +++ b/docs/FILE_LOGS.md @@ -1,87 +1,78 @@ # Stored log explorer -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**. +RustProxy 0.26.2 provides an authenticated, read-only **Logs** workspace at `/logs`. The top bar has one **Logs** menu with separate **Stored logs** and **Live Logs** actions; the two data paths remain independent. ## Separation from Live Logs 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: +Stored Logs reads only paths derived from the active RustProxy logging configuration: the combined structured access log or per-vhost JSONL directory plus the optional error log. Existing files remain browsable after their writer is disabled. Low-level `protocol_error` records remain persistence-only and do not enter Live Logs. -- the combined structured access-log path, or JSONL files directly inside the configured per-vhost directory; -- the configured optional error-log file. +## Search syntax -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. +The dashboard uses one search field and a page-size selector. Search is case-insensitive and supports field clauses, negation, quoted values, wildcards and numeric comparisons. + +Examples: + +```text +req: GET AND !country: Poland +status:>=500 AND path:/api/* +error:"connection reset" +client_ip:192.0.2.* AND status:5xx +``` + +Rules: + +- whitespace and explicit `AND` join clauses; +- `!field:value` excludes matching records; +- quotes keep spaces in one value; +- `*` acts as a wildcard; +- numeric fields accept `>`, `>=`, `<` and `<=`; +- `status:5xx` matches a status class; +- bare terms search all stored values; +- `req`/`request` aliases `method`, `ip`/`client` aliases `client_ip`, and `source` aliases the internal log-source ID; +- `OR` is not supported and is rejected explicitly. + +The API continues to accept the older `source`, `event`, `status_class`, `client_ip` and `vhost` parameters for compatibility, but the dashboard no longer exposes those separate controls. + +## JSON parsing and dynamic columns + +JSON/JSONL lines are parsed into record objects. Canonical access-log fields are reconstructed from the active JSON field mapping where possible, so fields such as `client_ip` remain usable even when their output key was renamed. + +The query response includes a bounded list of discovered leaf field names. The browser builds the column picker from those fields. The default concise view is time, event, status, client IP, vhost, record summary and source; any returned JSON field can be selected dynamically. Fields such as `matched_by`, `sni`, `authority`, `country`, `request_id` or custom mapped fields therefore appear only when the data actually contains them. + +At least one column must remain visible. Stored-log column selection is saved per administrator account and accepts up to 64 bounded field names. + +Selecting a row highlights it and expands parsed field/value pairs directly below the row. A nested **Raw JSON** section shows the normalized complete record. + +## RAW error logs + +RustProxy's built-in RAW error-log format is also normalized for browsing. The explorer extracts timestamp, level, target, message and recognizable `key=value` fields, while retaining the original line in `raw`. + +RAW is not a lossless structured serialization, so extraction is best-effort. Fields that cannot be separated safely remain visible through the message/raw record rather than being invented. ## Vhost-aware protocol records 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", - "transport": "tls", - "vhost": "app", - "sni": "app.example", - "tls_sni": "app.example", - "authority": null, - "matched_by": "sni", - "duration_ms": 0 -} -``` - -`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. - -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. - -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. - -## 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. +Attribution priority remains routed vhost, HTTP `:authority`, HTTP `Host`, TLS/QUIC SNI, then unmatched. Matching reuses normal exact/wildcard frontend resolution and listener binding. Client Host/SNI text never becomes a filesystem path. ## Browser security and bounds -The browser never supplies a filesystem path. `/api/file-logs` returns opaque source IDs derived from the current configuration, and `/api/file-logs/query` accepts only those IDs or `all`. +The dashboard no longer exposes the configured-file inventory. The Source selector is optional: `All configured files` is the default, while selecting one source narrows the same bounded query to that log only. -The reader is bounded: +The browser still never supplies a filesystem path. The server derives sources from the active configuration and reads only regular files. On Unix it opens with `O_NOFOLLOW`; per-vhost enumeration rejects a symlinked directory and accepts only direct regular `.jsonl` children. -- up to 2 MiB scanned from the tail of one source; +The reader remains bounded: + +- up to 2 MiB scanned from one source; - up to 8 MiB across one query; -- 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. +- up to 20,000 lines across a query; +- at most 32 sources in an all-source query; +- page size at most 500 and offset at most 10,000; +- search input at most 512 bytes and 32 parsed clauses; +- dynamic field discovery at most 64 leaf fields. -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. +If more than 32 sources are eligible, the newest 32 are scanned and the response is marked truncated. File reads run in a blocking worker rather than the async proxy runtime. -File reads run in a blocking worker rather than the async proxy runtime. Only regular files are accepted. On Unix the reader opens with `O_NOFOLLOW`; a symlink is not followed. Per-vhost enumeration also rejects a configured directory that is itself a symlink and only lists direct regular `.jsonl` children. - -The endpoints are private administration routes and inherit the existing panel session/Host/Origin access controls. There is no public-proxy route to the file reader. - -The explorer is diagnostic rather than a retention system. RustProxy does not add rollover, indexing or compression; use the operating-system log rotation policy for retention. +The explorer is diagnostic rather than a retention/indexing system. RustProxy does not add rollover, indexing or compression; use operating-system log rotation for retention. diff --git a/docs/INSTALL.md b/docs/INSTALL.md index 0052b4c..f8a4741 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -1,4 +1,4 @@ -# Installing and updating RustProxy 0.26.1 +# Installing and updating RustProxy 0.26.2 The scripts prepare a Linux/systemd installation. This source archive is **not a verified production build**: Rust/Cargo, real CA operations, live systemd startup diff --git a/docs/MIGRATION.md b/docs/MIGRATION.md index 2c90901..aba671e 100644 --- a/docs/MIGRATION.md +++ b/docs/MIGRATION.md @@ -1,5 +1,9 @@ # Deployment and configuration migration history +## 0.26.1 -> 0.26.2 + +No SQLite or saved-configuration migration is required. Version 0.26.2 changes only administration/log-explorer behavior and account-scoped view preferences. Existing JSONL and RAW log files remain readable, and old Stored Logs filter query parameters remain accepted by the API for compatibility even though the dashboard now uses one query field. + ## 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`. diff --git a/docs/README.md b/docs/README.md index 8391b88..a402beb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,7 +15,8 @@ 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. +- [0.26.2 implementation note](V0.26.2_IMPLEMENTATION.md) - unified Logs navigation, field-query search, dynamic JSON columns and RAW parsing. +- [0.26.1 implementation note](V0.26.1_IMPLEMENTATION.md) - historical vhost-aware protocol logging and Stored Logs UX milestone. - [TEST_REPORT](TEST_REPORT.md) - verification commands, results and environment limitations. ## Functional documentation @@ -61,7 +62,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 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. +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.2_IMPLEMENTATION.md](V0.26.2_IMPLEMENTATION.md); [V0.26.1_IMPLEMENTATION.md](V0.26.1_IMPLEMENTATION.md) is retained as the prior Stored Logs/protocol-attribution milestone; 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. diff --git a/docs/TEST_REPORT.md b/docs/TEST_REPORT.md index 0f7471c..7f84d26 100644 --- a/docs/TEST_REPORT.md +++ b/docs/TEST_REPORT.md @@ -1,16 +1,18 @@ -# Verification report - RustProxy 0.26.1 +# Verification report - RustProxy 0.26.2 ## Scope -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. +RustProxy 0.26.2 keeps the 0.26.1 vhost-aware persistent protocol diagnostics and focuses on the administration log workflow. Stored Logs and Live Logs now share one top-bar Logs menu while retaining independent data paths. -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. +Stored Logs replaces the fixed event/status/client/vhost controls with an optional source selector, one bounded field-query input and page size. The query parser supports `field:value`, explicit `AND`, `!` negation, quoted values, wildcards, numeric comparisons and aliases such as `req` -> `method`. JSON/JSONL records expose a bounded discovered-field list for dynamic columns; protocol-specific fields such as `matched_by` and `sni` are no longer hard-coded table columns. RAW error-log lines are best-effort parsed into timestamp, level, target, message and detected `key=value` fields while preserving `raw`. + +The dashboard removes the Configured files inventory but keeps an optional source selector with `All configured files` as the default, moves the column picker to the table toolbar, expands selected rows as parsed fields plus optional raw JSON, and uses `↑`/`↓` for backend sort direction. SQLite schema 8 and the saved proxy configuration format remain unchanged. ## Source-level regression coverage -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. +0.26.2-specific Rust tests cover field aliases, explicit AND, negation, quoted values, wildcard/status/numeric matching and built-in RAW error parsing. Retained 0.26.1 tests continue to cover canonical field recovery, safe source derivation, symlink refusal, bounded reads and protocol diagnostic normalization. -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. +Administration preference validation now permits up to 64 bounded, control-free Stored Logs field names so dynamically discovered JSON fields can be persisted per account. Browser JavaScript is syntax-checked, and the language/documentation/source-contract suites cover the new UI strings and release identity. ## Verification status in this workspace diff --git a/docs/V0.26.2_IMPLEMENTATION.md b/docs/V0.26.2_IMPLEMENTATION.md new file mode 100644 index 0000000..9a19763 --- /dev/null +++ b/docs/V0.26.2_IMPLEMENTATION.md @@ -0,0 +1,71 @@ +# RustProxy 0.26.2 implementation note + +## Scope + +Version 0.26.2 is a UI/query follow-up to the Stored Logs explorer. It does not change SQLite schema 8 or the saved proxy configuration format. + +The release: + +- combines Stored Logs and Live Logs under one top-bar Logs menu; +- replaces the fixed Stored Logs filter matrix with an optional source selector, one bounded field-query input and a page-size selector; +- discovers selectable table columns from parsed record fields; +- parses RustProxy RAW error-log lines into basic fields; +- removes the configured-file inventory while retaining the source selector as an optional index-like scope; +- moves column configuration to the upper-right table toolbar; +- uses arrow symbols for backend sort direction. + +## Stored Logs query syntax + +The `q` parameter remains bounded and now accepts clauses separated by whitespace or explicit `AND`. + +Supported forms include: + +```text +req: GET AND !country: Poland +status:>=500 AND path:/api/* +error:"connection reset" +``` + +`req`/`request` aliases `method`, `ip`/`client` aliases `client_ip`, and `source` aliases the internal source ID. A leading `!` negates a clause. Quoted text keeps spaces together. `*` is a wildcard. Numeric fields accept `>`, `>=`, `<` and `<=`; status values also accept forms such as `5xx`. + +All clauses are ANDed. `OR` is deliberately rejected rather than being interpreted ambiguously. Existing `source`, `event`, `status_class`, `client_ip` and `vhost` query parameters remain accepted by the API for compatibility. The dashboard emits `source` only when one specific log is selected; `All configured files` leaves the source parameter unset. + +## Parsed records and dynamic columns + +JSON/JSONL lines are normalized as before, including recovery of canonical fields from configured JSON mappings. The query response also returns a bounded list of discovered leaf field names. The browser uses those names to build the column picker dynamically. + +The default concise columns remain time, event, status, client IP, vhost, record summary and source. Protocol-specific fields such as `matched_by` and `sni` are no longer hard-coded columns; they appear automatically when present in returned JSON and can be selected like any other discovered field. + +Stored-log column preferences remain account-scoped. Validation now accepts up to 64 bounded, control-free field names so discovered JSON fields can be persisted without widening any filesystem or proxy configuration authority. + +## RAW error-log parsing + +The built-in RAW error format is parsed into: + +- `timestamp`; +- `level`; +- `target`; +- message text before the first recognized `key=value` field; +- subsequent detected `key=value` fields. + +Space-separated text following a `key=value` token is attached to that field until another valid field token begins. Because RAW is intentionally not a fully self-describing serialization, the original line is retained in `raw` and remains available in the expanded record view. + +## UI details + +Selecting a row shows parsed field/value pairs inline and offers the normalized raw JSON in a collapsible section. Column configuration sits in the table toolbar instead of the filter row. + +The previous Configured files block remains removed. Stored Logs keeps an optional Source selector as an index-like scope: `All configured files` is the default, and choosing one configured log limits the query to that source. The server continues to derive every readable path from active RustProxy logging configuration; the browser submits only a source ID, never a filesystem path. + +Stored Logs and Live Logs remain technically independent. The combined Logs top-bar control changes navigation only; Live Logs still reads the bounded in-memory request history, while Stored Logs reads bounded tails of configured files. + +## Security and bounds + +Existing file-reader protections remain unchanged: regular-file checks, Unix `O_NOFOLLOW`, configured-source derivation, blocking-worker I/O, bounded scan bytes/lines/source count, bounded result offsets and page size. + +Search parsing is bounded to 512 input bytes and at most 32 clauses. Field names and individual values are bounded before matching. Dynamic column discovery is capped at 64 leaf fields. + +## Regression coverage + +Source tests cover field aliases, negation, quoted values, status/numeric matching, wildcard matching and RAW error parsing in addition to the retained 0.26.1 stored-log normalization tests. + +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. diff --git a/grafana/README.md b/grafana/README.md index 66d0d27..de0f358 100644 --- a/grafana/README.md +++ b/grafana/README.md @@ -19,7 +19,7 @@ classes, cache outcomes, capture-state gauges and configured-route traffic. Raw are intentionally absent from Prometheus labels. Import the JSON in Grafana and select a Prometheus datasource when prompted. The -dashboard expects RustProxy 0.26.1 metric names and a scrape interval around 15s. +dashboard expects RustProxy 0.26.2 metric names and a scrape interval around 15s. Collectors can be disabled in RustProxy settings; panels for disabled collectors will simply show no series. diff --git a/grafana/rustproxy-overview.json b/grafana/rustproxy-overview.json index 141f86c..982aaf1 100644 --- a/grafana/rustproxy-overview.json +++ b/grafana/rustproxy-overview.json @@ -4529,9 +4529,9 @@ }, "timepicker": {}, "timezone": "browser", - "title": "RustProxy 0.26.1 - Operations", + "title": "RustProxy 0.26.2 - Operations", "uid": "rustproxy-0170", "version": 5, "weekStart": "", - "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." + "description": "RustProxy 0.26.2 operations dashboard including backend client lifecycle/retry diagnostics, bounded Traffic analytics, country and User-Agent-family visibility, and built-in Web Server traffic." } diff --git a/langs/en_US.json b/langs/en_US.json index 3f44308..b0b26a2 100644 --- a/langs/en_US.json +++ b/langs/en_US.json @@ -303,7 +303,6 @@ "vhostsDescription": "Manage hostnames, frontend bindings, backends and per-host policies.", "vhHealthTitle": "Backends", "vhManage": "Manage vhosts", - "vhHealthHint": "Live state: leases cover the entire exchange; traffic is an EWMA of payload bytes.", "vhHealthEmpty": "No backend diagnostics yet.", "vhVhost": "Vhost", "vhActive": "Active", @@ -1975,12 +1974,18 @@ "vhEnableHint": "Controls whether this vhost accepts traffic.", "backendColumnsRequired": "At least one backend table column must remain visible.", "logs": "Logs", + "storedLogs": "Stored logs", + "fileLogsSearchHelp": "Search syntax", + "fileLogsSearchHelpTitle": "Kibana-style field search", + "fileLogsSearchHelpText": "Use field:value clauses. Clauses are joined with AND; prefix a clause with ! to exclude it. Quotes keep spaces together.", + "fileLogsParsedFields": "Parsed fields", + "fileLogsRawJson": "Raw JSON", "filelogs": "Logs", "filelogsTitle": "Stored log explorer", "filelogsDescription": "Search configured on-disk RustProxy logs without coupling to the Live Logs memory buffer.", "fileLogsTitle": "Stored log explorer", "fileLogsMode": "LOCAL / READ-ONLY / BOUNDED", - "fileLogsHelp": "Reads only configured RustProxy log files. It is independent from Live Logs and never accepts an arbitrary filesystem path.", + "fileLogsHelp": "Searches bounded RustProxy access/error logs and parses structured fields without exposing filesystem paths.", "fileLogsSettingsHelp": "Browse configured access and error files with server-side filters. File logging can be off while Live Logs continues to use its memory buffer.", "openLogs": "Open Logs", "fileLogsSource": "Source", @@ -1992,7 +1997,7 @@ "fileLogsEmpty": "No records match the current filters.", "fileLogsDetails": "Record details", "fileLogsSelectRecord": "Select a record to inspect its JSON.", - "fileLogsSearchPlaceholder": "IP, path, error, request id...", + "fileLogsSearchPlaceholder": "req: GET AND !country: Poland", "fileLogsTruncated": "scan limit reached", "fileLogsSummary": "{from}–{to} of {total} matches · scanned {bytes} / {lines} lines", "fileLogsMatchedBy": "Matched by", @@ -2009,4 +2014,4 @@ "refresh": "Refresh", "applyFilters": "Apply filters" } -} +} \ No newline at end of file diff --git a/langs/pl_pl.json b/langs/pl_pl.json index c524f50..917fbd1 100644 --- a/langs/pl_pl.json +++ b/langs/pl_pl.json @@ -303,7 +303,6 @@ "vhostsDescription": "Zarządzaj domenami, frontendami, backendami i politykami hostów.", "vhHealthTitle": "Backendy", "vhManage": "Zarządzaj vhostami", - "vhHealthHint": "Stan bieżący: aktywność obejmuje całą wymianę; transfer to średnia EWMA danych.", "vhHealthEmpty": "Brak danych diagnostycznych backendów.", "vhVhost": "Vhost", "vhActive": "Aktywne", @@ -1975,12 +1974,18 @@ "vhEnableHint": "Określa, czy ten vhost przyjmuje ruch.", "backendColumnsRequired": "Co najmniej jedna kolumna tabeli backendów musi pozostać widoczna.", "logs": "Logs", + "storedLogs": "Zapisane logi", + "fileLogsSearchHelp": "Składnia wyszukiwania", + "fileLogsSearchHelpTitle": "Wyszukiwanie po polach jak w Kibanie", + "fileLogsSearchHelpText": "Używaj warunków pole:wartość. Warunki łączy AND; znak ! przed warunkiem wyklucza wynik. Cudzysłowy zachowują spacje.", + "fileLogsParsedFields": "Sparsowane pola", + "fileLogsRawJson": "Surowy JSON", "filelogs": "Logs", "filelogsTitle": "Przeglądarka zapisanych logów", "filelogsDescription": "Przeszukuj skonfigurowane logi RustProxy na dysku niezależnie od bufora Live Logs.", "fileLogsTitle": "Przeglądarka zapisanych logów", "fileLogsMode": "LOKALNIE / TYLKO ODCZYT / LIMITY", - "fileLogsHelp": "Czyta wyłącznie skonfigurowane pliki logów RustProxy. Działa niezależnie od Live Logs i nie przyjmuje dowolnej ścieżki systemu plików.", + "fileLogsHelp": "Przeszukuje logi access/error RustProxy w ustalonych limitach i parsuje pola bez ujawniania ścieżek systemu plików.", "fileLogsSettingsHelp": "Przeglądaj skonfigurowane pliki access/error z filtrami po stronie serwera. Logowanie do plików może być wyłączone, a Live Logs nadal korzysta z bufora pamięci.", "openLogs": "Otwórz Logs", "fileLogsSource": "Źródło", @@ -1992,7 +1997,7 @@ "fileLogsEmpty": "Brak rekordów pasujących do filtrów.", "fileLogsDetails": "Szczegóły rekordu", "fileLogsSelectRecord": "Wybierz rekord, aby zobaczyć jego JSON.", - "fileLogsSearchPlaceholder": "IP, ścieżka, błąd, request id...", + "fileLogsSearchPlaceholder": "req: GET AND !country: Poland", "fileLogsTruncated": "osiągnięto limit skanowania", "fileLogsSummary": "{from}–{to} z {total} wyników · przeskanowano {bytes} / {lines} linii", "fileLogsMatchedBy": "Dopasowanie przez", @@ -2009,4 +2014,4 @@ "refresh": "Odśwież", "applyFilters": "Zastosuj filtry" } -} +} \ No newline at end of file diff --git a/make_zip.py b/make_zip.py index 54cc9db..e6e418d 100755 --- a/make_zip.py +++ b/make_zip.py @@ -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.1.zip') + parser.add_argument('output', nargs='?', type=Path, default=ROOT.parent / 'RustProxy-0.26.2.zip') output = parser.parse_args().output.resolve() paths = files() checksum = ROOT / 'SHA256SUMS' diff --git a/scripts/check_source.py b/scripts/check_source.py index d36171d..0830bd1 100755 --- a/scripts/check_source.py +++ b/scripts/check_source.py @@ -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.1" +assert manifest["package"]["version"] == "0.26.2" 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.1"' in lock + assert 'name = "rust-proxy"\nversion = "0.26.2"' in lock assert 'name = "systemstat"' in lock and 'name = "metrics-process"' in lock and 'name = "sysinfo"' in lock, "Cargo.lock must be resolver-generated after adding cross-platform statistics libraries" for release_doc, marker in { - "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", + "README.md": "# RustProxy 0.26.2", + "CHANGELOG.md": "## 0.26.2", + "ROADMAP.md": "## 0.26.2", + "AGENTS.md": "Current source version: 0.26.2.", + "docs/TEST_REPORT.md": "# Verification report - RustProxy 0.26.2", }.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.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']: +for marker in ['"version": "0.26.2"', '"/api/traffic"', '"capture_user_agent"', '"capture_country"', '"hot_auto_trim_enabled"', '"hot_auto_trim_after_seconds"', '"hot_auto_trim_target_percent"', '"/api/config/{section}"', '"ConfigSectionUpdate"', '"cold_on_idle"', '"cold_on_age"', '"cold_max_hot_seconds"', '"cold_on_lru"', '"cold_on_shutdown"', '"cold_gc_interval_seconds"', '"cold_gc_max_delete"', '"collapse_enabled"', '"collapse_wait_timeout_ms"', '"collapse_timeout_fallback"', '"stale_if_error_timeout"', '"stale_if_error_connection"', '"stale_if_error_dns"', '"stale_if_error_tls"', '"stale_if_error_statuses"', '"proactive_refresh_enabled"', '"proactive_refresh_before_expiry_seconds"', '"proactive_refresh_percent"', '"proactive_refresh_max_inflight"', '"TransformSettings"', '"TransformOverrides"', '"/api/cache/cold/cleanup"', '"/api/cache/invalidate"', '"/api/cache/bans"', '"/api/cache/bans/{id}"', '"CacheInvalidationRequest"', '"CacheInvalidationResponse"', '"ColdCleanupJob"', '"match_type"', '"regex_insensitive"', '"rewrite"', '"response"', '"covered_hosts"', '"default": true']: assert marker in openapi, marker 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.1"' in openapi +assert '"RuntimeInformation"' in openapi and '"version": "0.26.2"' 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.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("PASS: 0.26.2 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.") diff --git a/scripts/deploy_core.py b/scripts/deploy_core.py index 3203bca..ffe6b64 100755 --- a/scripts/deploy_core.py +++ b/scripts/deploy_core.py @@ -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.1'}) + request = urllib.request.Request(url, headers={'User-Agent': 'RustProxy-installer/0.26.2'}) 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.') diff --git a/scripts/docs_check.py b/scripts/docs_check.py index f224a1e..ad2e965 100755 --- a/scripts/docs_check.py +++ b/scripts/docs_check.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Rust-independent documentation/Reader consistency checks for the 0.26.1 release.""" +"""Rust-independent documentation/Reader consistency checks for the 0.26.2 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.1") +assert readme.startswith("# RustProxy 0.26.2") 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.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 roadmap.startswith("# RustProxy roadmap") and "## 0.26.2" 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.1", "0.26.0", "0.25.6", "0.25.5", "0.25.4", "0.25.3"]: +for release in ["0.26.2", "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.1") < changelog.index("## 0.26.0") < changelog.index("## 0.25.6") < changelog.index("## 0.25.5") +assert changelog.index("## 0.26.2") < 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}" diff --git a/scripts/preflight.py b/scripts/preflight.py index 5736f08..4e44671 100755 --- a/scripts/preflight.py +++ b/scripts/preflight.py @@ -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.1 source tree: missing {path}") + raise ValueError(f"Incomplete RustProxy 0.26.2 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.1 entry point; extract the complete package, not individual files.") + raise ValueError("src/lib.rs is not the 0.26.2 entry point; extract the complete package, not individual files.") return messages diff --git a/scripts/release_smoke.py b/scripts/release_smoke.py index 69b08b3..580066c 100755 --- a/scripts/release_smoke.py +++ b/scripts/release_smoke.py @@ -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.1"' in (target / "Cargo.toml").read_text(encoding="utf-8") + assert 'version = "0.26.2"' 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.1" + assert manifest["to_version"] == "0.26.2" 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.1"' in openapi_0241 + assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.26.2"' 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.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"']: + for marker in ['"version": "0.26.2"', '"memory_preset"', '"cold_preset"', '"memory_budget_bytes"', '"max_inflight"', '"preset"', '"session_days"', '"session_lifetime_seconds"', '"accept_local_listener_risk"', '"backend_header"', '"backend_timing_header"', '"cold_on_age"', '"match_type"', '"regex_insensitive"', '"NotFoundHost"', '"not_found_hosts"', '"PrometheusSettings"', '"allow_trusted_network_without_token"', '"include_docker_vhosts"', '"include_streams"', '"include_logging"']: assert marker in openapi, marker assert '"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.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.") + print("PASS: 0.26.2 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__": diff --git a/scripts/swagger_assets.py b/scripts/swagger_assets.py index 37bd6b1..b37345d 100755 --- a/scripts/swagger_assets.py +++ b/scripts/swagger_assets.py @@ -85,7 +85,7 @@ def download(destination: Path) -> None: """TLS verification uses Python's default trust store; never executes a shell.""" started = time.monotonic() opener = build_opener(HttpsRedirects()) - request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.1"}) + request = Request(DOWNLOAD_URL, headers={"User-Agent": "RustProxy-build/0.26.2"}) with opener.open(request, timeout=30) as response, destination.open("wb") as output: length = response.headers.get("Content-Length") if length and int(length) > MAX_ARCHIVE: diff --git a/scripts/upgrade.py b/scripts/upgrade.py index e9aa8e8..5d4068c 100755 --- a/scripts/upgrade.py +++ b/scripts/upgrade.py @@ -46,7 +46,7 @@ def upgrade(source: Path, target: Path, *, stopped: bool, data_dir: Path | None for name in MANAGED: if (target / name).is_symlink(): raise ValueError(f"Managed target {name} is a symlink; refusing to replace it.") - backup = target / ".upgrade-backup" / ("before-0.26.1-" + uuid.uuid4().hex[:12]) + backup = target / ".upgrade-backup" / ("before-0.26.2-" + 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.1", "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.2", "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.1. Previous managed source files: {backup}") + print(f"Installed RustProxy 0.26.2. Previous managed source files: {backup}") print("Preserved data, .env, .git, target and other unmanaged files. No database migration was run.") print(f"Next: cd {args.target}; ./dev.sh test; ./dev.sh") diff --git a/scripts/web_server_contracts.py b/scripts/web_server_contracts.py index c446465..12c8a40 100755 --- a/scripts/web_server_contracts.py +++ b/scripts/web_server_contracts.py @@ -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 0.26.1", + "RustProxy 0.26.2", "traversal_and_symlink_escape_are_not_served", "try_files_rewrites_cache_rules_headers_and_downloads_work", "precompressed_sidecars_keep_mime_validators_and_range_semantics", diff --git a/src/admin/documentation/documents.rs b/src/admin/documentation/documents.rs index d7faffc..502aabc 100644 --- a/src/admin/documentation/documents.rs +++ b/src/admin/documentation/documents.rs @@ -399,6 +399,14 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[ "/docs/V0.25.1_AUDIT.md" )), ), + ( + "docs/V0.26.2_IMPLEMENTATION.md", + "technical", + include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/docs/V0.26.2_IMPLEMENTATION.md" + )), + ), ( "docs/V0.26.1_IMPLEMENTATION.md", "technical", diff --git a/src/admin/file_log_search.rs b/src/admin/file_log_search.rs new file mode 100644 index 0000000..c656fc9 --- /dev/null +++ b/src/admin/file_log_search.rs @@ -0,0 +1,384 @@ +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)), + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum FileLogSearchClause { + Any { + value: String, + negated: bool, + }, + Field { + field: String, + value: String, + negated: bool, + }, +} + +fn tokenize_file_log_search(input: &str) -> Result, ApiError> { + let mut tokens = Vec::new(); + let mut current = String::new(); + let mut quoted = false; + let mut escaped = false; + for ch in input.chars() { + if escaped { + current.push(ch); + escaped = false; + continue; + } + if quoted && ch == '\\' { + escaped = true; + continue; + } + if ch == '"' { + quoted = !quoted; + continue; + } + if ch.is_whitespace() && !quoted { + if !current.is_empty() { + tokens.push(std::mem::take(&mut current)); + } + continue; + } + current.push(ch); + } + if quoted || escaped { + return Err(ApiError( + StatusCode::BAD_REQUEST, + "invalid file log search syntax".into(), + )); + } + if !current.is_empty() { + tokens.push(current); + } + Ok(tokens) +} + +fn parse_file_log_search(input: &str) -> Result, ApiError> { + let tokens = tokenize_file_log_search(input)?; + let mut clauses = Vec::new(); + let mut index = 0usize; + while index < tokens.len() { + let token = &tokens[index]; + if token.eq_ignore_ascii_case("AND") { + index += 1; + continue; + } + if token.eq_ignore_ascii_case("OR") { + return Err(ApiError( + StatusCode::BAD_REQUEST, + "file log search supports AND and negation, not OR".into(), + )); + } + let (negated, term) = token + .strip_prefix('!') + .map_or((false, token.as_str()), |value| (true, value)); + if term.is_empty() { + return Err(ApiError( + StatusCode::BAD_REQUEST, + "invalid file log search syntax".into(), + )); + } + if let Some((field, inline_value)) = term.split_once(':') { + if field.is_empty() + || field.len() > 128 + || !field + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.')) + { + return Err(ApiError( + StatusCode::BAD_REQUEST, + "invalid file log search field".into(), + )); + } + let value = if inline_value.is_empty() { + index += 1; + let Some(next) = tokens.get(index) else { + return Err(ApiError( + StatusCode::BAD_REQUEST, + "missing file log search value".into(), + )); + }; + if next.eq_ignore_ascii_case("AND") || next.eq_ignore_ascii_case("OR") { + return Err(ApiError( + StatusCode::BAD_REQUEST, + "missing file log search value".into(), + )); + } + next.clone() + } else { + inline_value.to_owned() + }; + if value.is_empty() || value.len() > 256 { + return Err(ApiError( + StatusCode::BAD_REQUEST, + "invalid file log search value".into(), + )); + } + clauses.push(FileLogSearchClause::Field { + field: field.to_ascii_lowercase(), + value: value.to_ascii_lowercase(), + negated, + }); + } else { + clauses.push(FileLogSearchClause::Any { + value: term.to_ascii_lowercase(), + negated, + }); + } + if clauses.len() > 32 { + return Err(ApiError( + StatusCode::BAD_REQUEST, + "too many file log search clauses".into(), + )); + } + index += 1; + } + Ok(clauses) +} + +fn file_log_field_alias(field: &str) -> &str { + match field { + "req" | "request" => "method", + "ip" | "client" => "client_ip", + "source" => "_source", + "time" => "timestamp", + other => other, + } +} + +fn file_log_field<'a>(value: &'a Value, field: &str) -> Option<&'a Value> { + let mut current = value; + for part in file_log_field_alias(field).split('.') { + let object = current.as_object()?; + current = object.get(part).or_else(|| { + object + .iter() + .find(|(key, _)| key.eq_ignore_ascii_case(part)) + .map(|(_, value)| value) + })?; + } + Some(current) +} + +fn wildcard_match_ci(candidate: &str, pattern: &str) -> bool { + if !pattern.contains('*') { + return candidate.contains(pattern); + } + if pattern == "*" { + return true; + } + let starts_wild = pattern.starts_with('*'); + let ends_wild = pattern.ends_with('*'); + let parts = pattern + .split('*') + .filter(|part| !part.is_empty()) + .collect::>(); + if parts.is_empty() { + return true; + } + let mut cursor = 0usize; + for (index, part) in parts.iter().enumerate() { + let Some(found) = candidate[cursor..].find(part) else { + return false; + }; + if index == 0 && !starts_wild && found != 0 { + return false; + } + cursor += found + part.len(); + } + ends_wild || cursor == candidate.len() +} + +fn numeric_match(candidate: f64, pattern: &str) -> Option { + if let Some(raw) = pattern.strip_prefix(">=") { + return raw + .parse::() + .ok() + .map(|expected| candidate >= expected); + } + if let Some(raw) = pattern.strip_prefix("<=") { + return raw + .parse::() + .ok() + .map(|expected| candidate <= expected); + } + if let Some(raw) = pattern.strip_prefix('>') { + return raw.parse::().ok().map(|expected| candidate > expected); + } + if let Some(raw) = pattern.strip_prefix('<') { + return raw.parse::().ok().map(|expected| candidate < expected); + } + None +} + +fn file_log_value_matches(value: &Value, pattern: &str) -> bool { + match value { + Value::Null => pattern == "null", + Value::Bool(value) => wildcard_match_ci(if *value { "true" } else { "false" }, pattern), + Value::Number(value) => { + let rendered = value.to_string(); + if pattern.len() == 3 && pattern.ends_with("xx") { + return rendered.as_bytes().first() == pattern.as_bytes().first(); + } + numeric_match(value.as_f64().unwrap_or_default(), pattern) + .unwrap_or_else(|| wildcard_match_ci(&rendered, pattern)) + } + Value::String(value) => { + let candidate = value.to_ascii_lowercase(); + if pattern.ends_with("xx") && pattern.len() == 3 && candidate.len() == 3 { + return candidate.as_bytes().first() == pattern.as_bytes().first(); + } + if let Ok(candidate_number) = candidate.parse::() { + if let Some(result) = numeric_match(candidate_number, pattern) { + return result; + } + } + wildcard_match_ci(&candidate, pattern) + } + Value::Array(values) => values + .iter() + .any(|value| file_log_value_matches(value, pattern)), + Value::Object(_) => { + let mut rendered = String::new(); + append_search_values(value, &mut rendered); + wildcard_match_ci(rendered.trim(), pattern) + } + } +} + +fn record_matches_search(value: &Value, clauses: &[FileLogSearchClause]) -> bool { + clauses.iter().all(|clause| { + let (matched, negated) = match clause { + FileLogSearchClause::Any { + value: needle, + negated, + } => { + let mut searchable = String::new(); + append_search_values(value, &mut searchable); + (wildcard_match_ci(searchable.trim(), needle), *negated) + } + FileLogSearchClause::Field { + field, + value: needle, + negated, + } => ( + file_log_field(value, field) + .is_some_and(|value| file_log_value_matches(value, needle)), + *negated, + ), + }; + matched != negated + }) +} + +fn valid_raw_field_key(value: &str) -> bool { + !value.is_empty() + && value.len() <= 128 + && value + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '_' | '-' | '.')) +} + +fn parse_raw_error_record(line: &str) -> Value { + let mut parts = line.split_whitespace(); + let Some(timestamp) = parts.next() else { + return json!({"raw": line}); + }; + let Some(level) = parts.next() else { + return json!({"raw": line}); + }; + let Some(target) = parts.next() else { + return json!({"raw": line}); + }; + let rest = parts.collect::>(); + let first_field = rest.iter().position(|token| { + token + .split_once('=') + .is_some_and(|(key, _)| valid_raw_field_key(key)) + }); + let message_end = first_field.unwrap_or(rest.len()); + let mut object = serde_json::Map::new(); + object.insert("timestamp".into(), Value::String(timestamp.into())); + object.insert("level".into(), Value::String(level.into())); + object.insert("target".into(), Value::String(target.into())); + if message_end > 0 { + object.insert( + "message".into(), + Value::String(rest[..message_end].join(" ")), + ); + } + let mut index = message_end; + while index < rest.len() { + let Some((key, start)) = rest[index].split_once('=') else { + index += 1; + continue; + }; + if !valid_raw_field_key(key) { + index += 1; + continue; + } + let mut pieces = vec![start]; + index += 1; + while index < rest.len() + && !rest[index] + .split_once('=') + .is_some_and(|(next_key, _)| valid_raw_field_key(next_key)) + { + pieces.push(rest[index]); + index += 1; + } + object.insert(key.into(), Value::String(pieces.join(" "))); + } + object.insert("raw".into(), Value::String(line.into())); + Value::Object(object) +} + +fn collect_file_log_fields(value: &Value, output: &mut std::collections::BTreeSet) { + fn walk(prefix: &str, value: &Value, output: &mut std::collections::BTreeSet) { + if output.len() >= 64 { + return; + } + match value { + Value::Object(object) => { + for (key, child) in object { + if key == "_source_kind" || output.len() >= 64 { + continue; + } + let path = if prefix.is_empty() { + key.clone() + } else { + format!("{prefix}.{key}") + }; + if child.is_object() { + walk(&path, child, output); + } else { + output.insert(path); + } + } + } + _ if !prefix.is_empty() => { + output.insert(prefix.into()); + } + _ => {} + } + } + walk("", value, output); +} diff --git a/src/admin/file_logs.rs b/src/admin/file_logs.rs index 8dfe760..65e5492 100644 --- a/src/admin/file_logs.rs +++ b/src/admin/file_logs.rs @@ -4,7 +4,6 @@ const FILE_LOG_SCAN_LINES: usize = 20_000; const FILE_LOG_MAX_SOURCES: usize = 32; const FILE_LOG_MAX_LIMIT: usize = 500; const FILE_LOG_MAX_OFFSET: usize = 10_000; - #[derive(Clone)] struct FileLogSource { id: String, @@ -17,7 +16,6 @@ struct FileLogSource { size: u64, modified_ms: u64, } - #[derive(Default, Deserialize)] #[serde(default, deny_unknown_fields)] struct FileLogQuery { @@ -30,7 +28,6 @@ struct FileLogQuery { offset: Option, limit: Option, } - fn file_modified_ms(metadata: &std::fs::Metadata) -> u64 { metadata .modified() @@ -39,7 +36,6 @@ fn file_modified_ms(metadata: &std::fs::Metadata) -> u64 { .map(|duration| duration.as_millis() as u64) .unwrap_or(0) } - fn source_from_path( id: String, label: String, @@ -70,7 +66,6 @@ fn source_from_path( .map_or(0, file_modified_ms), } } - fn configured_file_log_sources(config: &crate::config::Config) -> Vec { let mut sources = Vec::new(); if config.logging.file.per_vhost { @@ -134,7 +129,6 @@ fn configured_file_log_sources(config: &crate::config::Config) -> Vec Value { Value::Array( sources @@ -155,7 +149,6 @@ fn file_log_sources_json(sources: &[FileLogSource]) -> Value { .collect(), ) } - async fn file_logs(State(state): State>) -> Result, ApiError> { let config = state.config.load().envelope.config.clone(); let sources = tokio::task::spawn_blocking(move || configured_file_log_sources(&config)) @@ -178,7 +171,6 @@ async fn file_logs(State(state): State>) -> Result, ApiErro } }))) } - fn open_file_log(path: &std::path::Path) -> std::io::Result { let mut options = std::fs::OpenOptions::new(); options.read(true); @@ -197,7 +189,6 @@ fn open_file_log(path: &std::path::Path) -> std::io::Result { } Ok(file) } - fn read_tail( source: &FileLogSource, budget: u64, @@ -228,7 +219,6 @@ fn read_tail( .collect::>(); Ok((lines, bytes, truncated || iter.next().is_some())) } - fn copy_mapped_field( object: &mut serde_json::Map, format: &crate::logging::JsonFormat, @@ -251,16 +241,35 @@ fn normalized_record( line: &str, format: &crate::logging::JsonFormat, ) -> Value { - let mut value = serde_json::from_str::(line).unwrap_or_else(|_| json!({"raw": line})); + let mut value = if source.format == "RAW" { + parse_raw_error_record(line) + } else { + serde_json::from_str::(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", + "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); } @@ -297,6 +306,7 @@ fn normalized_record( }); object.insert("_source".into(), Value::String(source.id.clone())); object.insert("_source_kind".into(), Value::String(source.kind.into())); + object.insert("_source_format".into(), Value::String(source.format.into())); value } @@ -306,27 +316,12 @@ 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 { +fn record_matches(value: &Value, query: &FileLogQuery, search: &[FileLogSearchClause]) -> bool { let Some(object) = value.as_object() else { 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 !record_matches_search(value, search) { + return false; } if let Some(event) = query .event @@ -374,7 +369,7 @@ fn validate_file_log_query(query: &FileLogQuery) -> Result<(), ApiError> { .is_none_or(|v| v.len() <= max && !v.chars().any(char::is_control)) }; if !text_ok(&query.source, 192) - || !text_ok(&query.q, 256) + || !text_ok(&query.q, 512) || !text_ok(&query.client_ip, 128) || !text_ok(&query.vhost, 256) || query.offset.unwrap_or(0) > FILE_LOG_MAX_OFFSET @@ -407,6 +402,7 @@ async fn file_logs_query( Query(query): Query, ) -> Result, ApiError> { validate_file_log_query(&query)?; + let search = parse_file_log_search(query.q.as_deref().unwrap_or(""))?; let config = state.config.load().envelope.config.clone(); tokio::task::spawn_blocking(move || { let mut sources = configured_file_log_sources(&config); @@ -428,7 +424,6 @@ async fn file_logs_query( sources.truncate(FILE_LOG_MAX_SOURCES); } } - let q = query.q.clone().unwrap_or_default().to_ascii_lowercase(); let offset = query.offset.unwrap_or(0); let limit = query.limit.unwrap_or(100).clamp(1, FILE_LOG_MAX_LIMIT); let mut remaining_bytes = FILE_LOG_SCAN_BYTES_TOTAL; @@ -436,6 +431,7 @@ async fn file_logs_query( let mut remaining_lines = FILE_LOG_SCAN_LINES; let mut scanned_lines = 0usize; let mut records = Vec::new(); + let mut fields = std::collections::BTreeSet::new(); for (index, source) in sources.iter().enumerate() { if remaining_bytes == 0 || remaining_lines == 0 { truncated = true; @@ -455,7 +451,8 @@ async fn file_logs_query( continue; } let value = normalized_record(source, &line, &log_format); - if record_matches(&value, &query, &q) { + if record_matches(&value, &query, &search) { + collect_file_log_fields(&value, &mut fields); records.push(value); } } @@ -465,8 +462,15 @@ async fn file_logs_query( } } records.sort_by(|a, b| { - let time = |value: &Value| value.get("time_ms").and_then(Value::as_u64).unwrap_or(0); - time(b).cmp(&time(a)) + let numeric = |value: &Value| value.get("time_ms").and_then(Value::as_u64); + match (numeric(a), numeric(b)) { + (Some(left), Some(right)) => right.cmp(&left), + _ => b + .get("timestamp") + .and_then(Value::as_str) + .unwrap_or("") + .cmp(a.get("timestamp").and_then(Value::as_str).unwrap_or("")), + } }); let total = records.len(); let page = records @@ -482,6 +486,7 @@ async fn file_logs_query( "truncated": truncated, "scanned_bytes": scanned_bytes, "scanned_lines": scanned_lines, + "fields": fields, "sources": file_log_sources_json(&sources), }))) }) diff --git a/src/admin/mod.rs b/src/admin/mod.rs index 0bfbe39..b2215c2 100644 --- a/src/admin/mod.rs +++ b/src/admin/mod.rs @@ -7,6 +7,7 @@ include!("preferences.rs"); include!("status.rs"); include!("logging_api.rs"); include!("file_logs.rs"); +include!("file_log_search.rs"); include!("operations.rs"); include!("certificates.rs"); include!("tail.rs"); diff --git a/src/admin/openapi.json b/src/admin/openapi.json index be70245..23988a3 100644 --- a/src/admin/openapi.json +++ b/src/admin/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.0.3", "info": { "title": "RustProxy Administration API", - "version": "0.26.1", + "version": "0.26.2", "description": "Administration API for the active panel origin. Sign in through the panel; the same-origin HttpOnly cookie authenticates all private operations. Mutations also require the exact Origin, X-RustProxy-Request: 1 and a session-bound X-CSRF-Token. Loopback HTTP is the default; Settings can enable HTTPS before using a non-loopback administration bind. This document contains no live configuration or credentials. Object schemas marked extensible are summaries: full configuration validation is authoritative in Rust." }, "servers": [ @@ -4928,6 +4928,13 @@ "type": "integer", "format": "int64" }, + "fields": { + "type": "array", + "description": "Bounded discovered leaf field names available for dynamic Stored Logs columns.", + "items": { + "type": "string" + } + }, "sources": { "type": "array", "items": { @@ -9685,10 +9692,10 @@ "name": "q", "in": "query", "required": false, - "description": "Case-insensitive text search in the bounded scanned records.", + "description": "Case-insensitive field query. Supports field:value, AND, ! negation, quoted values, wildcards and numeric comparisons; bare terms search all values.", "schema": { "type": "string", - "maxLength": 256 + "maxLength": 512 } }, { diff --git a/src/admin/preferences.rs b/src/admin/preferences.rs index 37338d5..b54f394 100644 --- a/src/admin/preferences.rs +++ b/src/admin/preferences.rs @@ -13,10 +13,7 @@ const LIVE_LOG_COLUMNS: &[&str] = &[ "websocket", "upstream_error", ]; -const FILE_LOG_COLUMNS: &[&str] = &[ - "time", "event", "status", "client_ip", "vhost", "transport", "matched_by", "sni", - "authority", "record", "source", -]; +const FILE_LOG_COLUMN_MAX: usize = 64; const BACKEND_TABLE_COLUMNS: &[&str] = &[ "vhost", "backend", @@ -38,10 +35,18 @@ fn default_admin_theme() -> String { "dark".to_owned() } fn default_file_log_columns() -> Vec { - ["time", "event", "status", "client_ip", "vhost", "record", "source"] - .into_iter() - .map(str::to_owned) - .collect() + [ + "time", + "event", + "status", + "client_ip", + "vhost", + "record", + "source", + ] + .into_iter() + .map(str::to_owned) + .collect() } fn default_backend_table_columns() -> Vec { BACKEND_TABLE_COLUMNS @@ -104,7 +109,7 @@ fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiE } } if preferences.file_log_columns.is_empty() - || preferences.file_log_columns.len() > FILE_LOG_COLUMNS.len() + || preferences.file_log_columns.len() > FILE_LOG_COLUMN_MAX { return Err(ApiError( StatusCode::UNPROCESSABLE_ENTITY, @@ -113,7 +118,11 @@ fn validate_admin_preferences(preferences: &AdminPreferences) -> Result<(), ApiE } 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()) { + if column.is_empty() + || column.len() > 128 + || column.chars().any(char::is_control) + || !seen.insert(column.as_str()) + { return Err(ApiError( StatusCode::UNPROCESSABLE_ENTITY, "invalid stored-log column preference".into(), diff --git a/src/admin/tests/part_06.rs b/src/admin/tests/part_06.rs index 8ddce7b..63216c9 100644 --- a/src/admin/tests/part_06.rs +++ b/src/admin/tests/part_06.rs @@ -1,100 +1,152 @@ - #[test] - fn stored_log_inventory_keeps_existing_files_visible_when_writer_is_disabled() { - use std::io::Write; +#[test] +fn stored_log_inventory_keeps_existing_files_visible_when_writer_is_disabled() { + use std::io::Write; - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("access.jsonl"); - let mut file = std::fs::File::create(&path).unwrap(); - writeln!(file, r#"{{"event":"request","status":200}}"#).unwrap(); + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("access.jsonl"); + let mut file = std::fs::File::create(&path).unwrap(); + writeln!(file, r#"{{"event":"request","status":200}}"#).unwrap(); - let mut config = Config::default(); - config.logging.file.enabled = false; - config.logging.file.per_vhost = false; - config.logging.file.path = path.to_string_lossy().into_owned(); - let sources = configured_file_log_sources(&config); - let access = sources.iter().find(|source| source.id == "access").unwrap(); - assert!(!access.enabled); - assert!(access.exists); - assert!(access.size > 0); - assert_eq!(access.path, path); + let mut config = Config::default(); + config.logging.file.enabled = false; + config.logging.file.per_vhost = false; + config.logging.file.path = path.to_string_lossy().into_owned(); + let sources = configured_file_log_sources(&config); + let access = sources.iter().find(|source| source.id == "access").unwrap(); + assert!(!access.enabled); + assert!(access.exists); + assert!(access.size > 0); + assert_eq!(access.path, path); +} + +#[test] +fn stored_log_tail_enforces_line_budget() { + use std::io::Write; + + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("access.jsonl"); + let mut file = std::fs::File::create(&path).unwrap(); + for id in 0..10 { + writeln!(file, r#"{{"time_ms":{id},"event":"request"}}"#).unwrap(); } + drop(file); - #[test] - fn stored_log_tail_enforces_line_budget() { - use std::io::Write; + let source = source_from_path( + "access".into(), + "Access log".into(), + "access", + path, + false, + "JSONL", + ); + let (lines, _bytes, truncated) = read_tail(&source, 1024 * 1024, 3).unwrap(); + assert_eq!(lines.len(), 3); + assert!(lines[0].contains(r#""time_ms":9"#)); + assert!(lines[2].contains(r#""time_ms":7"#)); + assert!(truncated); +} - let dir = tempfile::tempdir().unwrap(); - let path = dir.path().join("access.jsonl"); - let mut file = std::fs::File::create(&path).unwrap(); - for id in 0..10 { - writeln!(file, r#"{{"time_ms":{id},"event":"request"}}"#).unwrap(); - } - drop(file); +#[test] +fn stored_log_query_rejects_oversized_source_identifier() { + let query = FileLogQuery { + source: Some("x".repeat(193)), + ..FileLogQuery::default() + }; + assert!(validate_file_log_query(&query).is_err()); +} - let source = source_from_path( - "access".into(), - "Access log".into(), - "access", - path, - false, - "JSONL", +#[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, + &parse_file_log_search("error").unwrap() + )); + assert!(record_matches( + &record, + &query, + &parse_file_log_search(r#"error:"HTTP parser""#).unwrap() + )); + assert!(record_matches( + &record, + &query, + &parse_file_log_search("sni:app.example.test AND !country:Poland").unwrap() + )); + assert!(!record_matches( + &record, + &query, + &parse_file_log_search("sni:missing").unwrap() + )); +} + +#[test] +fn stored_log_search_supports_field_aliases_negation_and_numeric_filters() { + let record = json!({ + "method":"GET", + "country":"Germany", + "status":503, + "path":"/api/users" + }); + assert!(record_matches_search( + &record, + &parse_file_log_search("req: GET AND !country: Poland AND status:>=500").unwrap() + )); + assert!(record_matches_search( + &record, + &parse_file_log_search("status:5xx path:/api/*").unwrap() + )); + assert!(!record_matches_search( + &record, + &parse_file_log_search("country:Poland").unwrap() + )); +} + +#[test] +fn stored_log_raw_error_parser_extracts_basic_fields() { + let record = parse_raw_error_record( + "2026-10-05T19:00:00.000Z ERROR rust_proxy::http upstream failed client_ip=192.0.2.10 status=502 error=connection reset", ); - let (lines, _bytes, truncated) = read_tail(&source, 1024 * 1024, 3).unwrap(); - assert_eq!(lines.len(), 3); - assert!(lines[0].contains(r#""time_ms":9"#)); - assert!(lines[2].contains(r#""time_ms":7"#)); - assert!(truncated); - } + assert_eq!(record["timestamp"], "2026-10-05T19:00:00.000Z"); + assert_eq!(record["level"], "ERROR"); + assert_eq!(record["target"], "rust_proxy::http"); + assert_eq!(record["message"], "upstream failed"); + assert_eq!(record["client_ip"], "192.0.2.10"); + assert_eq!(record["status"], "502"); + assert_eq!(record["error"], "connection reset"); +} - #[test] - fn stored_log_query_rejects_oversized_source_identifier() { - let query = FileLogQuery { - source: Some("x".repeat(193)), - ..FileLogQuery::default() - }; - assert!(validate_file_log_query(&query).is_err()); - } +#[cfg(unix)] +#[test] +fn stored_log_reader_refuses_symlink_targets() { + use std::os::unix::fs::symlink; - #[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() { - use std::os::unix::fs::symlink; - - let dir = tempfile::tempdir().unwrap(); - let real = dir.path().join("real.jsonl"); - let link = dir.path().join("link.jsonl"); - std::fs::write(&real, b"{}\n").unwrap(); - symlink(&real, &link).unwrap(); - assert!(open_file_log(&link).is_err()); - } + let dir = tempfile::tempdir().unwrap(); + let real = dir.path().join("real.jsonl"); + let link = dir.path().join("link.jsonl"); + std::fs::write(&real, b"{}\n").unwrap(); + symlink(&real, &link).unwrap(); + assert!(open_file_log(&link).is_err()); +} diff --git a/src/admin/web/app/core.js b/src/admin/web/app/core.js index 2eabda8..67e86f8 100644 --- a/src/admin/web/app/core.js +++ b/src/admin/web/app/core.js @@ -344,7 +344,7 @@ function authReturnDestination() { } function lock(notify = true) { const destination = pendingAuthDestination || authReturnDestination(), navigating = typeof location !== "undefined" && (location.protocol === "http:" || location.protocol === "https:"); - pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage(""); + pendingAuthDestination = destination; if (notify) adminAuthChannel?.postMessage("locked"); $("api-docs").hidden = true; $("logs-menu").hidden = true; resetAdminPreferences(); window.rustProxyCertificates?.reset(); window.rustProxySettings?.reset(); window.rustProxyTrafficPanel?.reset(); window.rustProxyCachePanel?.reset(); window.rustProxyTransformPanel?.reset(); window.rustProxyAcme?.reset(); window.rustProxyIntegrations?.reset(); window.rustProxyLogs?.reset(); window.rustProxyFileLogs?.reset(); $("logs-open").hidden = true; $("file-logs-open").hidden = true; window.rustProxyAclProfiles?.reset(); window.rustProxyVhosts?.reset(); window.rustProxyWebServer?.reset(); window.rustProxyDocker?.reset(); window.rustProxyRedirects?.reset(); window.rustProxyNotFound?.reset(); window.rustProxyStreams?.reset(); authEpoch++; stopLive(); csrf = ""; authenticated = false; sessionInfo = null; envelope = null; certificateInventory = new Map(); previous = null; samples = []; protocolSamples = { h1: [], h2: [], h3: [] }; transformSamples = []; systemSamples = []; connectionOpenSamples = []; connectionExchangeSamples = []; networkInterfaceSamples.clear(); lastLogs = null; latestStatus = null; clearSettingsDirty(); connected = false; busy = false; $("workspace").hidden = true; $("login").hidden = navigating; $("logout").hidden = true; $("password").value = ""; $("password-confirm").value = ""; $("setup-token").value = ""; $("password-form").reset(); $("config-editor").value = ""; $("logs").replaceChildren(); $("audit-rows").replaceChildren(); authMessage(""); for (const id of ["certificate-list", "host-policy-list"]) { $(id)?.replaceChildren(); } $("workspace").querySelectorAll("form").forEach(form => form.reset()); $("workspace").querySelectorAll("input:not([type=checkbox]):not([type=radio]),textarea").forEach(input => { input.value = ""; }); @@ -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.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 activate(info) { pendingAuthDestination = null; authMessage(""); $("api-docs").hidden = false; $("logs-menu").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); } diff --git a/src/admin/web/app/settings.js b/src/admin/web/app/settings.js index ff89a18..bc6cdde 100644 --- a/src/admin/web/app/settings.js +++ b/src/admin/web/app/settings.js @@ -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.1"; + $("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.26.2"; for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name]; $("compress-min").value = compression.min_bytes ?? 1024; syncServerMode(); } diff --git a/src/admin/web/file-logs.js b/src/admin/web/file-logs.js index 333e3a8..4ecf4de 100644 --- a/src/admin/web/file-logs.js +++ b/src/admin/web/file-logs.js @@ -3,44 +3,39 @@ 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"]; + const specialLabels = new Map([ + ["time", "time"], ["event", "fileLogsEvent"], ["status", "status"], + ["client_ip", "clientIp"], ["vhost", "vhost"], ["record", "fileLogsRecord"], + ["source", "fileLogsSource"], + ]); + let availableColumns = new Set(defaultColumnIds); 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 label = `${source.label} · ${source.format} · ${bytes(Number(source.size) || 0)}`; const option = el("option", label); option.value = source.id; return option; } function renderInventory(value) { inventory = value; - const selected = sourceSelect().value || "all"; + const select = sourceSelect(); + const selected = select.value || "all"; const sources = Array.isArray(value?.sources) ? value.sources : []; const options = [Object.assign(el("option", t("fileLogsAllSources")), { value: "all" }), ...sources.map(sourceOption)]; - sourceSelect().replaceChildren(...options); - sourceSelect().value = ["all", ...sources.map(source => source.id)].includes(selected) ? selected : "all"; - $("file-log-inventory").textContent = sources.length - ? sources.map(source => `${source.label}: ${source.path} (${bytes(source.size || 0)})`).join(" · ") - : t("fileLogsNoSources"); + select.replaceChildren(...options); + select.value = sources.some(source => source.id === selected) ? selected : "all"; } async function loadInventory() { - const value = await api("/api/file-logs"); renderInventory(value); return value; + const value = await api("/api/file-logs"); + renderInventory(value); + return value; } + + const currentLimit = () => Math.max(1, Math.min(500, Number($("file-log-limit").value) || 100)); + function entryTime(entry) { if (entry.time_ms) return new Date(Number(entry.time_ms)).toLocaleString(locale()); if (entry.timestamp) return String(entry.timestamp); @@ -54,24 +49,67 @@ if (entry.raw) return entry.raw; return entry.outcome || "—"; } - function entryStatus(entry) { - const status = Number(entry.status) || 0; - return status ? String(status) : "—"; + function nestedValue(entry, path) { + let value = entry; + for (const part of path.split(".")) { + if (!value || typeof value !== "object" || !Object.hasOwn(value, part)) return undefined; + value = value[part]; + } + return value; + } + function displayValue(value) { + if (value === undefined || value === null || value === "") return "—"; + if (typeof value === "object") return JSON.stringify(value); + return String(value); + } + function columnValue(entry, id) { + if (id === "time") return entryTime(entry); + if (id === "record") return entrySummary(entry); + if (id === "source") return entry._source || "—"; + return displayValue(nestedValue(entry, id)); + } + function columnLabel(id) { + const key = specialLabels.get(id); + return key ? t(key) : id; + } + function normalizedField(field) { + if (field === "_source") return "source"; + if (field === "_source_kind" || field === "_source_format") return null; + return field; + } + function orderedColumns() { + const preferred = defaultColumnIds.filter(id => availableColumns.has(id)); + const rest = [...availableColumns].filter(id => !defaultColumnIds.includes(id)).sort((a, b) => a.localeCompare(b, locale(), { sensitivity: "base" })); + return [...preferred, ...rest]; + } + function applyFields(fields = []) { + for (const field of fields) { + if (typeof field !== "string") continue; + const normalized = normalizedField(field); + if (normalized) availableColumns.add(normalized); + } + for (const id of visibleColumns) availableColumns.add(id); + syncColumnPicker(); syncHeaders(); } function syncHeaders() { - document.querySelectorAll("#file-logs-table-head [data-file-log-column]").forEach(header => { - header.hidden = !visibleColumns.has(header.dataset.fileLogColumn); + const row = $("file-logs-table-head").querySelector("tr"); + const headers = orderedColumns().filter(id => visibleColumns.has(id)).map(id => { + const header = el("th", columnLabel(id)); header.dataset.fileLogColumn = id; + const key = specialLabels.get(id); if (key) header.dataset.i18n = key; + return header; }); + row.replaceChildren(...headers); } function syncColumnPicker() { const picker = $("file-log-column-options"); - picker.replaceChildren(...columns.map(column => { + picker.replaceChildren(...orderedColumns().map(id => { 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; + const input = document.createElement("input"); input.type = "checkbox"; input.checked = visibleColumns.has(id); input.dataset.column = id; + const text = el("span", columnLabel(id)); + const key = specialLabels.get(id); if (key) text.dataset.i18n = key; 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); + if (input.checked) visibleColumns.add(id); else visibleColumns.delete(id); syncHeaders(); renderRows(); void savePreferences(); }); label.append(input, text); return label; @@ -79,9 +117,10 @@ } function applyPreferences(preferences) { const selected = Array.isArray(preferences?.file_log_columns) - ? preferences.file_log_columns.filter(id => columns.some(column => column.id === id)) + ? preferences.file_log_columns.filter(id => typeof id === "string" && id.length <= 128) : []; visibleColumns = new Set(selected.length ? selected : defaultColumnIds); + for (const id of visibleColumns) availableColumns.add(id); preferencesLoaded = true; syncColumnPicker(); syncHeaders(); renderRows(); } @@ -92,45 +131,64 @@ try { do { preferenceSaveQueued = false; - const selected = columns.filter(column => visibleColumns.has(column.id)).map(column => column.id); + const selected = orderedColumns().filter(id => visibleColumns.has(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 flattenFields(value, prefix = "", output = []) { + if (output.length >= 128) return output; + if (value && typeof value === "object" && !Array.isArray(value)) { + for (const [key, child] of Object.entries(value)) { + if (key === "_source_kind") continue; + const path = prefix ? `${prefix}.${key}` : key; + if (child && typeof child === "object" && !Array.isArray(child)) flattenFields(child, path, output); + else output.push([path, displayValue(child)]); + if (output.length >= 128) break; + } + } else if (prefix) output.push([prefix, displayValue(value)]); + return output; + } + function detailRow(entry) { + const row = el("tr", undefined, "file-log-detail-row"); + const cell = el("td"); cell.colSpan = Math.max(1, visibleColumns.size); + const detail = el("div", undefined, "file-log-inline-detail"); + const heading = el("strong", t("fileLogsParsedFields")); heading.dataset.i18n = "fileLogsParsedFields"; + const fields = el("div", undefined, "file-log-field-grid"); + for (const [key, value] of flattenFields(entry)) { + const item = el("div", undefined, "file-log-field"); + item.append(el("code", key), el("span", value)); fields.append(item); + } + const raw = el("details", undefined, "file-log-raw-json"); + const summary = el("summary", t("fileLogsRawJson")); summary.dataset.i18n = "fileLogsRawJson"; + raw.append(summary, el("pre", JSON.stringify(entry, null, 2), "metrics-example")); + detail.append(heading, fields, raw); cell.append(detail); row.append(cell); return row; + } function renderRows(entries = currentEntries) { currentEntries = entries; + const ids = orderedColumns().filter(id => visibleColumns.has(id)); 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); + for (const id of ids) { + const value = columnValue(entry, id), cell = el("td", value); cell.title = value; row.append(cell); } 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); - } + nodes.push(row); if (selectedIndex === index) nodes.push(detailRow(entry)); }); $("file-log-rows").replaceChildren(...nodes); $("file-logs-empty").hidden = entries.length !== 0; } function queryString() { const params = new URLSearchParams(); - params.set("source", sourceSelect().value || "all"); - for (const [id, name] of [["file-log-q", "q"], ["file-log-event", "event"], ["file-log-status", "status_class"], ["file-log-client", "client_ip"], ["file-log-vhost", "vhost"]]) { - const value = $(id).value.trim(); if (value && value !== "all") params.set(name, value); - } + const source = sourceSelect().value || "all"; + if (source !== "all") params.set("source", source); + const q = $("file-log-q").value.trim(); if (q) params.set("q", q); params.set("offset", String(offset)); params.set("limit", String(currentLimit())); return params.toString(); } @@ -142,6 +200,7 @@ if (!inventory) await loadInventory(); const value = await api(`/api/file-logs/query?${queryString()}`); lastTotal = Number(value.total) || 0; selectedIndex = null; + applyFields(Array.isArray(value.fields) ? value.fields : []); 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") @@ -156,16 +215,19 @@ function reset() { 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(); + availableColumns = new Set(defaultColumnIds); visibleColumns = new Set(defaultColumnIds); + sourceSelect().replaceChildren(Object.assign(el("option", t("fileLogsAllSources")), { value: "all" })); + $("file-log-rows").replaceChildren(); syncColumnPicker(); syncHeaders(); } - $("file-logs-open").addEventListener("click", () => setPanel("filelogs")); + $("file-logs-open").addEventListener("click", () => { $("logs-menu").open = false; 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-log-limit").addEventListener("change", () => refresh(true)); + sourceSelect().addEventListener("change", () => refresh(true)); $("file-logs-prev").addEventListener("click", () => { offset = Math.max(0, offset - currentLimit()); void refresh(); }); $("file-logs-next").addEventListener("click", () => { if (offset + currentLimit() < lastTotal) { offset += currentLimit(); void refresh(); } }); - sourceSelect().addEventListener("change", () => refresh(true)); syncColumnPicker(); syncHeaders(); window.rustProxyFileLogs = { refresh: () => refresh(false), reset, inventory: loadInventory, applyPreferences }; })(); diff --git a/src/admin/web/index.html b/src/admin/web/index.html index bba6c98..9457d14 100644 --- a/src/admin/web/index.html +++ b/src/admin/web/index.html @@ -64,13 +64,18 @@
CONTROL ROOM / Overview
+ hidden="">Documentation + +
-