v0.31.5
This commit is contained in:
1 parent
75104f87ba
commit
4cc994b3bc
86 files changed
+942
-223
No files matched your search
@@ -2,7 +2,7 @@
|
||||
|
||||
Read README.md, ROADMAP.md and docs/TEST_REPORT.md before making changes.
|
||||
The project name is RustProxy; the Cargo package/binary remains `rust-proxy` for
|
||||
compatibility. Current source version: 0.31.5.
|
||||
compatibility. Current source version: 0.31.6.
|
||||
|
||||
## Delivery
|
||||
|
||||
|
||||
@@ -3,8 +3,20 @@
|
||||
> Operator-facing release history. Historical engineering notes are kept in
|
||||
> [`docs/CHANGELOG_TECHNICAL_ARCHIVE.md`](docs/CHANGELOG_TECHNICAL_ARCHIVE.md).
|
||||
|
||||
## 0.31.6
|
||||
|
||||
- Preserve logo click as the original `href="/"` full-page navigation. For H3 to HTTP/1.1 backend forwarding, complete empty GET/HEAD receive streams through a bounded first-frame probe before forwarding. Hyper already special-cases GET/HEAD H1 encoding; this change targets H3 receive-half lifecycle, not assumed chunked GET framing. Also bound H3 GET/HEAD cache and 404 preflight so a missing FIN cannot indefinitely prevent navigation; rewind/replay any nonempty body unchanged.
|
||||
- Do not abort all in-flight H3 responses when the H3 accept loop fails; stop accepting and drain the active request tasks within the normal grace bound.
|
||||
- Persist H3 pre-header peer cancels as status-0 diagnostic events rather than HTTP 400. For interrupted responses record phase, QUIC stream ID, method, query-free path, response status, sent byte count and redacted failure reason; preserve non-cancellation per-stream error reset behavior. Log start/completion/interruption of root-document `GET /` with a stream ID and response status in service INFO/WARN logs.
|
||||
- Add H3 navigation regression tests and iPhone reproduction checklist. Retain the 0.31.5 Stored Logs mobile layout, HOT/COLD cache preservation and administrator Origin protections. Rust build and real iOS testing are still required.
|
||||
|
||||
## 0.31.5
|
||||
|
||||
- Make the Stored Logs explorer responsive: compact, keyboard-expandable record cards and readable parsed fields on narrow screens; keep user-selected columns accessible without widening the whole page. Desktop table behavior remains available.
|
||||
- Do not persist a synthetic HTTP 400 for a QUIC peer cancelling an incomplete H3 request with `H3_REQUEST_CANCELLED`/`H3_NO_ERROR` on refresh. Preserve real HTTP/3 protocol-error logging and avoid a second internal reset after identified peer cancellation. The related white-page symptom still needs live browser/QUIC validation.
|
||||
- Preserve HOT/COLD cache compatibility identity when per-vhost HTTP/2 or HTTP/3 enablement changes; these are frontend protocol switches, not cached-response representation changes. No cache schema migration.
|
||||
- Clarify that the optional public panel HTTPS origin pins the external scheme/authority for Origin/CSRF validation with a trusted reverse proxy, while additional allowed DNS hostnames only expand the strict Host allowlist. Add source/Rust regression cases and a browser-width layout smoke test; compiler and live wire acceptance are still required.
|
||||
|
||||
- Give repeated-value analysis its own Stored Logs tab and move the rotation-consolidation option to a compact top-right control. Preserve the browser-local consolidation choice.
|
||||
|
||||
- Fingerprint panel, login and documentation JS/CSS asset filenames with a random 16-hex build identifier independent of the application version. Reject legacy JS/CSS URLs (404), retaining unversioned JSON/SVG; enforce exact fingerprints and preserve the existing HTML cache-header behavior. No SQLite/API changes.
|
||||
|
||||
Generated
+1
-1
@@ -1881,7 +1881,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rust-proxy"
|
||||
version = "0.31.5"
|
||||
version = "0.31.6"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rust-proxy"
|
||||
version = "0.31.5"
|
||||
version = "0.31.6"
|
||||
edition = "2021"
|
||||
rust-version = "1.99"
|
||||
description = "RustProxy - independent reverse proxy with local administration"
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
# RustProxy 0.31.5
|
||||
# RustProxy 0.31.6
|
||||
|
||||
**0.31.0 panel/cache additions:** browser-negotiated Zstd/Brotli/gzip for HTML, JS and CSS (Settings → Administration); optional HTTP BAN/PURGE restricted to explicit IP/CIDR allowlist (Settings → Cache, disabled by default); System information Refresh button beside the API badge. See [Settings](docs/SETTINGS.md) and [Cache](docs/CACHE.md).
|
||||
|
||||
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.31.5 User-Agent blocking and operator log-analysis workflow (Cargo verification required).**
|
||||
**0.31.6 HTTP/3 mobile full-navigation stability and diagnostics (Cargo/live verification required).**
|
||||
|
||||
## Layer-4 streams
|
||||
|
||||
@@ -13,13 +13,25 @@ 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.31.5
|
||||
## What changed in 0.31.6
|
||||
|
||||
- Make HTTP/3 `GET`/`HEAD` forwarding to HTTP/1.1 origins explicitly recognize a finished, empty H3 receive stream before upstream forwarding. Bounded probes at the cache/404 preflight and H1 forwarding stages preserve nonempty bodies and let slow peers continue streaming without blocking the document navigation. Hyper already special-cases H1 GET/HEAD framing; this is a receive-stream lifecycle fix, not a claim that GET was always chunked. It targets full reload via the original logo `<a href="/">` without changing its action or other panel navigation.
|
||||
- Keep in-flight H3 response tasks draining after an H3 accept-loop failure rather than immediately resetting every active stream.
|
||||
- Record peer cancellations with HTTP status **0** (not a synthetic HTTP 400) and emit diagnostic phases, H3 stream ID, method, sanitized path without query, selected response status and bytes already sent on interrupted H3 responses. These error events are in per-vhost file logs when logging is enabled; an H3 `GET /` also produces INFO/WARN navigation breadcrumbs in the service log to identify the *new* request. Do not confuse old-stream cancellations with failed new navigations.
|
||||
- Retain the 0.31.5 mobile `/logs` layout, cache protocol toggle fix and trusted panel-Origin behavior. SQLite schema 9 and Cold Cache format 3 are unchanged.
|
||||
- Rust/Cargo and actual iPhone+QUIC acceptance remain mandatory. See [0.31.6 implementation](docs/IMPLEMENTATION_0.31.6.md) and [test report](docs/TEST_REPORT.md).
|
||||
|
||||
## Previously in 0.31.5
|
||||
|
||||
- Add bounded **User-Agent blocking** globally under Settings → Security and per vhost under Vhosts → Security. Matching is case-insensitive; plain text matches a substring and `*` is a wildcard. Global and per-vhost rules are cumulative and run before opening an upstream.
|
||||
- Make Traffic table/chart values directly copyable, including User-Agent, IP, host/path and other displayed dimensions, to speed up blocklist creation and investigation.
|
||||
- Add **Advanced parser · repeated values** in Stored Logs. It counts repeated values for `user_agent`, `client_ip`, `host`, `path` or any supported parsed field across the bounded matching scan before pagination.
|
||||
- Add a default-enabled **Consolidate rotated logs per vhost** switch. Active `vhost.jsonl` and matching `vhost.jsonl.*.gz` rotations are queried as one logical vhost source while retaining individual source IDs internally.
|
||||
- Preserve SQLite schema 9 and existing saved configurations through serde defaults. See [Stored Logs](docs/FILE_LOGS.md) and [0.31.5 implementation and tests](docs/IMPLEMENTATION_0.31.5.md).
|
||||
- Make `/logs` usable on phones with expandable log cards, full parsed-field details and no global horizontal scrolling; dynamic columns remain available.
|
||||
- Treat H3 peer-cancelled pre-header requests (e.g. F5 navigation) as stream cancellations, not fabricated HTTP 400 results. This corrects error reporting and avoids redundant internal response resets; it does not yet prove the reported mobile white-screen symptom resolved.
|
||||
- Keep both memory and cold cache across per-vhost H2/H3 switches; changing frontend protocols alone does not invalidate cached representations.
|
||||
- Keep optional Administration **public panel HTTPS origin** for the trusted-proxy Origin/CSRF security context; **additional DNS hostnames** are a separate Host allowlist, not its replacement.
|
||||
- **Release gate remains open:** Rust/Cargo compilation and real proxy/browser acceptance are still required before production deployment.
|
||||
|
||||
## Previously in 0.31.4
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# RustProxy roadmap
|
||||
|
||||
## 0.31.6 - H3 iOS navigation hardening (runtime acceptance pending)
|
||||
|
||||
- Finish live Safari/Chrome iPhone logo-navigation and repeated-refresh acceptance over H3 after Rust compilation.
|
||||
- Collect matching `http3_*` diagnostic records and navigation/asset response records; verify the new top-level GET is not interrupted when old SSE/API streams cancel.
|
||||
|
||||
## 0.31.5 - User-Agent blocking and consolidated log analysis
|
||||
|
||||
- [x] Add bounded global and per-vhost User-Agent blocklists and enforce them before upstream contact.
|
||||
|
||||
+42
-35
@@ -1,18 +1,18 @@
|
||||
61bf40a0572889dc8b0dda9eae4e93e44b0de9364b3c798106c85700ffb37c23 .dockerignore
|
||||
4140a2956264b44f259e6a37fc00ba02097bbe68b09296e04d3d243c1287129b .env.example
|
||||
4a19a62f235c7f520b68411c32dc4930a8aa58c3d91d4a12fd83b8b9344db84d .gitignore
|
||||
095ff293a2250958aa9877e5f6b299018b412fc26556a355e72c3f782244279f AGENTS.md
|
||||
bc7ed26f595df6df1f037a43ea3f8eeb01dd7c4ddcd068950d54b8b69af6d985 CHANGELOG.md
|
||||
9b943bc748d57182cb96b084f5d2e54d516310e37f5d0004607f4df24b398c14 Cargo.lock
|
||||
20fe56f1a563ef54179f0050bb1a708dee595f8763261c953965bb3f95277f5b Cargo.toml
|
||||
9e6356475bf587091bab72246ec9e325b498f7e48a6220e9a18baa50d0a385a6 .gitignore
|
||||
715de2d377038368b11ba94b94930ef349b5c8b7e87454d9e80afae9b8fbe6e3 AGENTS.md
|
||||
8550600e24446331fd4c2fbbd30a69d38b3ebddbe8f251c9e191841483cda9b2 CHANGELOG.md
|
||||
b76929e915c20fb14013fb221a10b9a285ff19b9f95473a32c286f59a6d6ab9b Cargo.lock
|
||||
d3407b114f32880318c1661de9d05f9f2761451fe270cfdc45c883e4f59fa7dd Cargo.toml
|
||||
25c7f3c8162cbf5cc156439774118a81dae9fdc52b611ec7e40c230fc99a772c Dockerfile
|
||||
fac9ad52b9cb6f506988f6be8de88a2a2e2f288319f2f01b7849a8e053d32359 README.md
|
||||
aa94ea36f16a56ace163895b1a5fb01ba9a45ad44432cf45a2d884b89436df2e ROADMAP.md
|
||||
cbb080c2fbbd543385351c44b0c53153df423334811aac8f3be93da5182c3ac8 README.md
|
||||
1a85c502721492a0a2d653af9f18bb51bd0f149a0ce5813d176231cac972d90b ROADMAP.md
|
||||
da8311e3db945290666324a2f24bf4521b365272d7ac3f862ded1be993eea646 UPDATE_CHECK.toml
|
||||
f645f218dd062bf6cb54a8ee634290562cad364a4cccf09f16e3c8303582af8c build.rs
|
||||
ea8d091843f86f4c66a5c76b20268b2feb397070851b6e05c5bc7d7a8d9157eb deploy/README.md
|
||||
4834c278430ddf3bf842995735195368f1ac2c3b5c1d749df4f9238caf1e2b25 deploy/rustproxy.service
|
||||
b395a672fff045c1700858c2af21ea75aadce41877b0126271a6977cd3c00e7c dev.sh
|
||||
1a74775a1d98b21deb040ceeca8f7555a2839b35e5cce3a92a37749a22896adc dev.sh
|
||||
5f51efe621f45512a466fa7f2656867c6502a135ca040f4bfeec06a6919b39b6 docker-agent/README.md
|
||||
7fee7d51a7deb67e73e53a0bdff3aa4630aea9bab9b23a0f9b8001b2169892b1 docker-agent/certificate.go
|
||||
5e4f565c3c9de4dba897af2e801ea646a83395e72eae4717a6d502dff2fcec10 docker-agent/docker-agent.env.example
|
||||
@@ -37,13 +37,14 @@ c8e69e492179ce39958cc63615cf46c4ac08cfdb7f3483aaab66868a3abcd687 docs/CONTAINER
|
||||
befa50579e3475b22589f26380d85b3345b80e4fa947d49649a3b91a8dd8b8c2 docs/CUSTOM_LOCATIONS.md
|
||||
16d8f59908c66e595d6003495cbf5c068c68b10aa9495eb8612c38b2de0e4a7c docs/DOCKER_DISCOVERY.md
|
||||
5324b215075214a5bb9b388bc82d6491eacfc507df95856d1aecedd184e25b80 docs/ERROR_LOGGING.md
|
||||
ac855e78d546b5abe3215b93c0510050044b98c7ef593ac2405614999b2493cb docs/FILE_LOGS.md
|
||||
8dfc89a8b3512103b87864bd7c24a3dc3de4465edf8f9874e6bed1f4634dd930 docs/HTTP3.md
|
||||
74dfbc0d67241d3ec47f17850e69cb8348c10e55e53bb26176b715f36d01883e docs/FILE_LOGS.md
|
||||
36599e0b59164e662442f3e8203c16b3123d09b7684d8ecdbffce42d22cc221b docs/HTTP3.md
|
||||
5301e321611d494fd97b04ab648df1f66a6a0d1f1f4c3475eab485e138d5605b docs/IMPLEMENTATION_0.31.0.md
|
||||
eb52e72325e1307d05cb04878b6d22dfedea491908c5f36110b32421765096c8 docs/IMPLEMENTATION_0.31.1.md
|
||||
991ff3c09ca1e2bcf73b8fa2975405b955dd640c3f1e1246e75038c043c17183 docs/IMPLEMENTATION_0.31.3.md
|
||||
db2087608a1225c56e42c416d949b777fb15b1ed8b69a1e71fc2a6af6750a1e5 docs/IMPLEMENTATION_0.31.4.md
|
||||
ab2633deb3ad3c6bad056970363d3227213f919915a2298d9549e8029b5e0c78 docs/IMPLEMENTATION_0.31.5.md
|
||||
684471c3d40bf832d460d42506adbba3e4a128540463738a575eff3ebcf22001 docs/IMPLEMENTATION_0.31.6.md
|
||||
a00ec3c1d3b28ba10677435128e031e5f18fe8c5cef2ac90eb0f6779cefeb434 docs/INSTALL.md
|
||||
3b5c31c805e5aee0dc2100b81246a9b631b5abb996faa598ae94f87ba39305f3 docs/INTEGRATIONS_040.md
|
||||
4bb56add544f15d54695cf797cd16a51c09024360cdeef803a88988f4a54b3a5 docs/LIVE_LOGS.md
|
||||
@@ -57,7 +58,7 @@ e7516646dfb107d6240370f4e7460b1189f2eb21f398740381d5d102847bbfe6 docs/NOT_FOUND
|
||||
e68d5a5cf0991de9308f78afb0264e02979f7463a5f395eb7a6e9ec2bd3a3d54 docs/PHP_FPM.md
|
||||
bc068a176056f5ea4d3b637071e644ed58a2140494c233ef22aa66e9700e3299 docs/PHP_FPM_ISOLATION.md
|
||||
572c70fbd2f61fecd80a8ff9fa5de4a94c4f158de5e90da8e920f99b051241fa docs/PROMETHEUS.md
|
||||
4c6909970419f336735106d30af685010d63ab56b7721b794be219e62ab86060 docs/README.md
|
||||
4a074b8ad77f1bf286f966b973e115d1f48f02084d93df09fafeab670e15038b docs/README.md
|
||||
6e46a137b9c88dac8cb9e3fdca877430092392de9913d746278dbff8652460c2 docs/REDIRECTS.md
|
||||
0dfd0d431c5aedc67abac1ce5de040ffa790ec9578fd7d6f91a987c333d73358 docs/REFERENCES.md
|
||||
d85c5567fc6a7714b96ad7cfe4ee9ff2085f84c4f31534a6484340d03a73e0f7 docs/RENEWAL.md
|
||||
@@ -68,7 +69,7 @@ ec7e32fd19e6ba6ed1732f1f3dd92ac0860609e8c0bbd3c1998840cdfb24bf54 docs/SECURITY.
|
||||
007537ee971463072b2482dfa4fdcf1955f837ced0b711d4eef3b6267d16d4f6 docs/SECURITY_AUDIT_0.31.0.md
|
||||
970025a6d1a37452b8bead6c9d2b3d4743ea602251dc446d575531d76c11ae83 docs/SETTINGS.md
|
||||
3611defd206cde107e75cac006159856d12cd8f15df7ae2eb9ec3592ed2b4abd docs/STREAMS.md
|
||||
686377f81a4169e08599d50f9decfb18a4a084ce4d808aa77357deb8824cf2ba docs/TEST_REPORT.md
|
||||
07ea27c13cda6af001deb53a59330041bfbe11b47ab1a4fb186e97cffb941cf6 docs/TEST_REPORT.md
|
||||
cfb452d8496839e832b5fbacdd4e52525b0ecc3112fbb62340c1e580bd5c1717 docs/TODO.md
|
||||
751c9b13b47d0959a8575d6e82bdecba988b950a374ba0b084fbba08b4b65ebe docs/TRANSFORM.md
|
||||
660687fc961f71aae2bdf71595aa47a336fa782f4e78ceee8c1d1e63e46393ee docs/TRANSPORT.md
|
||||
@@ -140,8 +141,8 @@ cb01fdef286f38b442dd9252c23c4f8e44eb72e9ecdd99adb926e8e9f6318bb1 integrations/r
|
||||
96563a83e87cfb33e238c2a970ce36b1a1b51e91318d09b123c3755002ba2646 integrations/routeros-oci-build/build-routeros-oci.sh
|
||||
3f267cafa16006e5456ade78ee2d5fae02cc7ce747839523068ffd8d7c98798a integrations/routeros-oci-build/docker-entrypoint.sh
|
||||
71cde48daa538431dfac51e438b1ed23b061abfdb4e144ba704237d75ac6270c langs/README.md
|
||||
e0451bcd6dc17d1c0bd5becb0a5a35617df21d2f851982a64831e9f62c13bc17 langs/en_US.json
|
||||
10967c073e750d9f66f60bd0e2e89dbfb77bb4c5156ee694ffe6e86a7b76ccfa langs/pl_pl.json
|
||||
17796036957fe2afe593481d479460421a000d3c69038cfffe945149c53f1e21 langs/en_US.json
|
||||
b6adc6c6e462f41652ea4974694fe5e9b5e972922e95e81968dcf552fcf1d029 langs/pl_pl.json
|
||||
a99ad040be10fa028351e290b9363236b3513a1cadbdc9fb9e0260d517b9b0ae make_zip.py
|
||||
e830d37dd1f81a4b0aa7d535f5bba69e34e433b0fd99cc8ee0b296786c32379a migrations/0002_admin.sql
|
||||
20112cc82192098b8d602f1c4f7ca08712e9e1366c845fabb92753187c370d35 migrations/0003_certificate_renewals.sql
|
||||
@@ -167,7 +168,7 @@ a641bd35400c56f3b68b0a92e97b602d06090f03972ca8f01fa744e90dd97403 scripts/deploy
|
||||
2cc658faaa610b21b48942c59b955525539ad4503fd60f2a49c7e626a22faaff scripts/migration/migrate_npm.py
|
||||
a459d95bbb0076f33ade8b273b813308167f9d8d45242b309a2d18320ca0093c scripts/migration/migrate_npm_api.py
|
||||
31ccad3ec3c880119f3b7002f0c6652f8b43634edad8ab3763cb0e4497d0bb1f scripts/migration/migrate_npm_core.py
|
||||
9afb0d704e1770c31913e968014adc35dbfdeb40906235b7868f22a4562e3b71 scripts/resolve_dependencies_0312.sh
|
||||
7c9f1476c7f786c702c5d9c1693ad230b0900a67b6b3b3947f77b2c52f43eca1 scripts/resolve_dependencies_0312.sh
|
||||
20124173452b4514bad8850c1637569a9a191882478451e549bd16b432605fc1 scripts/runtime/acme_provider.py
|
||||
e69813fb1d3a4cdcfb7243964cc323fc21a603087bf2e84188fa42b9d3cee4b5 scripts/runtime/acme_rfc2136.py
|
||||
22e880e98c7b9d700c7a4c02b9b1d543e5dbd1f4ecdca42813a1083165b32a19 scripts/version.py
|
||||
@@ -185,7 +186,7 @@ e38331a1b186b49fe9d04051de52b603e1fa028def37e4e2f52e386b4d7b29ac src/acme/tests
|
||||
b6f38f3fd75d6685bbc882ed1077a38bf495cfcf01c397f69ed9b98be68b446f src/admin/access.rs
|
||||
019f4aa33e3596bc9c11ef8cf5703bcfffe46c15288c0eba83c8dd9e38055fb2 src/admin/certificates.rs
|
||||
5e150d4ef0413a2f92db5b6631669933b84d4b4796633d01476c4b39188cd05c src/admin/documentation.rs
|
||||
bc782a5f5908d82c5dc509ca572d0c38c87201726769b1903bc15a78fc8dd7b8 src/admin/documentation/documents.rs
|
||||
174a84114f2c8b14b7568a1467d5b7edcc0c82e8589fd3ae331113bfdaf41a41 src/admin/documentation/documents.rs
|
||||
67b901485d5858d5b5bf78a6260035db32241531d99481754c3533fdf404facb src/admin/documentation/tests.rs
|
||||
1ce779b84aa8f47bdb3c34f83593c1f2f06d2235f0319847096cfcd035434168 src/admin/file_log_search.rs
|
||||
b3aad0db47d930ee177253cb5d5eb772272452bab4385a34566b111c239a6302 src/admin/file_logs.rs
|
||||
@@ -193,7 +194,7 @@ c6b552cd5acd9e743c28b1a5564420c214c619fe891ab0561b7d4499aae8d6c0 src/admin/file
|
||||
d5eb4019a4aa6a4fdcad9310dce9076dcb85ca4640540c64e924367ba2276234 src/admin/file_logs_query.rs
|
||||
7de7da489a367658b700f00776fa43604d3554500b5fbef4da2764e402e68afa src/admin/logging_api.rs
|
||||
58802528128f53e493f7339063433a747b605007a8842b5efc670132d897d6f7 src/admin/mod.rs
|
||||
9c65356d79438aabf0adcee6c6b1f0c4236425af205430e06a12685c1be40eb5 src/admin/openapi.json
|
||||
261978723c4ad0b0731cf71ca688add07d7b59af5a0c1e6adcccbc92c198fe7f src/admin/openapi.json
|
||||
0d1a5324e8ad32f5ab0861b722663f50f066e256bc5fd5dcced6a63f2bd399d1 src/admin/operations.rs
|
||||
30895e05c5d5d0a26be29f667e0f6d72f16a96a5c8a98e09cd11d8ef2d7b4763 src/admin/panel_assets.rs
|
||||
75bbedd54fdabb2b489642a642ee7c6533f69cdacb85e251763a56ba11174d5c src/admin/panel_content.rs
|
||||
@@ -222,7 +223,7 @@ bffa6611f3776e97d0065b510b63f083aabf7b3429fcc5cb26b1883fdb7c47dd src/admin/test
|
||||
578f93cc5771b5858813945e5608b15c572bd3d7fd15bae291d292e2994ece25 src/admin/tests/part_11.rs
|
||||
48fcd818c522fe80f2c7b2e218f87de6a7772ffd0a24e93502780b03ec78e335 src/admin/web/acme.js
|
||||
446011c2c7afddf7734ceb268c75a3b4146dbe8e915b11986495e956b6c62c69 src/admin/web/app/config-history.js
|
||||
7374ba5ee2c9e09677369542abec3a5b3d450df13aec705b0d8796a083e9d974 src/admin/web/app/core.js
|
||||
a42640573b29bf0d13f87b3336d3c86e50cd7f8af2c453836edfda23628fba97 src/admin/web/app/core.js
|
||||
23721bfbac850c4f77b116cc32bb9d2db6361ac4dd7e310a8c8070ab906997f6 src/admin/web/app/preferences.js
|
||||
729ed152eb8471c67fb0e55e44fe0669c2708802ce83228df9cb4e53c14cee02 src/admin/web/app/refresh.js
|
||||
984d092961bfaaa19f982e54c7d4be474e8417b208d814ac88bda63f1ab19f4c src/admin/web/app/runtime.js
|
||||
@@ -242,9 +243,9 @@ f7d0955ab18c15e04369204e789f341d2e832f927c1ff20a3b2b9e35e7898f00 src/admin/web/
|
||||
386b9dc3aecebe7727f7cf7bbd2a02ab036a3abf01315975111a51559128df73 src/admin/web/error_503.html
|
||||
e6c4d91d1542d0a5b47bb8736f9539d43a6713df5a473b635519d5b7eac2eb9c src/admin/web/error_504.html
|
||||
0a5fff35555153f59d4346933030a67b6e1a8768cee7a47cfa2fcf1a2c564f63 src/admin/web/favicon.svg
|
||||
4645af5dda304f839cd3af79bd5c52001f3092a5545aeafd7b37c2499fed24b7 src/admin/web/file-logs.js
|
||||
bc9e11724536de89bced1f0a752974068a0229ee14c3488592a27fc58261aaed src/admin/web/file-logs.js
|
||||
b14e7b3578322f309627f6fe965cc16abcf85da3437c9070be6caecae1908743 src/admin/web/host404.js
|
||||
c3178df6f9bb4bbb187e6d350ceda3dfa9db425b566aa27af75aa7945b1433a4 src/admin/web/index.html
|
||||
5d0f813af08bb125f201b7d000f5fcb7372c0567289d3a02940ba4d0d51820d0 src/admin/web/index.html
|
||||
24db88842707f764064657712d3660c6ac129fbfb47d188c0903925cf8244ce5 src/admin/web/integrations.js
|
||||
7a12f020fc587205694978ab41b660e9a80487d413fbf6e34fc525eabc58308f src/admin/web/login-keys.json
|
||||
cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/login.html
|
||||
@@ -253,7 +254,7 @@ cbd41678eadd994dc5252628ee8ef9551fa9f7d8694ca5abd206240d9a96ad30 src/admin/web/
|
||||
90d9a0836662135b79fb6f183c4f3eff60f8a64392d6c5795ccf1d8ef6862263 src/admin/web/redirects.js
|
||||
8fcad0d1a161ac6f45d773d1dc3925db2d32b34bdbe57595924f84eacb4f9136 src/admin/web/settings.js
|
||||
bb67e77af5514478485cfb69f339d63d7eb0cbadd093fa054814a416260ccc1d src/admin/web/streams.js
|
||||
98bb49a71b43ab0f843acca1d841ef1f53c63dfd1adb53d137c2564eaea58a65 src/admin/web/style.css
|
||||
09093fe70301df47fd3aaa0fc78ce1c44b256d86ae56a7732c2d8ddf9499393d src/admin/web/style.css
|
||||
d290c048450655b3eeae7c6ec54a82d645d1965513694ab29fdf7eef796a1abd src/admin/web/traffic-panel.js
|
||||
a6e7bbff4fd2d50ecf759ed28161a672d7b483af2b35c9f912746a7a06a9f476 src/admin/web/transform-panel.js
|
||||
57a2b975716bdab32c33ec1bf9427b25998550e81ce191440ee66ffb7e2a249e src/admin/web/vhosts/core.js
|
||||
@@ -365,9 +366,12 @@ c1b49bef28d87712e4c46138e70b52c4bcfb1a00196579031fac043d0b4aaa7b src/geoip/upda
|
||||
9a912dad0b8d1e10410fd95d0da3684656092ef85cf8b23eda4ffe7fb92921d8 src/geoip/update_tests.rs
|
||||
13269be4ad8d3e4c91893753f654c3022e6f86d171c5d1ff21eda2d9185ec61f src/headers.rs
|
||||
fbb6859780309f8cca1aeaba43fa67d13a1edf6f5a60c5c982d2067b71fb0846 src/headers/tests.rs
|
||||
d0cd8589321595309834690fa9ddb98488886bc98f34ea45aa0ccb3035412fb5 src/http3.rs
|
||||
2165a0a4b27d621985e2030326ad52b8bbd59d7b9e4f092f72f7e92b4e58b497 src/http3/body.rs
|
||||
68bc11160bc5275a904e48af8698b37433096fb6d592364d8281d59bb9e2b3f4 src/http3/tests.rs
|
||||
9e21edc67a14ef8a2ce3dda0ed18784d0fd0be305973e5d216058b7ac802c6cb src/http3.rs
|
||||
eb57117938a447d9a6c2ec1806ebd9fecc640a68c88950a8b35fa9bad14d0dc6 src/http3/body.rs
|
||||
ed25da264a2d25b40c4fcddc97c626a3a43d4f3a9be6c6d31af58b9e4925c198 src/http3/diagnostics.rs
|
||||
37f54961d8f52b79a75fb2be31e81f4fdb1e66b06350345c4483d6197b8a0bbc src/http3/identity.rs
|
||||
f2a8c2a98f4ccfb708f9d75a48ec7b351232be9b8a4364b7e63ba14d28eead7c src/http3/settings.rs
|
||||
a31db7eac12b578b1eb5a74fae98bbe36312da95b7d7173135dbc41fc454da21 src/http3/tests.rs
|
||||
68fb5ab6a889995a66ad12cb6acf88c5d04f0aca572e2ae15f1a9cd171a5f24b src/lib.rs
|
||||
2551189a3e5701371026c9abd2c5d44d37495f28fb67e220180421bfd315e3dd src/logging.rs
|
||||
3849a09a7d7402dc4d867b646cda153c574327ddd3c707578ddb4dc6f95b1563 src/logging/error_runtime.rs
|
||||
@@ -400,7 +404,7 @@ e03f0d6b3f35dae6b98c394837aa0d17e4d73a8306767b4c607868bc00c30286 src/prometheus
|
||||
59095a04707c2e4714358f4462dcf21c41952474b9b815c42bbd14e96c573846 src/prometheus/web_php.rs
|
||||
b5856fd38a957b2ca9c7e3606d84d13d5cfcf3a45b487b15ae1d2dc248cbda57 src/proxy/body.rs
|
||||
16b9d4db3b20b2ab14c47ac14fcd18d3d8ca77c3ad9a1f37866ebfa60f379862 src/proxy/body/tests.rs
|
||||
d3126b44f0a32d60421bb5afe8e83adf4a253faf7dc77f500bce8fe5d78b9d74 src/proxy/cache_helpers.rs
|
||||
3d2c8462fd259c299ed26493cb2316d0b7349bd32fedc9b0c11e77b21d6faefc src/proxy/cache_helpers.rs
|
||||
aa49c9928bb91e3d5bae0d340d88396a0417b9edec6192d41e1403533319a897 src/proxy/cache_http_invalidation.rs
|
||||
771cc1a38927f96e593b144dbab66cd64a5dc0ed42d54b1615c359845b2b7223 src/proxy/error_fallback.rs
|
||||
bf2e6bbd64fb45fceed54af0bff1c1e00da22b79c64dd8f5c23d2af0dbab0b7d src/proxy/forward/cache_lookup.rs
|
||||
@@ -413,9 +417,10 @@ cea9e47ecb31b0e4c0bbe0eb83e0e53b5e7bdcb9930107b67a8c34eef01a3f49 src/proxy/forw
|
||||
69cb26cc475cc6fdee235df78227543b7c5e591b7cdf6cb7a2ff1024beffc845 src/proxy/forward_entry.rs
|
||||
c438066616527a3108f2ad53e3698ae9b2a80961714394cdd0a450c92f99d992 src/proxy/headers.rs
|
||||
5cb7522ceb9a9f083a15596ac4898559295e38d1138c80099577211de8a868ec src/proxy/headers/tests.rs
|
||||
42155c7742da4173f71dc4ef488288e1fd665fbb295f02745421785571926295 src/proxy/mod.rs
|
||||
632ee37457d9bf1dbcd3211afdf489f4fc9faf018007d5c1036169436e8e5dea src/proxy/mod.rs
|
||||
55133ceace2e5262d9c54fa41bea099a27b817d1e09c1cd0e2493fd9cee4da38 src/proxy/redirect_helpers.rs
|
||||
cc682f0d5754e0b806a4eb5950e4cc00611aca5ce4668414fddef1d692489805 src/proxy/tests/authority_tests.rs
|
||||
0f2eacadf1447db8aa1c1a78005aa262e0f747096bfe7e0a0e41a2cd31a0d17f src/proxy/tests/h3_navigation_tests.rs
|
||||
d134b0eb617f82f9971b3f3461379ea37c57bd0079bf54748d0c434b2ef1b475 src/proxy/tests/http_invalidation_tests.rs
|
||||
e80c7fe44abb7607c64ea02d56542753dc25290b3bcf4c4d45b75f2db6ca90d0 src/proxy/types.rs
|
||||
9999f717492755d62fc05cd5c1aff802ff0da901fe3e6b928bd8a000833c8799 src/proxy/upstream_helpers.rs
|
||||
@@ -432,8 +437,8 @@ d0939e1b0829c25356a0b67ecb3a178c74936dd07baa15fee24eb32544db6cad src/runtime.rs
|
||||
4e0b25c2b86eefddd76c64b39dc1c1cd6d243addc32cdea99e6c3ac84ed879a6 src/runtime/certificates.rs
|
||||
8d2443df8dab1ce6f11878e991b4928046b7362c417c5703f6d1f3a977646cbe src/runtime/constructors.rs
|
||||
31dc5cdea00efd63594dc6becd5a9103594696145cbc8e7b7594fb407214ad9d src/runtime/docker.rs
|
||||
e90df3fff8a25465425b099ae358d19085809e56d948ba47e01e53f8b93d5dd0 src/runtime/tests.rs
|
||||
0046525317ffbd5d3b34845656b0f58d9d31f38b3d8acd71e90d94bd7cdfb8ab src/runtime/types.rs
|
||||
6b2b57888863a396461fff202bfdaa6d8e869b2628cc171c0823f31f388e5178 src/runtime/tests.rs
|
||||
4036b48925f5b25de70e21dfbb278488c20b39248dcda4cad4d19d5d7aed2b8d src/runtime/types.rs
|
||||
88bc546fdda20d3f1f8f5e7d7872ede1fa334b8bb5102c729bd72f56e3fb94b9 src/runtime/updates.rs
|
||||
208a378b14ca80e1412ffa19ef1b140594182971e2e797ee0bb95bcdb8404da7 src/secret_store.rs
|
||||
6dca8da072a3bd610704a391bb6e25c9042971f4ebc60edb47665ce1d74cbc58 src/secret_store/tests.rs
|
||||
@@ -535,27 +540,29 @@ e5c58fc9b7685ec911bfa86f6ca4f0cc97c19a90c0cff5b60ea9887f1afe9ced tools/checks/a
|
||||
fd082812411cff7afb71b2289a6f1a469b8d3c8a7cfdcdd9e567a2a4baf81be4 tools/checks/admin_surface_audit_0310.py
|
||||
412255d383fcc8306c6086784fb416d9814e3ec790e440a0927431ca615f3411 tools/checks/backend_cache_0297_ui_test.mjs
|
||||
2a493dbce2991a23d807c01952f58608a364f541560836b4cb385e75e108e9e7 tools/checks/backend_health_0300_ui_test.mjs
|
||||
95b2024d6354aae1c40203c6cb3f557062baa184e15a0a01af19495b3899cf6c tools/checks/check_source.py
|
||||
821736b16c13ce9a86b95fe91909c985c61fde5e382299a9b70183400b1d465e tools/checks/check_source.py
|
||||
dc4df51fc15d4b6345c6625890ddbe014103851ebfa16abc583fbb0d809f4015 tools/checks/dashboard_boot_0311_ui_test.mjs
|
||||
259d4b0985c82b631e151a07e8ef3b4c760019cb99bb2af458ce577497f7df58 tools/checks/dashboard_startup_0310_ui_test.mjs
|
||||
3b66874913b429f3f45a52642495829849d25e7f40c5304555e9971f32b25fae tools/checks/dependency_upgrade_0312.py
|
||||
da49215969f31c0c2194d84dd178e101be5937b56c2f2a030a19df271aca436c tools/checks/docs_check.py
|
||||
ce506b3c5e0a01552000fc2e0c841a359ff97956fcc0cbbc772f90dd0fce7999 tools/checks/dependency_upgrade_0312.py
|
||||
1a4bba46b9ebc052b76fb356c3e1437c8d028d1f18b83994520a39d12de555e6 tools/checks/docs_check.py
|
||||
6ca66c0998d1da852ad66976c91e4d7d3628195aa49ddb44cea95f531eb50033 tools/checks/file_logs_tabs_0315_ui_test.mjs
|
||||
83c74f0027a6d2f51f637d552593f9904e60ccd69665f6460136c48b159afb60 tools/checks/h3_cache_mobile_0315_test.mjs
|
||||
33338a4778a32be16b8518ead7f642c6dc57206e3f671852ef52378c5814d9b0 tools/checks/h3_navigation_0316.py
|
||||
e33a9cc51d58bc92a637c0e6a1c88cbd24ba4605be55748212e296c3c35a4060 tools/checks/high_traffic_memory_model.py
|
||||
4f7821e7e25b09ce251d25a0b3fb4d8ce8396c36a5ed971c2637aeb325425909 tools/checks/langs_check.py
|
||||
f8f87ba891338b565249d02c3cf5f02cec44e606b4205de99fcbea1eb2c24d57 tools/checks/memory_contracts.py
|
||||
af98579ee942e774d88775c261ac351847c28bb4a5396dc67e458bd6b1ff7963 tools/checks/origin_resume_0313_ui_test.mjs
|
||||
1ac812929c2a8c0699ae3bdebd0d7e05b5bcaf86c87533bb669b5ba7955e3245 tools/checks/panel_cache_http_0310_ui_test.mjs
|
||||
4cdee49186bdfbde5d10f0a0bfe9577f4c7dc1fd4b735d55c55703dee01c87da tools/checks/preflight.py
|
||||
b94a388135b8986730ca5c95c03445a1cd5dcdc9e787e3b53f9188e3e41aedd6 tools/checks/preflight.py
|
||||
021a97bd6763b59f3f7eca15f51efdf88d0c863f1f81a2e42ba80a9a09ca3fec tools/checks/refresh_0310_ui_test.mjs
|
||||
75aee85614ad8c8e7e3c4d843197c20ddfa975bf96208d39356532d66f8ad966 tools/checks/release_smoke.py
|
||||
811d926d5a78c7fcc83325a7de9c89acc9437857faf9ba374d0f077c0fd55bc5 tools/checks/release_smoke.py
|
||||
f69845bc26ec33160fc756953991cf32aa8ad6a7fb4a131caa2093db91e783ef tools/checks/rust_module_contracts.py
|
||||
ecfe0bb21a7549225a19b0846bb6de8c99cbfbbf5a706d33cf84db2c7146abed tools/checks/rust_source.py
|
||||
adf84a983e0dbba228e47d71ec2d5c9964f7ca9989d5139de58f96ee961b035a tools/checks/security_contracts.py
|
||||
23b9250245e6bec5ef728c73cee4c88f64924afc3597f8a9b58a1924235df98e tools/checks/security_dependency_contracts.py
|
||||
f4d9a92f25c40b879247750c1ef2764419368afb79bfe74414c0259cbb1330eb tools/checks/security_logs_0315_ui_test.mjs
|
||||
598c390550357d89534b4fe882815bbe052280f0b870626c281d3500c6a6462c tools/checks/security_logs_0315_ui_test.mjs
|
||||
f7fe205ee0cfe0347f73c10d626aa1dadda6a708bf8a46c25fc90075eeec968a tools/checks/source_module_contracts.py
|
||||
730c47345deff507044f6989806e9fab9945edaee7dcb75555e2d51a33d6d34a tools/checks/system_info_0301_ui_test.mjs
|
||||
19574fb1d7c786402eb864e723fdc2f86a9575841a2f41c3a06cc43460aaa6a3 tools/checks/system_info_0301_ui_test.mjs
|
||||
543a488951ea6ec8fe06e04f168ccc4d4f2af3cfc281c9a728ab7bd87377fa98 tools/checks/system_info_no_subprocess_contracts.py
|
||||
a5105ab53de67e6b1114974c92b019014e1aa93acc445bc5dd6c82af0b0d3a16 tools/checks/version_contracts.py
|
||||
161637cb5e2e613e3c71cf8489cc936fa84913a2a02649463f8893a28a674242 tools/checks/vhost_origin_errors_0297_ui_test.mjs
|
||||
|
||||
@@ -90,6 +90,7 @@ python3 tools/checks/preflight.py --repair-known
|
||||
if [[ "$mode" == "repair" ]]; then printf 'Source preflight: OK\n'; exit 0; fi
|
||||
if [[ "$mode" == "test" ]]; then
|
||||
python3 tools/checks/rust_module_contracts.py
|
||||
python3 tools/checks/h3_navigation_0316.py
|
||||
python3 tools/checks/system_info_no_subprocess_contracts.py
|
||||
python3 tools/checks/source_module_contracts.py
|
||||
python3 tools/checks/version_contracts.py
|
||||
@@ -115,6 +116,7 @@ if [[ "$mode" == "test" ]]; then
|
||||
node tools/checks/admin_public_origin_0314_config_sim.mjs
|
||||
node tools/checks/admin_panel_config_0314_ui_test.mjs
|
||||
node tools/checks/security_logs_0315_ui_test.mjs
|
||||
node tools/checks/h3_cache_mobile_0315_test.mjs
|
||||
node tools/checks/panel_cache_http_0310_ui_test.mjs
|
||||
node tools/checks/vhost_origin_errors_0297_ui_test.mjs
|
||||
node tools/checks/www_redirect_ui_test.mjs
|
||||
|
||||
Regular → Executable
File mode changed.
@@ -45,6 +45,17 @@ At least one column must remain visible. Stored-log column selection is saved pe
|
||||
|
||||
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.
|
||||
|
||||
## Narrow-screen navigation (0.31.5)
|
||||
|
||||
At viewport widths up to 700px, `/logs` uses a vertically scrolling list of
|
||||
compact cards in place of a horizontally expanding results table. Each card
|
||||
shows every enabled column, with long values wrapped or shortened until
|
||||
expanded; tap the card (or focus and press Enter/Space) to inspect all parsed
|
||||
fields and the complete Raw JSON. Search, source, page size, column selection,
|
||||
pagination and repeated-value analysis stay available. Large raw text and
|
||||
aggregate tables scroll **inside their own containers**, never across the entire
|
||||
administration page. Tablet/desktop results keep the normal tabular layout.
|
||||
|
||||
## Repeated-value analysis
|
||||
|
||||
The **Repeated values** tab (advanced parser) can count repeated values for any parsed field returned by the explorer. Typical security-investigation fields are `user_agent`, `client_ip`, `host` and `path`. Counts are calculated across the bounded matching scan before pagination, so changing the page does not change the count set. Minimum repetitions and top-result count are configurable within server limits. Values in the result table are directly copyable.
|
||||
|
||||
+29
-1
@@ -7,7 +7,7 @@ conformance, interoperability or production performance until the acceptance tes
|
||||
## Stack and configuration
|
||||
|
||||
The adapter delegates QUIC transport/TLS to Quinn 0.11.12 and HTTP/3/QPACK plus the
|
||||
Quinn adapter to `hpx-h3 2.5.19` with its `quinn` feature. The crate is imported as `h3`
|
||||
Quinn adapter to `hpx-h3 2.5.20` with its `quinn` feature. The crate is imported as `h3`
|
||||
in source. These are Cargo dependencies, not bundled binaries.
|
||||
Rustls provides TLS 1.3 for QUIC; SNI certificates come from the existing inventory.
|
||||
HTTP/1.1 and optional HTTP/2 remain on TCP at the same port. HTTP/3 is on UDP.
|
||||
@@ -113,6 +113,34 @@ is no separate external-port override. Open UDP as well as TCP in your firewall/
|
||||
balancer. Scope labels do not perform those operations. Clients lacking QUIC or blocked
|
||||
UDP can still connect using TCP; no guarantee is made about browser protocol preference.
|
||||
|
||||
## H3 full-page navigation to HTTP/1.1 backend (0.31.6)
|
||||
|
||||
The RustProxy logo remains a normal `href="/"` link, not a client-side panel change. On an H3-enabled frontend it produces a new document GET through the proxy. When forwarding an H3 GET/HEAD to an HTTP/1.1 backend, RustProxy now probes for the stream's end for at most 35 ms per initial frame. A finished empty request is passed to H1 with a confirmed complete receive half rather than an unknown body state. Hyper already special-cases H1 GET/HEAD framing; no claim is made that ordinary GET requests were chunked. A nonempty frame and all subsequent bytes are replayed without buffering the whole request. The slow-peer case falls back to regular streaming.
|
||||
|
||||
If `h3.accept()` fails, existing request tasks get the normal bounded drain instead of immediate mass abort. The new diagnostics use status=0 for cancellations (not a real HTTP response), and persist interrupted-response details including `phase=`, `stream_id=`, `method=`, `path=`, `response_status=`, `body_bytes_sent=` and `error=` in the bounded, redacted `error_detail`. These are stored per vhost when file logging is enabled; the normal access logs remain available for successful document and asset GETs.
|
||||
|
||||
**iOS validation procedure:** keep H3 on, click the logo repeatedly on an authenticated panel behind the real RustProxy vhost, and collect timestamps for `http3_peer_cancelled_before_headers`, `http3_peer_cancelled_after_headers`, `http3_response_write_error`, `http3_accept_error`, and the GET `/` and asset access records. Collect browser Web Inspector Network/QUIC trace if a blank document persists. Correlate the **new GET/** status and transmitted bytes with older cancelled streams. Mere cancellation of an obsolete API or SSE stream is not proof that the new document failed. This implementation is not proven to resolve an iOS issue until live tests pass.
|
||||
|
||||
## Browser refresh and stream cancellation (0.31.5)
|
||||
|
||||
A browser may cancel an obsolete H3 stream when the user reloads or leaves a
|
||||
page, sometimes before its HEADERS arrive. RFC 9114 `H3_REQUEST_CANCELLED`
|
||||
(and a peer-originated `H3_NO_ERROR` reset) is not itself a server-side HTTP 400.
|
||||
RustProxy therefore omits that fabricated 400 access-log entry and avoids
|
||||
adding `H3_INTERNAL_ERROR` to a directly identified cancelled response stream.
|
||||
Malformed or forbidden headers still yield a protocol-error 400.
|
||||
|
||||
This correction does **not** establish the cause of a blank document after F5.
|
||||
If the new navigation fails, compare its actual document/asset statuses over H3
|
||||
and H2, the configured reverse-proxy Host/Origin/forwarded-proto chain, and any
|
||||
connection-level QUIC errors. An error record without HTTP method, path and
|
||||
request headers cannot identify the browser document which failed. Capture
|
||||
browser network traces and QUIC qlogs with timestamps for live acceptance.
|
||||
|
||||
Per-vhost `http2_enabled`/`http3_enabled` toggles also no longer rotate the global
|
||||
HOT/COLD cache identity. Origin representation policy and cache invalidation are
|
||||
unchanged; the frontend protocol remains part of live request routing.
|
||||
|
||||
## Verification gate
|
||||
|
||||
```bash
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# RustProxy 0.31.6 implementation and acceptance gate
|
||||
|
||||
## Scope
|
||||
|
||||
This patch is based on the delivered 0.31.5 source archive. It is a narrow HTTP/3 navigation follow-up. It does not change the logo link, normal panel SPA navigation, administrator security checks or the HTTP/3 listener controls. The report of iOS `21.0.1` is operator-provided device metadata and has not been independently verified.
|
||||
|
||||
## Code changes
|
||||
|
||||
1. `src/proxy/cache_helpers.rs`: the cache/404 empty-body preflight is now bounded to 35 ms per initial frame for H3 GET/HEAD. A delayed FIN bypasses cache rather than blocking the entire route; all frames remain available for streaming. Separately, the H3-to-H1 unknown-length request-body normalization now includes `GET` and `HEAD`. The first meaningful frame is inspected up to eight times, and GET/HEAD wait for at most 35 ms per frame. If end-of-stream is observed, the body becomes an exact, empty H1 request. Nonempty DATA and trailers are replayed via the existing `ReplayBody` wrapper. A slow stream is not discarded or buffered and stays on the existing streaming path. This makes the H3 receive half complete before H1 forwarding when FIN is already available, without relying on a claim that H1 GET/HEAD were chunked (Hyper already special-cases those methods).
|
||||
2. `src/http3.rs`: a failure of the H3 accept loop logs the event and stops accepting, but lets the existing bounded drain handle responses already in progress instead of aborting all response tasks immediately.
|
||||
3. `src/http3.rs`: pre-HEADERS peer stream cancellations are logged at HTTP status **0**, never fabricated HTTP 400. Response failures record the transport stage, H3 stream identifier, method, path without query, advertised response status, bytes successfully sent and bounded/redacted error in per-vhost protocol-error logs. Peer-initiated cancellation remains distinct from a true truncated/error response.
|
||||
4. A full HTTP/3 `GET /` emits `http3_navigation_started` and `http3_navigation_complete` at INFO in the process/service logs (or `http3_navigation_interrupted` at WARN) with SNI, connection endpoints, stream ID, response status and sent bytes. No query, Cookie, Authorization or body content is included. Per-vhost HTTP/3 protocol errors are logged separately in JSONL when enabled. Correlate a missing completion for the new stream rather than assuming any cancellation caused a blank screen.
|
||||
5. No change to the logo anchor `href="/"`, browser code paths, SSE on-page behavior, SQL schema, settings JSON, H1 WebSockets, QUIC RFC 9220 support, HOT/COLD cache or the `/logs` responsive view.
|
||||
|
||||
## Tests and risk
|
||||
|
||||
Rust unit tests cover empty and nonempty GET and HEAD forwarding, cache/404 preflight and stalled-client bounds; these are **source-level additions**, not proof of execution unless Cargo is available. The repository ships static checks, Node UI tests, SQLite upgrade smoke and Go tests. The pinned Cargo.lock dependency graph has not been re-resolved in this environment; a compiler-enabled build host must run `scripts/resolve_dependencies_0312.sh`, `./dev.sh check`, `./dev.sh test`, and a release build before deploying.
|
||||
|
||||
Reproduce on the actual iPhone with H3 **enabled**: open `https://rp.h.linuxiarz.pl`, sign in, click the original RustProxy logo repeatedly, compare successful GET `/` and JS/CSS access events with `http3_*` diagnostic events from the same peer and timestamp. In a failure capture Safari Web Inspector Network entries for document, JS and CSS, a QUIC packet/qlog trace if available, and the vhost JSONL. With systemd installations, view navigation breadcrumbs with `journalctl -u rustproxy --since '10 minutes ago' | grep 'http3_navigation_'`; the service log is separate from vhost JSONL. Old `H3_REQUEST_CANCELLED` events may be harmless; a failed new navigation or incomplete response is the acceptance condition. Test H1/H2, unrelated vhosts, cache retention and `/logs` mobile width as regressions.
|
||||
|
||||
**Acceptance remains OPEN** until compiled and tested on a real HTTP/3 listener with mobile devices; the original white-screen cause is not proven from one cancellation log alone.
|
||||
+2
-1
@@ -31,6 +31,7 @@ This is the canonical index for the current project documentation. The authentic
|
||||
- [0.26.3 implementation note](V0.26.3_IMPLEMENTATION.md) - global interface scaling, chart themes, square draft dock and Settings navigation polish.
|
||||
- [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.
|
||||
- [0.31.6 H3 iOS navigation hardening and diagnostic events](IMPLEMENTATION_0.31.6.md) - bounded empty GET/HEAD handling, H3 error draining and mobile reproduction.
|
||||
- [0.31.5 User-Agent blocking and consolidated log analysis](IMPLEMENTATION_0.31.5.md) - global/per-vhost UA blocklists, Traffic copy actions, repeated-value aggregation and per-vhost gzip consolidation.
|
||||
- [0.31.4 administration public Origin and settings](IMPLEMENTATION_0.31.4.md) - opt-in HTTPS origin, one-field DNS allowlist, additional hosts in advanced settings, legacy deduplication, CSRF and remaining deployment checks.
|
||||
- [0.31.3 mobile session / invalid-Origin recovery](IMPLEMENTATION_0.31.3.md) - deterministic BFCache and visibility session coordination, 403 diagnostics, recovery and remaining acceptance.
|
||||
@@ -84,7 +85,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. The current release implementation note is [IMPLEMENTATION_0.31.5.md](IMPLEMENTATION_0.31.5.md); [IMPLEMENTATION_0.31.4.md](IMPLEMENTATION_0.31.4.md) is retained as the previous administration-Origin milestone; [IMPLEMENTATION_0.31.3.md](IMPLEMENTATION_0.31.3.md) is retained as the previous mobile recovery milestone; [IMPLEMENTATION_0.31.1.md](IMPLEMENTATION_0.31.1.md) is retained as the prior mobile milestone; [IMPLEMENTATION_0.31.0.md](IMPLEMENTATION_0.31.0.md) is retained as the previous milestone; [V0.29.1_IMPLEMENTATION.md](V0.29.1_IMPLEMENTATION.md) is retained as historical context, and [V0.29.0_IMPLEMENTATION.md](V0.29.0_IMPLEMENTATION.md) is retained as the prior redirect/health/cache/log milestone; [V0.28.2_IMPLEMENTATION.md](V0.28.2_IMPLEMENTATION.md) is retained as the prior certificate-hardening milestone; [V0.28.1_IMPLEMENTATION.md](V0.28.1_IMPLEMENTATION.md) is retained as the prior external-path milestone; [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md) is retained as the prior GeoIP/listener/cache milestone, and [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md) is retained as the prior HTTP/3/cache-maintenance milestone, [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability.
|
||||
Version-labelled `V*.md` files are retained as historical implementation/audit records. Their version number describes the audited milestone. The current release implementation note is [IMPLEMENTATION_0.31.6.md](IMPLEMENTATION_0.31.6.md); [IMPLEMENTATION_0.31.5.md](IMPLEMENTATION_0.31.5.md) is retained as the previous log/cache milestone; [IMPLEMENTATION_0.31.4.md](IMPLEMENTATION_0.31.4.md) is retained as the previous administration-Origin milestone; [IMPLEMENTATION_0.31.3.md](IMPLEMENTATION_0.31.3.md) is retained as the previous mobile recovery milestone; [IMPLEMENTATION_0.31.1.md](IMPLEMENTATION_0.31.1.md) is retained as the prior mobile milestone; [IMPLEMENTATION_0.31.0.md](IMPLEMENTATION_0.31.0.md) is retained as the previous milestone; [V0.29.1_IMPLEMENTATION.md](V0.29.1_IMPLEMENTATION.md) is retained as historical context, and [V0.29.0_IMPLEMENTATION.md](V0.29.0_IMPLEMENTATION.md) is retained as the prior redirect/health/cache/log milestone; [V0.28.2_IMPLEMENTATION.md](V0.28.2_IMPLEMENTATION.md) is retained as the prior certificate-hardening milestone; [V0.28.1_IMPLEMENTATION.md](V0.28.1_IMPLEMENTATION.md) is retained as the prior external-path milestone; [V0.28.0_IMPLEMENTATION.md](V0.28.0_IMPLEMENTATION.md) is retained as the prior GeoIP/listener/cache milestone, and [V0.27.3_IMPLEMENTATION.md](V0.27.3_IMPLEMENTATION.md) is retained as the prior HTTP/3/cache-maintenance milestone, [V0.27.2_IMPLEMENTATION.md](V0.27.2_IMPLEMENTATION.md) is retained as the prior managed-ACME repair, [V0.27.1_IMPLEMENTATION.md](V0.27.1_IMPLEMENTATION.md) as the prior managed-runtime/list-sorting milestone, and older version notes remain historical evidence. The latest retained audit is [V0.25.5_AUDIT.md](V0.25.5_AUDIT.md); older audit files remain available for traceability.
|
||||
|
||||
Older context documents such as [INTEGRATIONS_040](INTEGRATIONS_040.md), [MANAGEMENT](MANAGEMENT.md), [SECURITY_AUDIT](SECURITY_AUDIT.md), [ACME_RFC2136](ACME_RFC2136.md) and older version audits remain available for traceability but should not override the current functional/technical documents above.
|
||||
|
||||
|
||||
+21
-1
@@ -1,4 +1,24 @@
|
||||
# Verification report - RustProxy 0.31.5
|
||||
# Verification report - RustProxy 0.31.6
|
||||
|
||||
## 0.31.6 - iPhone H3 full navigation and observability (2026-10-11)
|
||||
|
||||
- **Target:** iPhone Safari and Chrome when an existing dashboard session navigates through the unchanged sidebar logo link (`<a class="brand" href="/">`). It creates an H3 document GET while older SSE/API requests can be cancelled.
|
||||
- **Fix candidates implemented:** The H3 GET/HEAD cache and 404 preflight avoids unbounded waiting on a delayed request FIN; on an H3->HTTP/1.1 vhost, a truly empty GET or HEAD stream is normalized before forwarding (bounded 35 ms first-frame probe; nonempty data replayed; pending client keeps streaming). On an H3 accept error, no longer `abort_all` active responses; bounded drain still applies.
|
||||
- **Diagnostics:** Persist `http3_peer_cancelled_before_headers` events with status 0 (not 400). For interrupted responses write `http3_peer_cancelled_after_headers` or `http3_response_write_error`, including sanitized path without query, stream ID, response phase/status, bytes sent and redacted error. Normal process INFO/WARN logs also record the start, completion or interruption of root-document H3 GET `/` using its stream ID; no cookies or query strings. These records remain in vhost file logs and are subject to configured log sinks. They are distinct from application response access logs.
|
||||
- **Rust regression cases added:** GET/HEAD empty-body normalization, nonempty GET payload replay, bounded cache preflight and stalled GET; retained 0.31.5 H3 cancellation classification tests. Static release and documentation contract check extended.
|
||||
- **NOT VERIFIED:** Rust compilation, a live QUIC listener, reverse-proxy-to-panel traffic, iOS Safari/Chrome, frame captures, HTTP/3 stream integrity, TLS/QUIC delivery, persisted data upgrades. Do not describe root cause or fix as proven until captured on device. No WS-over-H3 is assumed; H1 websocket origins do not activate RFC 9220 by themselves.
|
||||
- **No changes:** Sidebar logo link, H3 on/off configuration, SQLite schema 9, cache format, frontend routes, authentication/session/CSRF or WebSocket settings.
|
||||
- **PASS — executed for 0.31.6:** `./dev.sh test` completed all pre-Cargo Python/SQLite/schema/upgrade/security/source checks, 300,000 simulated memory-model operations, 2,000 requests to a Python test peer, all Node UI regressions (including the 0.31.5 mobile/H3 suite updated for the new error path), documents and both language packs. `tools/checks/h3_navigation_0316.py` also passed. The script then exited **127** solely because Rust/Cargo is unavailable. `GOPROXY=off go test ./...` in `docker-agent`, `bash -n dev.sh` and OpenAPI JSON parsing passed. Source guards are not execution of Rust tests.
|
||||
|
||||
## 0.31.5 follow-up - mobile Stored Logs, H3 peer aborts, cache preservation (2026-10-11)
|
||||
|
||||
- **Problem 1, mobile `/logs`:** narrow pages grew horizontally under dynamically selected log columns and large JSON/error fields. CSS now scopes width to the Logs workspace, presents entries as two-column, keyboard-expandable cards at widths `<=700px`, preserves every user-selected field in its card, stacks dense filters, wraps long values, and retains independently horizontally scrollable code/aggregate data. Details and Raw JSON remain accessible. Existing row interaction and per-user column choices are unchanged.
|
||||
- **Problem 2, H3 synthetic status:** `resolver.resolve_request()` returned `StreamError::RemoteTerminate(H3_REQUEST_CANCELLED)` for a browser-cancelled stream before request headers; RustProxy previously called `log_protocol_error(..., 400, ...)`, making a *transport cancellation* appear to be an application HTTP 400 with no method/path. Now peer cancellations (`H3_REQUEST_CANCELLED` or peer `H3_NO_ERROR`) are low-level debug observations, while malformed messages still retain an attributed HTTP 400. Response send errors directly classified as peer cancellations no longer provoke a redundant `H3_INTERNAL_ERROR` reset. We deliberately retain the old `ReceiveBody::Drop` safeguards against h3/quinn pending-read panic.
|
||||
- **Important limit:** the supplied log cannot establish why the new navigation rendered a blank page; an old request may simply have been cancelled by F5. Missing HTTP method, path, authority, User-Agent, and 0 sent bytes prevent correlation to the failed top-level document. The *root cause of the blank page has not been reproduced or verified*. Validate Safari and Chrome with actual RustProxy QUIC/HTTPS, the reverse-proxy Host/Origin/`X-Forwarded-Proto` chain, browser Network waterfall (especially HTML and fingerprinted JS/CSS), and connection-level debug/qlog before accepting the fix as complete.
|
||||
- **Problem 3, cache:** `cache_revision()` used to hash complete vhost configuration, including `http2_enabled` and `http3_enabled`; an unrelated frontend protocol toggle rotated the global compatibility identity and triggered `CacheInvalidation::All`, flushing HOT and COLD. Normalize these two flags to the default `true` only for the cache-identity digest; live routing and serialized configuration remain unmodified. Rust tests cover H2, H3, combined toggles and a true cache-policy edit.
|
||||
- **Problem 4, public origin vs allowlist:** public HTTPS origin fixes scheme + host + port for security checks behind a *trusted* proxy (and implicitly allows its DNS host); extra allowed DNS hosts only expand allowed names. Retain both; improve Polish and English help text, no API/SQLite format changes.
|
||||
- **Verification, 2026-10-11:** `./dev.sh test` passed its entire available Python/SQL/schema/upgrade/security/security-dependency/Node/docs/language pre-Cargo suite, including new `h3_cache_mobile_0315_test.mjs`, then exited **127** because `cargo` is absent. `./dev.sh check` similarly exited **127** for missing Cargo. `GOPROXY=off go test ./...` in `docker-agent` passed. `bash -n dev.sh`, Node syntax, JSON syntax and focused source gates also passed. Rust unit tests were **not compiled or executed**.
|
||||
- **Browser layout smoke:** headless Chromium/Python Playwright rendered a synthetic fixture with the actual HTML/CSS log workspace at widths **320, 375, 430, 700, 768, 1024px**. In every case `body.scrollWidth === innerWidth`; cards were grid rows at `<=700px`, with desktop table rows at 768px+. Parsed fields, long unbroken values and raw JSON were included. This was a *static layout simulation*, **not** authenticated panel/live H3 acceptance.
|
||||
|
||||
## 0.31.5 follow-up - Stored Logs navigation polish
|
||||
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
+1
-1
@@ -2336,7 +2336,7 @@
|
||||
"administrationPublicOrigin": "Public panel HTTPS origin (optional)",
|
||||
"administrationHostsAdvanced": "Advanced: additional allowed DNS hostnames",
|
||||
"administrationHostsHelp": "Enter only additional hostnames. The public panel URL is included automatically. Without that URL, this remains an independent strict allowlist.",
|
||||
"administrationPublicOriginHelp": "Behind a trusted reverse proxy, enter the external HTTPS panel URL once. Its hostname is automatically allowed. Restart required; public proxy routing is unchanged.",
|
||||
"administrationPublicOriginHelp": "Pins the browser-facing HTTPS scheme, hostname and optional port for Origin/CSRF checks behind a trusted reverse proxy; this is not just a DNS allowlist. The hostname is automatically allowed. Restart required.",
|
||||
"userAgentBlocking": "User-Agent blocking",
|
||||
"globalUaBlockingTitle": "Global User-Agent blocklist",
|
||||
"globalUaBlockingHelp": "Blocks matching User-Agent values on public proxy vhosts before an upstream is opened. Per-vhost lists are configured in Vhosts → Security.",
|
||||
|
||||
+1
-1
@@ -2336,7 +2336,7 @@
|
||||
"administrationPublicOrigin": "Publiczny adres HTTPS panelu (opcjonalnie)",
|
||||
"administrationHostsAdvanced": "Zaawansowane: dodatkowe dozwolone hosty DNS",
|
||||
"administrationHostsHelp": "Wpisz wyłącznie dodatkowe domeny. Domena z publicznego adresu panelu jest dozwolona automatycznie. Bez tego adresu lista pozostaje niezależnym ścisłym ograniczeniem.",
|
||||
"administrationPublicOriginHelp": "Za zaufanym reverse proxy wpisz tylko publiczny adres HTTPS panelu. Jego domena zostanie dozwolona automatycznie. Wymagany restart; ruch proxy bez zmian.",
|
||||
"administrationPublicOriginHelp": "Za zaufanym reverse proxy ustala publiczny schemat HTTPS, domenę i opcjonalny port używane do kontroli Origin/CSRF. To nie tylko lista dozwolonych domen. Domena jest dopuszczana automatycznie. Wymagany restart.",
|
||||
"userAgentBlocking": "Blokowanie User-Agent",
|
||||
"globalUaBlockingTitle": "Globalna lista blokowanych User-Agent",
|
||||
"globalUaBlockingHelp": "Blokuje pasujące wartości User-Agent na publicznych vhostach proxy przed otwarciem połączenia do upstreamu. Listy per vhost są w Vhosts → Security.",
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
+2
-2
@@ -8,8 +8,8 @@ if ! command -v cargo >/dev/null 2>&1 || ! command -v rustc >/dev/null 2>&1; the
|
||||
exit 127
|
||||
fi
|
||||
python3 tools/checks/preflight.py
|
||||
if [[ "$(python3 scripts/version.py)" != '0.31.5' ]]; then
|
||||
printf 'Expected RustProxy 0.31.5 sources.\n' >&2
|
||||
if [[ "$(python3 scripts/version.py)" != '0.31.6' ]]; then
|
||||
printf 'Expected RustProxy 0.31.6 sources.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'Toolchain: '; rustc --version
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -244,6 +244,7 @@ pub(super) static READER_DOCUMENTS: &[(&str, &str, &str)] = &[
|
||||
"guides",
|
||||
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/docs/STREAMS.md")),
|
||||
),
|
||||
reader_doc!("docs/IMPLEMENTATION_0.31.6.md", "technical"),
|
||||
reader_doc!("docs/IMPLEMENTATION_0.31.5.md", "technical"),
|
||||
reader_doc!("docs/IMPLEMENTATION_0.31.4.md", "technical"),
|
||||
reader_doc!("docs/IMPLEMENTATION_0.31.3.md", "technical"),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.0.3",
|
||||
"info": {
|
||||
"title": "RustProxy Administration API",
|
||||
"version": "0.31.5",
|
||||
"version": "0.31.6",
|
||||
"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": [
|
||||
|
||||
@@ -463,6 +463,15 @@ function setMobileMenu(open) {
|
||||
}
|
||||
mobileMenuToggle?.addEventListener("click", event => { event.stopPropagation(); setMobileMenu(!dashboardSidebar.classList.contains("mobile-nav-open")); });
|
||||
document.querySelectorAll(".nav").forEach(button => button.addEventListener("click", () => { setPanel(button.dataset.panel); setMobileMenu(false); }));
|
||||
// The brand is a Home navigation action, not a hard reload of the HTTP/3
|
||||
// document. Preserve href="/" as the no-JS and modified-click fallback.
|
||||
document.querySelector('.brand[href="/"]')?.addEventListener("click", event => {
|
||||
if (!authenticated || event.defaultPrevented || event.button !== 0 ||
|
||||
event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return;
|
||||
event.preventDefault();
|
||||
setPanel("overview");
|
||||
setMobileMenu(false);
|
||||
});
|
||||
document.addEventListener("click", event => { if (dashboardSidebar?.classList.contains("mobile-nav-open") && !dashboardSidebar.contains(event.target)) setMobileMenu(false); });
|
||||
document.addEventListener("keydown", event => { if (event.key === "Escape" && dashboardSidebar?.classList.contains("mobile-nav-open")) { setMobileMenu(false); mobileMenuToggle?.focus(); } });
|
||||
window.addEventListener("popstate", () => { setPanel(panelFromLocation(), false); setMobileMenu(false); });
|
||||
|
||||
@@ -238,8 +238,15 @@
|
||||
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");
|
||||
row.setAttribute("aria-label", entrySummary(entry));
|
||||
for (const id of ids) {
|
||||
const value = columnValue(entry, id), cell = el("td", value); cell.title = value; row.append(cell);
|
||||
const value = columnValue(entry, id), cell = el("td", value); cell.title = value;
|
||||
// Mobile uses compact, expandable cards instead of a width-growing
|
||||
// table. All parsed fields remain accessible in the detail row.
|
||||
cell.dataset.mobileColumn = id;
|
||||
const label = columnLabel(id);
|
||||
cell.dataset.mobileLabel = label;
|
||||
row.append(cell);
|
||||
}
|
||||
const select = () => { selectedIndex = selectedIndex === index ? null : index; renderRows(); };
|
||||
row.addEventListener("click", select);
|
||||
|
||||
@@ -2758,7 +2758,7 @@
|
||||
<label><span data-i18n="administrationPublicOrigin">Public panel HTTPS origin (optional)</span><input
|
||||
id="admin-public-origin" maxlength="255" placeholder="https://panel.example.com"
|
||||
type="url" spellcheck="false" autocomplete="off" /></label>
|
||||
<p class="fine" data-i18n="administrationPublicOriginHelp">When behind a trusted reverse proxy, enter the panel's external HTTPS address once. Its DNS name is automatically allowed. A restart is required.</p>
|
||||
<p class="fine" data-i18n="administrationPublicOriginHelp">Pins the browser-facing HTTPS scheme, hostname and optional port for Origin/CSRF checks behind a trusted reverse proxy; it is not only a DNS allowlist. The hostname is automatically allowed. Restart required.</p>
|
||||
<details id="admin-extra-hosts-details">
|
||||
<summary data-i18n="administrationHostsAdvanced">Advanced: additional allowed DNS hostnames</summary>
|
||||
<label><span data-i18n="administrationHosts">Additional allowed DNS hostnames</span><textarea
|
||||
|
||||
@@ -10265,3 +10265,201 @@ html[data-chart-style] :is(#workspace, dialog) svg polyline {
|
||||
@media (max-width: 800px) {
|
||||
.file-log-aggregate-form { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
|
||||
/* Stored Logs on narrow screens: bounded container and a compact record list.
|
||||
The complete parsed JSON is available by opening any row, while the desktop
|
||||
table and the persisted column picker keep their original behavior. */
|
||||
@media (max-width: 700px) {
|
||||
#panel-filelogs,
|
||||
#panel-filelogs .file-logs-workspace,
|
||||
#file-log-view-entries,
|
||||
#file-log-view-counts,
|
||||
#panel-filelogs .file-logs-table,
|
||||
#panel-filelogs .file-logs-table-toolbar,
|
||||
#panel-filelogs .file-log-field-grid {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
#panel-filelogs .file-logs-workspace {
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
overflow: visible;
|
||||
}
|
||||
#panel-filelogs .card-heading {
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
#panel-filelogs .file-logs-heading-actions {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
#panel-filelogs .file-logs-heading-actions .file-log-consolidate {
|
||||
margin-inline-end: auto;
|
||||
white-space: normal;
|
||||
}
|
||||
#panel-filelogs .file-log-search-help-popover {
|
||||
width: min(320px, calc(100vw - 54px));
|
||||
}
|
||||
#panel-filelogs .file-logs-filter,
|
||||
#panel-filelogs .file-log-aggregate-form {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
#panel-filelogs .file-log-limit { max-width: none; }
|
||||
#panel-filelogs .file-logs-filter label,
|
||||
#panel-filelogs .file-logs-filter input,
|
||||
#panel-filelogs .file-logs-filter select,
|
||||
#panel-filelogs .file-log-aggregate-form label,
|
||||
#panel-filelogs .file-log-aggregate-form input,
|
||||
#panel-filelogs .file-log-aggregate-form select {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
#panel-filelogs .file-log-tabs { min-width: 0; }
|
||||
#panel-filelogs .file-log-tab {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
#panel-filelogs .file-logs-table-toolbar {
|
||||
align-items: flex-start;
|
||||
gap: 5px;
|
||||
}
|
||||
#panel-filelogs .file-log-column-picker { align-self: flex-end; }
|
||||
#panel-filelogs .file-logs-table {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
#panel-filelogs .file-logs-table table,
|
||||
#panel-filelogs .file-logs-table tbody {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
#panel-filelogs .file-logs-table thead { display: none; }
|
||||
#panel-filelogs .file-logs-table tbody tr:not(.file-log-detail-row) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 5px 10px;
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
}
|
||||
#panel-filelogs .file-logs-table tbody tr:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
#panel-filelogs .file-logs-table tbody tr.selected { border-color: var(--accent); }
|
||||
#panel-filelogs .file-logs-table tbody tr:not(.file-log-detail-row) > td {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: unset;
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
/* Every user-selected column is available even when the administrator
|
||||
replaces all default columns with custom parsed fields. */
|
||||
#panel-filelogs .file-logs-table td[data-mobile-column="record"] {
|
||||
grid-column: 1 / -1;
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
padding-inline-end: 16px;
|
||||
}
|
||||
#panel-filelogs .file-logs-table td[data-mobile-column="time"],
|
||||
#panel-filelogs .file-logs-table td[data-mobile-column="event"],
|
||||
#panel-filelogs .file-logs-table td[data-mobile-column="status"],
|
||||
#panel-filelogs .file-logs-table td[data-mobile-column="vhost"] {
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
#panel-filelogs .file-logs-table tbody tr:not(.file-log-detail-row) > td::before {
|
||||
content: attr(data-mobile-label) ": ";
|
||||
color: var(--text-subtle);
|
||||
font-weight: 400;
|
||||
}
|
||||
#panel-filelogs .file-logs-table tbody tr:not(.file-log-detail-row) > td[data-mobile-column="record"]::before {
|
||||
content: "";
|
||||
}
|
||||
#panel-filelogs .file-logs-table tbody tr:not(.file-log-detail-row)::after {
|
||||
content: "+";
|
||||
position: absolute;
|
||||
inset-block-end: 4px;
|
||||
inset-inline-end: 8px;
|
||||
color: var(--muted);
|
||||
font-size: 15px;
|
||||
pointer-events: none;
|
||||
}
|
||||
#panel-filelogs .file-logs-table tbody tr.selected::after { content: "-"; }
|
||||
#panel-filelogs .file-logs-table .file-log-detail-row {
|
||||
display: block;
|
||||
margin: -8px 0 8px;
|
||||
border: 1px solid var(--accent);
|
||||
border-top: 0;
|
||||
background: var(--panel2);
|
||||
}
|
||||
#panel-filelogs .file-logs-table .file-log-detail-row td {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
#panel-filelogs .file-log-field { grid-template-columns: minmax(0, 1fr); }
|
||||
#panel-filelogs .file-log-inline-detail { min-width: 0; padding: 8px; }
|
||||
#panel-filelogs .file-log-inline-detail pre { max-width: 100%; overflow-x: auto; }
|
||||
#panel-filelogs .file-log-aggregate-table {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
#panel-filelogs .file-log-aggregate-table table { table-layout: fixed; }
|
||||
#panel-filelogs .file-log-aggregate-table td:first-child {
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
#panel-filelogs .file-logs-pager { justify-content: space-between; }
|
||||
}
|
||||
|
||||
/* Tablet/narrow-desktop log tables scroll locally instead of expanding <main>. */
|
||||
#panel-filelogs,
|
||||
#panel-filelogs .file-logs-workspace,
|
||||
#file-log-view-entries,
|
||||
#file-log-view-counts {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
#panel-filelogs .file-logs-workspace { grid-template-columns: minmax(0, 1fr); }
|
||||
#panel-filelogs .file-logs-table,
|
||||
#panel-filelogs .file-log-aggregate-table {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
#panel-filelogs .file-logs-table { overflow: visible; }
|
||||
#panel-filelogs .file-logs-table td[data-mobile-column="time"],
|
||||
#panel-filelogs .file-logs-table td[data-mobile-column="vhost"] { grid-column: 1 / -1; }
|
||||
#panel-filelogs .file-logs-table td[data-mobile-column="record"] {
|
||||
max-height: 4.4em;
|
||||
overflow: hidden;
|
||||
}
|
||||
#panel-filelogs .file-logs-table tr.selected td[data-mobile-column="record"] {
|
||||
max-height: none;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
+80
-137
@@ -28,66 +28,7 @@ use tokio::{
|
||||
task::JoinSet,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
pub alt_svc_max_age: u64,
|
||||
pub max_concurrent_connections: u32,
|
||||
pub max_concurrent_bidistream: u32,
|
||||
pub max_concurrent_unistream: u32,
|
||||
/// QUIC idle timeout in seconds; zero disables the transport idle timeout.
|
||||
pub max_idle_timeout: u64,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
alt_svc_max_age: 3600,
|
||||
max_concurrent_connections: 512,
|
||||
max_concurrent_bidistream: 100,
|
||||
max_concurrent_unistream: 100,
|
||||
max_idle_timeout: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Settings {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
ensure!(
|
||||
self.alt_svc_max_age <= 604800,
|
||||
"HTTP/3 Alt-Svc lifetime must be at most seven days"
|
||||
);
|
||||
ensure!(
|
||||
(1..=4096).contains(&self.max_concurrent_connections),
|
||||
"HTTP/3 connections per listener must be 1..4096"
|
||||
);
|
||||
ensure!(
|
||||
(1..=1024).contains(&self.max_concurrent_bidistream),
|
||||
"HTTP/3 bidirectional streams must be 1..1024"
|
||||
);
|
||||
ensure!((3..=1024).contains(&self.max_concurrent_unistream),"HTTP/3 requires at least three peer unidirectional streams (control and QPACK), maximum 1024");
|
||||
ensure!(
|
||||
self.max_idle_timeout <= 86400,
|
||||
"HTTP/3 idle timeout must be 0..86400 seconds"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
pub fn transport(&self) -> Result<quinn::TransportConfig> {
|
||||
self.validate()?;
|
||||
let mut transport = quinn::TransportConfig::default();
|
||||
transport
|
||||
.max_concurrent_bidi_streams(self.max_concurrent_bidistream.into())
|
||||
.max_concurrent_uni_streams(self.max_concurrent_unistream.into())
|
||||
.max_idle_timeout(if self.max_idle_timeout == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(self.max_idle_timeout).try_into()?)
|
||||
})
|
||||
.stream_receive_window((256 * 1024u32).into())
|
||||
.receive_window((8 * 1024 * 1024u32).into())
|
||||
.send_window(8 * 1024 * 1024)
|
||||
.datagram_receive_buffer_size(None);
|
||||
Ok(transport)
|
||||
}
|
||||
}
|
||||
include!("http3/settings.rs");
|
||||
pub fn crypto(
|
||||
tls: &Arc<rustls::ServerConfig>,
|
||||
policy: &crate::tls::ProtocolPolicy,
|
||||
@@ -176,77 +117,6 @@ pub fn advertise(
|
||||
}
|
||||
}
|
||||
}
|
||||
struct Lease {
|
||||
_permit: OwnedSemaphorePermit,
|
||||
metrics: Arc<Metrics>,
|
||||
}
|
||||
impl Drop for Lease {
|
||||
fn drop(&mut self) {
|
||||
self.metrics
|
||||
.http3_active_connections
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
self.metrics
|
||||
.active_connections
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn log_protocol_error(
|
||||
state: &App,
|
||||
kind: &'static str,
|
||||
detail: impl std::fmt::Display,
|
||||
peer: SocketAddr,
|
||||
local: SocketAddr,
|
||||
status: u16,
|
||||
authority: Option<String>,
|
||||
host: Option<String>,
|
||||
sni: Option<String>,
|
||||
) {
|
||||
let resolved = state.config.load().resolve_logging_vhost(
|
||||
local,
|
||||
true,
|
||||
None,
|
||||
authority.as_deref(),
|
||||
host.as_deref(),
|
||||
sni.as_deref(),
|
||||
);
|
||||
let (vhost, matched_by) = resolved
|
||||
.map(|resolved| (Some(resolved.vhost), resolved.matched_by))
|
||||
.unwrap_or((None, "none"));
|
||||
state.logs.push_protocol_error_attributed(
|
||||
kind,
|
||||
detail,
|
||||
peer,
|
||||
local,
|
||||
"quic",
|
||||
"3",
|
||||
status,
|
||||
sni,
|
||||
authority.or(host),
|
||||
vhost,
|
||||
matched_by,
|
||||
);
|
||||
}
|
||||
fn request_identity<B>(request: &http::Request<B>) -> (Option<String>, Option<String>) {
|
||||
let authority = request
|
||||
.uri()
|
||||
.authority()
|
||||
.map(|value| value.as_str().to_owned());
|
||||
let host = request
|
||||
.headers()
|
||||
.get(http::header::HOST)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::to_owned);
|
||||
(authority, host)
|
||||
}
|
||||
fn connection_sni(connection: &quinn::Connection) -> Option<String> {
|
||||
connection
|
||||
.handshake_data()?
|
||||
.downcast::<quinn::crypto::rustls::HandshakeData>()
|
||||
.ok()?
|
||||
.server_name
|
||||
.and_then(|name| crate::config::request_host(&name).ok())
|
||||
}
|
||||
pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duration) -> Result<()> {
|
||||
let slots = Arc::new(Semaphore::new(
|
||||
bound.settings.max_concurrent_connections as usize,
|
||||
@@ -386,9 +256,19 @@ async fn connection_loop(
|
||||
let (mut request,stream)=match resolved {
|
||||
Ok(Ok(request))=>request,
|
||||
Ok(Err(error))=>{
|
||||
log_protocol_error(
|
||||
&state, "http3_request_error", error, peer, local, 400, None, None, tls_sni.clone(),
|
||||
);
|
||||
if client_cancelled_request(&error) {
|
||||
// Record the cancellation with status=0 (no HTTP response)
|
||||
// for iOS navigation investigations, never synthetic 400.
|
||||
log_protocol_error(
|
||||
&state, "http3_peer_cancelled_before_headers",
|
||||
format!("phase=request_headers stream_status=cancelled error={error}"),
|
||||
peer, local, 0, None, None, tls_sni.clone(),
|
||||
);
|
||||
} else {
|
||||
log_protocol_error(
|
||||
&state, "http3_request_error", error, peer, local, 400, None, None, tls_sni.clone(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
},
|
||||
Err(_)=>{
|
||||
@@ -404,6 +284,22 @@ async fn connection_loop(
|
||||
let extended_connect=request.method()==Method::CONNECT && protocol.is_some();
|
||||
let websocket=extended_connect && protocol.as_deref()==Some("websocket");
|
||||
let (mut send,receive)=stream.split();
|
||||
let stream_id=format!("{:?}",send.send_id());
|
||||
let diagnostic_method=request.method().as_str().to_owned();
|
||||
// Do not record query strings or cookies in transport diagnostics.
|
||||
let diagnostic_path=request.uri().path().chars().take(160).collect::<String>();
|
||||
let (diagnostic_authority, diagnostic_host)=request_identity(&request);
|
||||
// GET / is a full document navigation (the original logo).
|
||||
// Emit only query-free, bounded navigation metadata.
|
||||
let root_navigation=diagnostic_method=="GET" && diagnostic_path=="/";
|
||||
if root_navigation { log_root_navigation(
|
||||
"started", peer, local, &tls_sni, &stream_id, "request_headers", 0, 0, false,
|
||||
); }
|
||||
tracing::debug!(
|
||||
peer=%peer, stream_id=%stream_id,
|
||||
method=%diagnostic_method, path=%diagnostic_path,
|
||||
"HTTP/3 request headers accepted"
|
||||
);
|
||||
if invalid_headers(request.headers()) {
|
||||
let (authority, host) = request_identity(&request);
|
||||
log_protocol_error(
|
||||
@@ -437,8 +333,12 @@ async fn connection_loop(
|
||||
crate::proxy::headers::strip_hop_by_hop(&mut parts.headers);
|
||||
parts.version=Version::HTTP_3;
|
||||
let response=http::Response::from_parts(parts,());
|
||||
let response_status=response.status().as_u16();
|
||||
let mut response_phase="response_headers";
|
||||
let mut sent_body_bytes=0u64;
|
||||
let sent=async {
|
||||
tokio::time::timeout(state.body_idle_timeout,send.send_response(response)).await??;
|
||||
response_phase="response_body";
|
||||
if let Some(tunnel)=tunnel {
|
||||
let Some(mut receive)=tunnel_receive.take() else {
|
||||
send.stop_stream(Code::H3_INTERNAL_ERROR);
|
||||
@@ -462,21 +362,62 @@ async fn connection_loop(
|
||||
while let Some(frame)=body.frame().await {
|
||||
let frame=frame?;
|
||||
match frame.into_data() {
|
||||
Ok(data)=>{if !data.is_empty() {tokio::time::timeout(state.body_idle_timeout,send.send_data(data)).await??;}},
|
||||
Ok(data)=>{if !data.is_empty() {
|
||||
let data_len=data.len() as u64;
|
||||
tokio::time::timeout(state.body_idle_timeout,send.send_data(data)).await??;
|
||||
sent_body_bytes=sent_body_bytes.saturating_add(data_len);
|
||||
}},
|
||||
Err(frame)=>{if let Ok(mut trailers)=frame.into_trailers() {
|
||||
crate::proxy::headers::strip_hop_by_hop(&mut trailers);
|
||||
tokio::time::timeout(state.body_idle_timeout,send.send_trailers(trailers)).await??;
|
||||
}},
|
||||
}
|
||||
}
|
||||
response_phase="response_finish";
|
||||
tokio::time::timeout(state.body_idle_timeout,send.finish()).await??;
|
||||
Ok::<(),anyhow::Error>(())
|
||||
}.await;
|
||||
if let Err(error)=sent {tracing::debug!(%error,"HTTP/3 response interrupted");send.stop_stream(Code::H3_INTERNAL_ERROR);}
|
||||
if let Err(error)=sent {
|
||||
let cancelled=error.downcast_ref::<h3::error::StreamError>()
|
||||
.is_some_and(client_cancelled_request);
|
||||
let kind=if cancelled { "http3_peer_cancelled_after_headers" }
|
||||
else { "http3_response_write_error" };
|
||||
log_protocol_error(
|
||||
&state, kind,
|
||||
format!("phase={response_phase} stream_id={stream_id} method={diagnostic_method} path={diagnostic_path} response_status={response_status} body_bytes_sent={sent_body_bytes} error={error}"),
|
||||
peer, local, 0,
|
||||
diagnostic_authority, diagnostic_host, tls_sni.clone(),
|
||||
);
|
||||
if root_navigation { log_root_navigation(
|
||||
"interrupted", peer, local, &tls_sni, &stream_id, response_phase,
|
||||
response_status, sent_body_bytes, cancelled,
|
||||
); }
|
||||
if !cancelled {
|
||||
// Never mark a truncated upstream response as success.
|
||||
// Reset only this response stream, not sibling requests.
|
||||
send.stop_stream(Code::H3_INTERNAL_ERROR);
|
||||
}
|
||||
} else if root_navigation {
|
||||
log_root_navigation("complete", peer, local, &tls_sni, &stream_id,
|
||||
"response_finish", response_status, sent_body_bytes, false);
|
||||
} else {
|
||||
log_h3_response_complete(peer, &stream_id, response_status, sent_body_bytes);
|
||||
}
|
||||
});
|
||||
},
|
||||
Ok(None)=>break,
|
||||
Err(error)=>{requests.abort_all();return Err(error.into());},
|
||||
Err(error)=>{
|
||||
// The parser/QUIC transport can stop accepting after a peer
|
||||
// refresh while unrelated response streams are still alive.
|
||||
// Abort_all here reset those streams (including a new GET /).
|
||||
// Stop accepting and allow the bounded drain below instead.
|
||||
log_protocol_error(
|
||||
&state, "http3_accept_error", &error,
|
||||
connection.remote_address(), local, 0, None, None, tls_sni.clone(),
|
||||
);
|
||||
tracing::debug!(%error, "HTTP/3 accept stopped; draining in-flight streams");
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -492,6 +433,8 @@ async fn connection_loop(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
include!("http3/identity.rs");
|
||||
include!("http3/diagnostics.rs");
|
||||
include!("http3/body.rs");
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+3
-3
@@ -110,8 +110,8 @@ impl HttpBody for ReceiveBody {
|
||||
}
|
||||
impl Drop for ReceiveBody {
|
||||
fn drop(&mut self) {
|
||||
// Dropping the request stream cancels the receive side at QUIC level.
|
||||
// Explicit STOP_SENDING is used only from poll_frame paths where the
|
||||
// stream handle is available and a protocol error was detected.
|
||||
// Do not call stop_sending here: h3/quinn can still have a pending
|
||||
// receive read and has previously panicked on that cancellation path.
|
||||
// Protocol framing violations explicitly stop only from poll_frame.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
struct Lease {
|
||||
_permit: OwnedSemaphorePermit,
|
||||
metrics: Arc<Metrics>,
|
||||
}
|
||||
impl Drop for Lease {
|
||||
fn drop(&mut self) {
|
||||
self.metrics
|
||||
.http3_active_connections
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
self.metrics
|
||||
.active_connections
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn log_protocol_error(
|
||||
state: &App,
|
||||
kind: &'static str,
|
||||
detail: impl std::fmt::Display,
|
||||
peer: SocketAddr,
|
||||
local: SocketAddr,
|
||||
status: u16,
|
||||
authority: Option<String>,
|
||||
host: Option<String>,
|
||||
sni: Option<String>,
|
||||
) {
|
||||
let resolved = state.config.load().resolve_logging_vhost(
|
||||
local,
|
||||
true,
|
||||
None,
|
||||
authority.as_deref(),
|
||||
host.as_deref(),
|
||||
sni.as_deref(),
|
||||
);
|
||||
let (vhost, matched_by) = resolved
|
||||
.map(|resolved| (Some(resolved.vhost), resolved.matched_by))
|
||||
.unwrap_or((None, "none"));
|
||||
state.logs.push_protocol_error_attributed(
|
||||
kind,
|
||||
detail,
|
||||
peer,
|
||||
local,
|
||||
"quic",
|
||||
"3",
|
||||
status,
|
||||
sni,
|
||||
authority.or(host),
|
||||
vhost,
|
||||
matched_by,
|
||||
);
|
||||
}
|
||||
|
||||
/// Service-level breadcrumbs for a full HTTP/3 GET / navigation. User-provided
|
||||
/// query strings, headers and error details must not reach this diagnostic.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn log_root_navigation(
|
||||
stage: &'static str,
|
||||
peer: SocketAddr,
|
||||
local: SocketAddr,
|
||||
sni: &Option<String>,
|
||||
stream_id: &str,
|
||||
phase: &'static str,
|
||||
response_status: u16,
|
||||
body_bytes_sent: u64,
|
||||
cancelled: bool,
|
||||
) {
|
||||
match stage {
|
||||
"started" => tracing::info!(
|
||||
peer=%peer, local=%local, sni=?sni, stream_id=%stream_id,
|
||||
method="GET", path="/", "http3_navigation_started"
|
||||
),
|
||||
"complete" => tracing::info!(
|
||||
peer=%peer, local=%local, sni=?sni, stream_id=%stream_id,
|
||||
method="GET", path="/", response_status, body_bytes_sent,
|
||||
"http3_navigation_complete"
|
||||
),
|
||||
"interrupted" => tracing::warn!(
|
||||
peer=%peer, local=%local, sni=?sni, stream_id=%stream_id,
|
||||
method="GET", path="/", phase, response_status, body_bytes_sent,
|
||||
cancelled, "http3_navigation_interrupted"
|
||||
),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn log_h3_response_complete(peer: SocketAddr, stream_id: &str, response_status: u16, body_bytes_sent: u64) {
|
||||
tracing::debug!(
|
||||
peer=%peer, stream_id=%stream_id, response_status, body_bytes_sent,
|
||||
"HTTP/3 response completed"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/// A client can cancel an obsolete in-flight request during navigation/F5.
|
||||
/// RFC 9114 section 4.1.1 gives this a stream reset code, not an HTTP 400.
|
||||
/// Treat only peer-originated cancellation as benign; local protocol errors
|
||||
/// and other remote reset codes remain visible to operators.
|
||||
fn client_cancelled_code(code: Code) -> bool {
|
||||
code == Code::H3_REQUEST_CANCELLED || code == Code::H3_NO_ERROR
|
||||
}
|
||||
fn client_cancelled_request(error: &h3::error::StreamError) -> bool {
|
||||
matches!(
|
||||
error,
|
||||
h3::error::StreamError::RemoteTerminate { code, .. } if client_cancelled_code(*code)
|
||||
)
|
||||
}
|
||||
fn request_identity<B>(request: &http::Request<B>) -> (Option<String>, Option<String>) {
|
||||
let authority = request
|
||||
.uri()
|
||||
.authority()
|
||||
.map(|value| value.as_str().to_owned());
|
||||
let host = request
|
||||
.headers()
|
||||
.get(http::header::HOST)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(str::to_owned);
|
||||
(authority, host)
|
||||
}
|
||||
fn connection_sni(connection: &quinn::Connection) -> Option<String> {
|
||||
connection
|
||||
.handshake_data()?
|
||||
.downcast::<quinn::crypto::rustls::HandshakeData>()
|
||||
.ok()?
|
||||
.server_name
|
||||
.and_then(|name| crate::config::request_host(&name).ok())
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
pub alt_svc_max_age: u64,
|
||||
pub max_concurrent_connections: u32,
|
||||
pub max_concurrent_bidistream: u32,
|
||||
pub max_concurrent_unistream: u32,
|
||||
/// QUIC idle timeout in seconds; zero disables the transport idle timeout.
|
||||
pub max_idle_timeout: u64,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
alt_svc_max_age: 3600,
|
||||
max_concurrent_connections: 512,
|
||||
max_concurrent_bidistream: 100,
|
||||
max_concurrent_unistream: 100,
|
||||
max_idle_timeout: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Settings {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
ensure!(
|
||||
self.alt_svc_max_age <= 604800,
|
||||
"HTTP/3 Alt-Svc lifetime must be at most seven days"
|
||||
);
|
||||
ensure!(
|
||||
(1..=4096).contains(&self.max_concurrent_connections),
|
||||
"HTTP/3 connections per listener must be 1..4096"
|
||||
);
|
||||
ensure!(
|
||||
(1..=1024).contains(&self.max_concurrent_bidistream),
|
||||
"HTTP/3 bidirectional streams must be 1..1024"
|
||||
);
|
||||
ensure!((3..=1024).contains(&self.max_concurrent_unistream),"HTTP/3 requires at least three peer unidirectional streams (control and QPACK), maximum 1024");
|
||||
ensure!(
|
||||
self.max_idle_timeout <= 86400,
|
||||
"HTTP/3 idle timeout must be 0..86400 seconds"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
pub fn transport(&self) -> Result<quinn::TransportConfig> {
|
||||
self.validate()?;
|
||||
let mut transport = quinn::TransportConfig::default();
|
||||
transport
|
||||
.max_concurrent_bidi_streams(self.max_concurrent_bidistream.into())
|
||||
.max_concurrent_uni_streams(self.max_concurrent_unistream.into())
|
||||
.max_idle_timeout(if self.max_idle_timeout == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(self.max_idle_timeout).try_into()?)
|
||||
})
|
||||
.stream_receive_window((256 * 1024u32).into())
|
||||
.receive_window((8 * 1024 * 1024u32).into())
|
||||
.send_window(8 * 1024 * 1024)
|
||||
.datagram_receive_buffer_size(None);
|
||||
Ok(transport)
|
||||
}
|
||||
}
|
||||
@@ -67,6 +67,15 @@ mod tests {
|
||||
assert!(!h.contains_key("alt-svc"));
|
||||
}
|
||||
#[test]
|
||||
fn client_cancellation_is_a_stream_event_not_an_http_400() {
|
||||
// Exercise the production classifier; RemoteTerminate is
|
||||
// non-exhaustive in hpx-h3 and cannot be constructed by this crate.
|
||||
assert!(client_cancelled_code(Code::H3_REQUEST_CANCELLED));
|
||||
assert!(client_cancelled_code(Code::H3_NO_ERROR));
|
||||
assert!(!client_cancelled_code(Code::H3_MESSAGE_ERROR));
|
||||
assert!(!client_cancelled_code(Code::H3_INTERNAL_ERROR));
|
||||
}
|
||||
#[test]
|
||||
fn h3_rejects_http1_connection_specific_headers() {
|
||||
for name in ["connection", "transfer-encoding", "upgrade"] {
|
||||
let mut h = HeaderMap::new();
|
||||
|
||||
@@ -342,8 +342,24 @@ async fn empty_for_cache(request: &mut Request) -> Result<bool, axum::Error> {
|
||||
return Ok(false);
|
||||
}
|
||||
// Multiplexed transports may end a body with an empty DATA frame.
|
||||
// On H3 document GET/HEAD, waiting without a bound can stall the entire
|
||||
// cache/404-routing stage before proxy forwarding even starts. A timed
|
||||
// probe that finds no FIN conservatively bypasses caching; it never drops
|
||||
// or consumes a nonempty request body.
|
||||
let bounded_h3_navigation = request.version() == Version::HTTP_3
|
||||
&& (request.method() == Method::GET || request.method() == Method::HEAD);
|
||||
for _ in 0..8 {
|
||||
match request.body_mut().frame().await {
|
||||
let next = if bounded_h3_navigation {
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_millis(35), request.body_mut().frame(),
|
||||
).await {
|
||||
Ok(frame) => frame,
|
||||
Err(_) => return Ok(false),
|
||||
}
|
||||
} else {
|
||||
request.body_mut().frame().await
|
||||
};
|
||||
match next {
|
||||
None => {
|
||||
*request.body_mut() = Body::empty();
|
||||
return Ok(true);
|
||||
@@ -381,8 +397,6 @@ async fn normalize_h3_h1_unknown_body(
|
||||
let method = request.method();
|
||||
if request.version() != Version::HTTP_3
|
||||
|| upstream_version != Version::HTTP_11
|
||||
|| method == Method::GET
|
||||
|| method == Method::HEAD
|
||||
|| method == Method::CONNECT
|
||||
|| request.headers().contains_key(http::header::CONTENT_LENGTH)
|
||||
|| request.body().size_hint().exact().is_some()
|
||||
@@ -393,8 +407,27 @@ async fn normalize_h3_h1_unknown_body(
|
||||
// Ignore a small number of empty DATA frames while looking for either the
|
||||
// stream end or the first frame that carries request semantics. The body
|
||||
// idle timer in MeteredBody still bounds a stalled peer.
|
||||
// A full page navigation (e.g. clicking the RustProxy logo) is a GET.
|
||||
// H3 HEADERS can precede FIN on a document GET. Resolve the receive half
|
||||
// when FIN arrives promptly, before handing the request to an H1 origin.
|
||||
// Hyper already special-cases GET/HEAD H1 encoding; this probe targets
|
||||
// receive-stream completion, not a presumed chunked GET on the wire.
|
||||
// When the peer is slow or sends a real request body, retain streaming and
|
||||
// do not delay navigation by the general upload/body-idle deadline.
|
||||
let navigation_method = method == Method::GET || method == Method::HEAD;
|
||||
for _ in 0..8 {
|
||||
match request.body_mut().frame().await {
|
||||
let next = if navigation_method {
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_millis(35),
|
||||
request.body_mut().frame(),
|
||||
).await {
|
||||
Ok(frame) => frame,
|
||||
Err(_) => return Ok(()),
|
||||
}
|
||||
} else {
|
||||
request.body_mut().frame().await
|
||||
};
|
||||
match next {
|
||||
None => {
|
||||
*request.body_mut() = Body::empty();
|
||||
return Ok(());
|
||||
|
||||
@@ -8,6 +8,8 @@ include!("forward_entry.rs");
|
||||
|
||||
#[cfg(test)]
|
||||
include!("tests/authority_tests.rs");
|
||||
#[cfg(test)]
|
||||
include!("tests/h3_navigation_tests.rs");
|
||||
|
||||
#[cfg(test)]
|
||||
include!("tests/http_invalidation_tests.rs");
|
||||
@@ -0,0 +1,75 @@
|
||||
mod h3_navigation_tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn h3_bodyless_get_and_head_are_normalized_for_http1_navigation() {
|
||||
use bytes::Bytes;
|
||||
for method in [Method::GET, Method::HEAD] {
|
||||
let stream = futures_util::stream::empty::<Result<Bytes, std::io::Error>>();
|
||||
let mut request = Request::builder()
|
||||
.method(method.clone())
|
||||
.version(Version::HTTP_3)
|
||||
.uri("https://rp.example.test/")
|
||||
.body(Body::from_stream(stream))
|
||||
.unwrap();
|
||||
assert_eq!(request.body().size_hint().exact(), None);
|
||||
normalize_h3_h1_unknown_body(&mut request, Version::HTTP_11).await.unwrap();
|
||||
assert!(request.body().is_end_stream(), "{method}");
|
||||
assert_eq!(request.body().size_hint().exact(), Some(0), "{method}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn h3_get_with_payload_retains_payload_for_http1_upstream() {
|
||||
use bytes::Bytes;
|
||||
let stream = futures_util::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(b"unexpected-but-valid"))]);
|
||||
let mut request = Request::builder().method(Method::GET)
|
||||
.version(Version::HTTP_3).uri("https://rp.example.test/")
|
||||
.body(Body::from_stream(stream)).unwrap();
|
||||
normalize_h3_h1_unknown_body(&mut request, Version::HTTP_11).await.unwrap();
|
||||
let body = request.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(body, Bytes::from_static(b"unexpected-but-valid"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn h3_stalled_cache_probe_bypasses_without_blocking_navigation() {
|
||||
use bytes::Bytes;
|
||||
let stream = futures_util::stream::pending::<Result<Bytes, std::io::Error>>();
|
||||
let mut request = Request::builder().method(Method::GET)
|
||||
.version(Version::HTTP_3).uri("https://rp.example.test/")
|
||||
.body(Body::from_stream(stream)).unwrap();
|
||||
let empty = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(750), empty_for_cache(&mut request),
|
||||
).await.expect("bounded cache preflight").unwrap();
|
||||
assert!(!empty, "unknown body is not cacheable");
|
||||
assert_eq!(request.body().size_hint().exact(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn h3_cache_probe_replays_nonempty_get_without_dropping_bytes() {
|
||||
use bytes::Bytes;
|
||||
let stream = futures_util::stream::iter([
|
||||
Ok::<_, std::io::Error>(Bytes::from_static(b"intact")),
|
||||
]);
|
||||
let mut request = Request::builder().method(Method::GET)
|
||||
.version(Version::HTTP_3).uri("https://rp.example.test/")
|
||||
.body(Body::from_stream(stream)).unwrap();
|
||||
assert!(!empty_for_cache(&mut request).await.unwrap());
|
||||
assert_eq!(request.into_body().collect().await.unwrap().to_bytes(), Bytes::from_static(b"intact"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn h3_stalled_empty_get_does_not_block_navigation() {
|
||||
use bytes::Bytes;
|
||||
let stream = futures_util::stream::pending::<Result<Bytes, std::io::Error>>();
|
||||
let mut request = Request::builder().method(Method::GET)
|
||||
.version(Version::HTTP_3).uri("https://rp.example.test/")
|
||||
.body(Body::from_stream(stream)).unwrap();
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_millis(750),
|
||||
normalize_h3_h1_unknown_body(&mut request, Version::HTTP_11),
|
||||
).await.expect("bounded GET probe").unwrap();
|
||||
assert_eq!(request.body().size_hint().exact(), None);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -69,6 +69,30 @@ mod cache_revision_tests {
|
||||
CacheInvalidation::None
|
||||
));
|
||||
|
||||
for label in ["http2", "http3"] {
|
||||
let mut protocol_only = base.clone();
|
||||
if label == "http2" {
|
||||
protocol_only.vhosts[0].http2_enabled = !base.vhosts[0].http2_enabled;
|
||||
} else {
|
||||
protocol_only.vhosts[0].http3_enabled = !base.vhosts[0].http3_enabled;
|
||||
}
|
||||
// The effective configuration really changes, but the cache key
|
||||
// does not. This must hold for both toggling directions.
|
||||
assert_ne!(
|
||||
(protocol_only.vhosts[0].http2_enabled, protocol_only.vhosts[0].http3_enabled),
|
||||
(base.vhosts[0].http2_enabled, base.vhosts[0].http3_enabled),
|
||||
"{label} toggle did not change a transport switch"
|
||||
);
|
||||
assert_eq!(cache_revision(&protocol_only), identity, "{label} rotated cache");
|
||||
assert!(matches!(cache_invalidation(&base, &protocol_only), CacheInvalidation::None));
|
||||
assert!(matches!(cache_invalidation(&protocol_only, &base), CacheInvalidation::None));
|
||||
}
|
||||
let mut both_protocols = base.clone();
|
||||
both_protocols.vhosts[0].http2_enabled = !base.vhosts[0].http2_enabled;
|
||||
both_protocols.vhosts[0].http3_enabled = !base.vhosts[0].http3_enabled;
|
||||
assert_eq!(cache_revision(&both_protocols), identity);
|
||||
assert!(matches!(cache_invalidation(&base, &both_protocols), CacheInvalidation::None));
|
||||
|
||||
let mut vhost_diagnostics = base.clone();
|
||||
vhost_diagnostics.vhosts[0].cache.expose_only_hits = Some(true);
|
||||
assert_eq!(cache_revision(&vhost_diagnostics), identity);
|
||||
|
||||
@@ -117,6 +117,11 @@ fn cache_revision(config: &config::Config) -> u64 {
|
||||
let mut vhosts = config.vhosts.clone();
|
||||
for vhost in &mut vhosts {
|
||||
vhost.cache.expose_only_hits = None;
|
||||
// Frontend ALPN/QUIC switches decide how a client reaches a vhost,
|
||||
// not which representation the origin returns. Hot and cold entries
|
||||
// must survive HTTP/2 or HTTP/3 opt-in/out saves.
|
||||
vhost.http2_enabled = true;
|
||||
vhost.http3_enabled = true;
|
||||
}
|
||||
let encoded = serde_json::to_vec(&(
|
||||
&cache,
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
+10
-10
@@ -24,7 +24,7 @@ def read_docker_agent_source() -> str:
|
||||
|
||||
preflight(root)
|
||||
manifest = tomllib.loads((root / "Cargo.toml").read_text())
|
||||
assert manifest["package"]["version"] == "0.31.5"
|
||||
assert manifest["package"]["version"] == "0.31.6"
|
||||
assert manifest["dependencies"]["quinn"]["version"] == "0.11.12"
|
||||
h3dep = manifest["dependencies"]["h3"]
|
||||
assert h3dep["package"] == "hpx-h3" and h3dep["version"] == "=2.5.20" 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.31.5"' in lock
|
||||
assert 'name = "rust-proxy"\nversion = "0.31.6"' 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.31.5",
|
||||
"CHANGELOG.md": "## 0.31.5",
|
||||
"ROADMAP.md": "## 0.31.5",
|
||||
"AGENTS.md": "Current source version: 0.31.5.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.31.5",
|
||||
"README.md": "# RustProxy 0.31.6",
|
||||
"CHANGELOG.md": "## 0.31.6",
|
||||
"ROADMAP.md": "## 0.31.6",
|
||||
"AGENTS.md": "Current source version: 0.31.6.",
|
||||
"docs/TEST_REPORT.md": "# Verification report - RustProxy 0.31.6",
|
||||
}.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"
|
||||
@@ -285,7 +285,7 @@ assert 'Opt-in health decisions based on ordinary user traffic. Defaults to disa
|
||||
assert '"force_ssl"' in openapi and '"forward_client_ip"' in openapi
|
||||
assert '"backend_header"' in openapi and '"backend_timing_header"' in openapi
|
||||
assert '"StreamMetrics"' in openapi and '"streams"' in openapi
|
||||
for marker in ['"version": "0.31.5"', '"/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.31.6"', '"/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
|
||||
@@ -1394,7 +1394,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.31.5"' in openapi
|
||||
assert '"RuntimeInformation"' in openapi and '"version": "0.31.6"' in openapi
|
||||
|
||||
# 0.24.1 compiler and hostname-cardinality regressions.
|
||||
tls_0241 = rust_source(root, "src/tls.rs")
|
||||
@@ -1633,5 +1633,5 @@ for marker_0282 in [
|
||||
assert 'Zewnętrzne ścieżki plików' in (root / 'langs/pl_pl.json').read_text()
|
||||
assert 'setfacl -m u:rustproxy:--x /etc/ssl/' in (root / 'docs/V0.28.2_IMPLEMENTATION.md').read_text()
|
||||
|
||||
print("PASS: 0.31.5 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.")
|
||||
print("PASS: 0.31.6 manifest/package, shared system-statistics/runtime metadata, Settings logging, retained H2, external-packaging/Grafana/Prometheus/404-host/custom-location/path/cache/dashboard/Nagios UI/source contracts, stopped legacy-renewal removal, session/admin/transport contracts, deterministic package source and embedded SQLite schema-v9 migrations.")
|
||||
print("NOT VERIFIED: Rust parsing, types, compilation, linking or runtime behavior. Run ./dev.sh test.")
|
||||
Regular → Executable
+3
-3
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check the 0.31.5 dependency/source migration; optionally require a resolved lock.
|
||||
"""Check the 0.31.6 dependency/source migration; optionally require a resolved lock.
|
||||
|
||||
This is not a substitute for rustc, cargo metadata or runtime tests.
|
||||
"""
|
||||
@@ -14,7 +14,7 @@ parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--require-lock', action='store_true', help='Require the post-upgrade Cargo.lock produced by cargo update')
|
||||
args = parser.parse_args()
|
||||
manifest = tomllib.loads((ROOT / 'Cargo.toml').read_text())
|
||||
assert manifest['package']['version'] == '0.31.5'
|
||||
assert manifest['package']['version'] == '0.31.6'
|
||||
assert manifest['package']['rust-version'] == '1.99'
|
||||
deps = manifest['dependencies']
|
||||
expected_requirements = {
|
||||
@@ -78,4 +78,4 @@ if missing:
|
||||
print(f'PENDING: lockfile still requires cargo update ({details})')
|
||||
else:
|
||||
print('PASS: pinned key packages found in post-upgrade lock; cargo --locked verification is still required')
|
||||
print('PASS: 0.31.5 source dependency APIs, manifest, TLS features, toolchain images and Swagger pin (static only)')
|
||||
print('PASS: 0.31.6 source dependency APIs, manifest, TLS features, toolchain images and Swagger pin (static only)')
|
||||
Regular → Executable
+3
-2
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.31.5 release."""
|
||||
"""Rust-independent documentation/Reader consistency checks for the 0.31.6 release."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
from rust_source import rust_source
|
||||
@@ -30,6 +30,7 @@ required = [
|
||||
DOCS / "V0.25.1_AUDIT.md",
|
||||
DOCS / "V0.25.0_AUDIT.md",
|
||||
DOCS / "IMPLEMENTATION_0.31.5.md",
|
||||
DOCS / "IMPLEMENTATION_0.31.6.md",
|
||||
DOCS / "IMPLEMENTATION_0.31.3.md",
|
||||
DOCS / "IMPLEMENTATION_0.31.1.md",
|
||||
DOCS / "TEST_REPORT.md",
|
||||
@@ -67,7 +68,7 @@ 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.31.5")
|
||||
assert readme.startswith("# RustProxy 0.31.6")
|
||||
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
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Source contract tests only: no Rust compilation, live H3 peer or UI browser.
|
||||
// Keep these checks aligned with Rust unit tests in src/http3/tests.rs and
|
||||
// src/runtime/tests.rs; do not interpret source checks as wire acceptance.
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const read = path => fs.readFileSync(new URL(`../../${path}`, import.meta.url), 'utf8');
|
||||
const h3 = read('src/http3.rs');
|
||||
const identity = read('src/http3/identity.rs');
|
||||
const body = read('src/http3/body.rs');
|
||||
const h3Tests = read('src/http3/tests.rs');
|
||||
const cache = read('src/runtime/types.rs');
|
||||
const cacheTests = read('src/runtime/tests.rs');
|
||||
const css = read('src/admin/web/style.css');
|
||||
const html = read('src/admin/web/index.html');
|
||||
const logs = read('src/admin/web/file-logs.js');
|
||||
const en = JSON.parse(read('langs/en_US.json')).translations;
|
||||
const pl = JSON.parse(read('langs/pl_pl.json')).translations;
|
||||
|
||||
// A peer-initiated RESET_STREAM or STOP_SENDING must not be logged as a
|
||||
// fabricated HTTP 400 when the browser aborts a prior navigation on F5.
|
||||
assert.match(identity, /fn client_cancelled_code\(code: Code\)/);
|
||||
assert.match(identity, /code == Code::H3_REQUEST_CANCELLED \|\| code == Code::H3_NO_ERROR/);
|
||||
assert.match(identity, /StreamError::RemoteTerminate \{ code, \.\. \} if client_cancelled_code\(\*code\)/);
|
||||
assert.match(h3, /Ok\(Err\(error\)\)\s*=>\s*\{\s*if client_cancelled_request\(&error\)/);
|
||||
assert.match(h3, /\} else \{\s*log_protocol_error\(\s*&state, "http3_request_error", error, peer, local, 400/);
|
||||
assert.match(h3, /"http3_invalid_headers"[^\n]*\n\s*peer, local, 400/);
|
||||
assert.match(h3, /error\.downcast_ref::<h3::error::StreamError>\(\)\s*\.is_some_and\(client_cancelled_request\)/);
|
||||
assert.match(h3, /if !cancelled \{[\s\S]*?send\.stop_stream\(Code::H3_INTERNAL_ERROR\)/);
|
||||
const bodyDrop = body.slice(body.indexOf('impl Drop for ReceiveBody'));
|
||||
assert.ok(!bodyDrop.includes('stop_sending('), 'Do not reintroduce a receive-stream Drop panic');
|
||||
assert.match(h3Tests, /client_cancelled_code\(Code::H3_MESSAGE_ERROR\)/);
|
||||
|
||||
// H2/H3 client transport selection is not a cached representation property.
|
||||
const revision = cache.slice(cache.indexOf('fn cache_revision('), cache.indexOf('fn cache_invalidation('));
|
||||
assert.match(revision, /for vhost in &mut vhosts/);
|
||||
assert.match(revision, /vhost\.http2_enabled = true;/);
|
||||
assert.match(revision, /vhost\.http3_enabled = true;/);
|
||||
assert.ok(revision.indexOf('vhost.http3_enabled = true;') < revision.indexOf('serde_json::to_vec('));
|
||||
assert.match(cacheTests, /cache_invalidation\(&base, &protocol_only\), CacheInvalidation::None/);
|
||||
assert.match(cacheTests, /cache_invalidation\(&protocol_only, &base\), CacheInvalidation::None/);
|
||||
assert.match(cacheTests, /cache_revision\(&both_protocols\), identity/);
|
||||
assert.match(cacheTests, /CacheInvalidation::All\(value\) if value == cache_identity/);
|
||||
|
||||
// Every row is still keyboard-expandable and all selected columns are
|
||||
// represented on the mobile card rather than removed by display:none.
|
||||
assert.match(html, /id="panel-filelogs"/);
|
||||
assert.match(logs, /row\.tabIndex = 0/);
|
||||
assert.match(logs, /row\.setAttribute\("aria-expanded"/);
|
||||
assert.match(logs, /event\.key === "Enter" \|\| event\.key === " "/);
|
||||
assert.match(logs, /cell\.dataset\.mobileColumn = id/);
|
||||
assert.match(logs, /cell\.dataset\.mobileLabel = label/);
|
||||
assert.match(logs, /if \(selectedIndex === index\) nodes\.push\(detailRow\(entry\)\)/);
|
||||
assert.match(css, /@media \(max-width: 700px\)/);
|
||||
assert.match(css, /#panel-filelogs \.file-logs-table tbody tr:not\(\.file-log-detail-row\) \{\s*display: grid/);
|
||||
assert.match(css, /#panel-filelogs \.file-logs-table tbody tr:not\(\.file-log-detail-row\) > td \{\s*display: block/);
|
||||
assert.match(css, /overflow-wrap: anywhere/);
|
||||
assert.match(css, /#panel-filelogs \.file-logs-table,\s*#panel-filelogs \.file-log-aggregate-table \{[^}]*overflow-x: auto/s);
|
||||
assert.match(css, /#panel-filelogs \.file-log-inline-detail pre \{ max-width: 100%; overflow-x: auto; \}/);
|
||||
|
||||
// Exercise the actual browser code's entry-summary function with a logged
|
||||
// H3 cancellation and a normal access event; this is a JS unit simulation.
|
||||
const start = logs.indexOf(' function entrySummary(entry) {');
|
||||
const end = logs.indexOf(' function nestedValue(', start);
|
||||
assert.ok(start >= 0 && end > start, 'entrySummary function exists');
|
||||
const context = vm.createContext({});
|
||||
vm.runInContext(logs.slice(start, end), context);
|
||||
assert.equal(vm.runInContext('entrySummary({reason:"http3_request_error",error:"Remote reset: H3_REQUEST_CANCELLED"})', context), 'http3_request_error');
|
||||
assert.equal(vm.runInContext('entrySummary({method:"GET",host:"rp.example.test",path:"/admin"})', context), 'GET rp.example.test/admin');
|
||||
|
||||
for (const [language, pack] of [['en_US', en], ['pl_pl', pl]]) {
|
||||
assert.match(pack.administrationPublicOriginHelp, /HTTPS/i, `${language}: origin scheme`);
|
||||
assert.match(pack.administrationPublicOriginHelp, /Origin\/CSRF/i, `${language}: security purpose`);
|
||||
assert.ok(pack.administrationHostsAdvanced.length > 10, `${language}: advanced DNS hosts`);
|
||||
}
|
||||
console.log('PASS: H3 cancellation classification, cache stability, mobile log-card/keyboard contracts and origin help.');
|
||||
console.log('NOT VERIFIED: Rust build, actual QUIC navigation or full authenticated mobile UI.');
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compiler-independent guard for the 0.31.6 H3 navigation patch."""
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
html = (ROOT / 'src/admin/web/index.html').read_text()
|
||||
assert '0.31.6' in (ROOT / "Cargo.toml").read_text()
|
||||
assert '<a class="brand" href="/"' in html, 'logo must remain a true full-page navigation'
|
||||
assert 'addEventListener("click"' not in html.split('<a class="brand" href="/"',1)[1].split('</a>',1)[0]
|
||||
body = (ROOT / 'src/proxy/cache_helpers.rs').read_text()
|
||||
assert 'let navigation_method = method == Method::GET || method == Method::HEAD;' in body
|
||||
assert body.count('std::time::Duration::from_millis(35)') >= 2
|
||||
assert 'let bounded_h3_navigation = request.version() == Version::HTTP_3' in body
|
||||
assert 'Body::new(body::ReplayBody::new(frame, tail))' in body
|
||||
assert '|| method == Method::GET' not in body.split('async fn normalize_h3_h1_unknown_body',1)[1].split('// Ignore a small number',1)[0]
|
||||
h3 = (ROOT / 'src/http3.rs').read_text()
|
||||
assert '"http3_peer_cancelled_before_headers"' in h3
|
||||
assert '"http3_peer_cancelled_after_headers"' in h3
|
||||
assert '"http3_response_write_error"' in h3
|
||||
assert 'stream_id={stream_id}' in h3 and 'body_bytes_sent={sent_body_bytes}' in h3
|
||||
diagnostics = (ROOT / 'src/http3/diagnostics.rs').read_text()
|
||||
assert 'http3_navigation_complete' in diagnostics and 'http3_navigation_started' in diagnostics
|
||||
assert 'Err(error)=>{requests.abort_all();return Err(error.into());}' not in h3
|
||||
rust_tests = (ROOT / 'src/proxy/tests/h3_navigation_tests.rs').read_text()
|
||||
for marker in ('h3_bodyless_get_and_head_are_normalized_for_http1_navigation',
|
||||
'h3_get_with_payload_retains_payload_for_http1_upstream',
|
||||
'h3_stalled_empty_get_does_not_block_navigation',
|
||||
'h3_stalled_cache_probe_bypasses_without_blocking_navigation',
|
||||
'h3_cache_probe_replays_nonempty_get_without_dropping_bytes'):
|
||||
assert marker in rust_tests, marker
|
||||
print('PASS: RustProxy 0.31.6 original logo, H3 empty-GET receive lifecycle, bounded probe, per-stream diagnostics and drain invariants')
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
+2
-2
@@ -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/config/internal_routing.rs", "src/proxy/websocket.rs", "src/proxy/error_fallback.rs", "src/cache/mod.rs", "src/cache/model.rs", "src/cache/lifecycle.rs", "src/cache/cold.rs", "src/cache/dashboard.rs", "src/cache/tickets.rs", "src/cache/lookup.rs", "src/cache/policy.rs", "src/cache/fill.rs", "src/cache/tests.rs", "src/settings.rs", "src/compression.rs", "src/diagnostics.rs", "src/traffic.rs", "src/system_stats/mod.rs", "src/system_stats/linux.rs", "src/vhost.rs", "src/vhost/error_fallback.rs", "src/web_server/mod.rs", "src/web_server/config.rs", "src/web_server/filesystem.rs", "src/web_server/security.rs", "src/web_server/response.rs", "src/web_server/request.rs", "src/web_server/routing.rs", "src/web_server/fastcgi.rs", "src/web_server/htaccess.rs", "src/web_server/serve.rs", "src/web_server/tests.rs", "src/balance.rs", "src/security.rs", "src/admin/web/vhosts/core.js", "src/admin/web/vhosts/health.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/session-resume.js", "src/admin/web/app/preferences.js", "src/admin/web/app/config-history.js", "src/admin/web/app/runtime.js", "src/admin/web/app/settings.js", "src/admin/web/management.js", "src/admin/web/drafts.js", "src/pages.rs", "tools/dev/mock_server.py", "tools/dev/mock_auth.py", "src/forwarding.rs", "src/tinyauth.rs", "src/http3.rs", "src/stream.rs", "src/admin/web/integrations.js", "src/admin/web/traffic-panel.js", "src/prometheus.rs", "src/admin/web/settings.js", "src/admin/web/docker.js", "src/docker.rs", "docker-agent/main.go", "docker-agent/model.go", "docker-agent/docker.go", "docker-agent/network.go", "docker-agent/certificate.go", "docker-agent/server.go", "docker-agent/main_test.go", "docker-agent/rustproxy-docker-agent.service", "docker-agent/install.sh", "docs/DOCKER_DISCOVERY.md", "docs/WEB_SERVER.md", "docs/PHP_FPM.md", "docs/README.md", "docs/TODO.md", "docs/V0.25.5_AUDIT.md", "docs/V0.25.4_AUDIT.md", "docs/V0.25.3_AUDIT.md", "docs/V0.25.2_AUDIT.md", "docs/CHANGELOG_TECHNICAL_ARCHIVE.md", "docs/ROADMAP_HISTORY.md", "tools/checks/docs_check.py", "tools/checks/web_server_contracts.py", "tools/checks/rust_source.py", "tools/checks/rust_module_contracts.py", "tools/checks/source_module_contracts.py", "scripts/runtime/acme_provider.py", "src/basic_auth.rs", "scripts/deploy/deploy.py", "scripts/deploy/deploy_core.py", "scripts/deploy/deploy_runtime.py", "scripts/migration/migrate_npm.py", "scripts/migration/migrate_npm_core.py", "scripts/migration/migrate_npm_api.py", "docs/NPM_MIGRATION.md", "docs/STREAMS.md", "install.sh", "install-debian13.sh", "update.sh", "build.rs", "scripts/build/swagger_assets.py", "src/admin/documentation.rs", "src/admin/openapi.json", "langs/en_US.json", "langs/pl_pl.json", "Dockerfile", ".dockerignore", "docs/CONTAINER.md", "docs/PROMETHEUS.md", "docs/NOT_FOUND_HOSTS.md", "grafana/rustproxy-overview.json"]:
|
||||
if not (root / path).is_file():
|
||||
raise ValueError(f"Incomplete RustProxy 0.31.5 source tree: missing {path}")
|
||||
raise ValueError(f"Incomplete RustProxy 0.31.6 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.31.5 entry point; extract the complete package, not individual files.")
|
||||
raise ValueError("src/lib.rs is not the 0.31.6 entry point; extract the complete package, not individual files.")
|
||||
return messages
|
||||
|
||||
|
||||
|
||||
Regular → Executable
+5
-5
@@ -135,7 +135,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.31.5"' in (target / "Cargo.toml").read_text(encoding="utf-8")
|
||||
assert 'version = "0.31.6"' 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()
|
||||
@@ -146,7 +146,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.31.5"
|
||||
assert manifest["to_version"] == "0.31.6"
|
||||
assert manifest["removed_obsolete_runtime_state"] is True
|
||||
assert not legacy.exists()
|
||||
assert not (backup / "legacy-data").exists()
|
||||
@@ -617,7 +617,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.31.5"' in openapi_0241
|
||||
assert '"RuntimeInformation"' in openapi_0241 and '"version": "0.31.6"' 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
|
||||
@@ -1108,7 +1108,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.31.5"', '"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.31.6"', '"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"']:
|
||||
@@ -1313,7 +1313,7 @@ def main() -> None:
|
||||
source_contract_smoke()
|
||||
container_contract_smoke()
|
||||
grafana_contract_smoke()
|
||||
print("PASS: 0.31.5 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
print("PASS: 0.31.6 SQLite/schema-9/upgrade, local-response 404 hosts, Prometheus/Grafana diagnostics, external Debian-packaging separation, cache/dashboard/logging/Nagios, retained admin/H3/stream/TLS contracts and reproducible source archives.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
@@ -21,7 +21,7 @@ const openapi = json('src/admin/openapi.json');
|
||||
const en = json('langs/en_US.json').translations;
|
||||
const pl = json('langs/pl_pl.json').translations;
|
||||
|
||||
assert.match(manifest, /version = "0\.31\.5"/);
|
||||
assert.match(manifest, /version = "0\.31\.6"/);
|
||||
assert.match(lib, /pub mod user_agent;/);
|
||||
assert.match(model, /user_agent_blocking: crate::user_agent::Policy/);
|
||||
assert.match(vhost, /user_agent_blocking: crate::user_agent::Policy/);
|
||||
@@ -52,7 +52,7 @@ assert.match(fileLogs, /localStorage\.setItem\(CONSOLIDATION_STORAGE_KEY/);
|
||||
assert.match(fileLogs, /aggregate_min_count/);
|
||||
assert.match(fileLogs, /file-log-aggregate-copy/);
|
||||
|
||||
assert.equal(openapi.info.version, '0.31.5');
|
||||
assert.equal(openapi.info.version, '0.31.6');
|
||||
assert.ok(openapi.components.schemas.Config.properties.user_agent_blocking);
|
||||
assert.ok(openapi.components.schemas.Vhost.properties.user_agent_blocking);
|
||||
assert.ok(openapi.components.schemas.FileLogSource.properties.group_id);
|
||||
@@ -68,4 +68,4 @@ for (const pack of [en, pl]) {
|
||||
}
|
||||
}
|
||||
|
||||
console.log('PASS: 0.31.5 UA blocking, copy controls, repeated-value parser, vhost log consolidation, OpenAPI and locales.');
|
||||
console.log('PASS: 0.31.6 UA blocking, copy controls, repeated-value parser, vhost log consolidation, OpenAPI and locales.');
|
||||
Regular → Executable
File mode changed.
@@ -43,7 +43,7 @@ const code = js.slice(js.indexOf(' const runtime = latestStatus?.runtime || {
|
||||
const elements = new Map();
|
||||
const dollar = id => { if (!elements.has(id)) elements.set(id, { textContent: '' }); return elements.get(id); };
|
||||
const status = {
|
||||
version: '0.31.5', uptime_seconds: 4201,
|
||||
version: '0.31.6', uptime_seconds: 4201,
|
||||
runtime: {
|
||||
platform: 'Linux', operating_system: 'Debian GNU/Linux 13', kernel_version: '6.12.0', architecture: 'x86_64',
|
||||
statistics_source: 'systemstat+sysinfo', data_directory: '/var/lib/rustproxy', database_path: '/var/lib/rustproxy/config.sqlite3',
|
||||
@@ -63,7 +63,7 @@ const context = {
|
||||
vm.runInNewContext(code, context);
|
||||
assert.equal(dollar('runtime-operating-system').textContent, 'Debian GNU/Linux 13');
|
||||
assert.equal(dollar('runtime-kernel-version').textContent, '6.12.0');
|
||||
assert.equal(dollar('runtime-version').textContent, '0.31.5');
|
||||
assert.equal(dollar('runtime-version').textContent, '0.31.6');
|
||||
assert.equal(dollar('runtime-logical-cpus').textContent, '8');
|
||||
assert.equal(dollar('runtime-cpu-usage').textContent, '17.2%');
|
||||
assert.equal(dollar('runtime-python').textContent, 'Available · 3.13.5');
|
||||
@@ -124,4 +124,4 @@ for (const key of ['python_version','pip_version','certbot_version']) {
|
||||
assert.ok(runtimeSchema.properties.certificate_runtime.properties[key]);
|
||||
assert.equal(runtimeSchema.properties.certificate_runtime.properties[key].nullable, true);
|
||||
}
|
||||
console.log('PASS: 0.31.5 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
|
||||
console.log('PASS: 0.31.6 legend palette, About cleanup, system tab/navigation, read-only API schema and simulated status rendering.');
|
||||
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Regular → Executable
File mode changed.
Reference in new issue
Block a user